Connect your sales
App SDKs
Copy-paste SDK files for iPhone, Android and React Native to count installs from partner links.
Add one file to your app so Bridgly can count installs that came from a partner's link, and give you the click id to send with the sign-up. Each SDK is a single file with no dependencies. Copy it into your project.
Read Apps and sign-ups first for how installs, sign-ups and payouts fit together.
Before you start
- Create an App key under Settings → Workspace → API keys → App key. It starts with
pk_live_. - The App key is safe inside your app: it can only report installs and look up the link someone tapped. Never put your server key (
ak_live_…) in an app.
What every SDK does
- On first launch, it tells Bridgly the app was installed, with the link it came from if there was one.
- When a Bridgly link opens the app, it reads the click id from the link.
- It keeps that click id for you. Send it to your server with the new account, and include it as
clickIdwhen you report the sign-up. - It never collects names, emails, advertising ids or IP-based fingerprints. It creates a random install id that isn't tied to the person or the device.
iPhone (Swift)
Add Bridgly.swift to your Xcode project (iOS 13+).
import SwiftUI
@main
struct MyApp: App {
init() {
Bridgly.configure(appKey: "pk_live_…") { result in
// result.deepLinkPath: in-app page to open, if you set one
// result.clickId: send to your server with the sign-up
}
}
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in Bridgly.handle(url: url) }
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
if let url = activity.webpageURL { Bridgly.handle(url: url) }
}
}
}
}
On iPhone, new installs are matched when the app is opened from a partner link (universal link), or when you turn on the "Get the app" page in Payouts → Settings. If you turn it on, use Bridgly.configure(appKey: "pk_live_…", readClipboardOnFirstOpen: true). iOS shows a paste prompt the first time.
The SDK file, Bridgly.swift:
import Foundation
#if canImport(UIKit)
import UIKit
#endif
/// Bridgly app SDK.
///
/// Counts app installs that came from a partner's Bridgly link, so the brand
/// can pay the partner per install, and returns the click id so the app can
/// pass it along when it reports the sign-up.
///
/// // AppDelegate / App init
/// Bridgly.configure(appKey: "pk_live_…")
///
/// // When a Bridgly link opens the app (universal link):
/// Bridgly.handle(url: url)
///
/// No IP or device fingerprinting. Matching is exact:
/// - the app was opened by a Bridgly link (universal link), or
/// - (optional) the brand turned on the iPhone "Get the app" page and the
/// user allowed paste: the link is read from the clipboard on first open.
public enum Bridgly {
public struct Result: Equatable {
/// Bridgly click id of the partner link, if the open came from one.
public let clickId: String?
/// In-app path to route to (deferred deep link), if configured.
public let deepLinkPath: String?
/// True when this install earned the partner a commission.
public let installCredited: Bool
}
static var appKey: String?
static var baseURL = URL(string: "https://www.bridgly.co")!
static let installIdKey = "co.bridgly.installId"
static let firstOpenDoneKey = "co.bridgly.firstOpenDone"
static let clickIdKey = "co.bridgly.clickId"
/// Call once at launch. `readClipboardOnFirstOpen` only matters if the
/// brand turned on the "Get the app" page in Bridgly; iOS shows a paste
/// prompt when it is read.
public static func configure(
appKey: String,
baseURL: URL? = nil,
readClipboardOnFirstOpen: Bool = false,
completion: ((Result) -> Void)? = nil
) {
self.appKey = appKey
if let baseURL = baseURL { self.baseURL = baseURL }
let defaults = UserDefaults.standard
guard !defaults.bool(forKey: firstOpenDoneKey) else { return }
var clipboard: String?
#if canImport(UIKit) && os(iOS)
if readClipboardOnFirstOpen, UIPasteboard.general.hasURLs || UIPasteboard.general.hasStrings {
let text = UIPasteboard.general.string
if let text = text, text.contains("bridgly_click_id=") { clipboard = text }
}
#endif
report(firstOpen: true, url: nil, clipboard: clipboard) { result in
defaults.set(true, forKey: firstOpenDoneKey)
completion?(result)
}
}
/// Call when a Bridgly universal link opens the app.
@discardableResult
public static func handle(url: URL, completion: ((Result) -> Void)? = nil) -> Bool {
guard let clickId = clickId(from: url.absoluteString) else { return false }
UserDefaults.standard.set(clickId, forKey: clickIdKey)
let firstOpen = !UserDefaults.standard.bool(forKey: firstOpenDoneKey)
report(firstOpen: firstOpen, url: url.absoluteString, clipboard: nil) { result in
if firstOpen { UserDefaults.standard.set(true, forKey: firstOpenDoneKey) }
completion?(result)
}
return true
}
/// Click id of the last Bridgly link that opened this app (nil if none).
/// Send it to your server and include it as `clickId` when you report the
/// sign-up (POST /api/track/lead with your server key).
public static var clickId: String? { UserDefaults.standard.string(forKey: clickIdKey) }
/// Random per-install id (not tied to the device or user).
public static var installId: String {
let defaults = UserDefaults.standard
if let id = defaults.string(forKey: installIdKey) { return id }
let id = UUID().uuidString.replacingOccurrences(of: "-", with: "").lowercased()
defaults.set(id, forKey: installIdKey)
return id
}
// MARK: - internals
static func clickId(from text: String) -> String? {
let decoded = text.removingPercentEncoding ?? text
guard let range = decoded.range(of: "bridgly_click_id=") else { return nil }
let candidate = String(decoded[range.upperBound...].prefix(36)).lowercased()
let pattern = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
return candidate.range(of: pattern, options: .regularExpression) != nil ? candidate : nil
}
static func requestBody(firstOpen: Bool, url: String?, clipboard: String?) -> [String: Any] {
var body: [String: Any] = ["installId": installId, "platform": "ios", "firstOpen": firstOpen]
if let url = url { body["url"] = url }
if let clipboard = clipboard { body["clipboard"] = clipboard }
return body
}
static func report(firstOpen: Bool, url: String?, clipboard: String?, completion: @escaping (Result) -> Void) {
let empty = Result(clickId: nil, deepLinkPath: nil, installCredited: false)
guard let appKey = appKey else { completion(empty); return }
var request = URLRequest(url: baseURL.appendingPathComponent("api/app/v1/open"))
request.httpMethod = "POST"
request.setValue("Bearer \(appKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try? JSONSerialization.data(withJSONObject: requestBody(firstOpen: firstOpen, url: url, clipboard: clipboard))
request.timeoutInterval = 10
URLSession.shared.dataTask(with: request) { data, _, _ in
guard
let data = data,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
else { DispatchQueue.main.async { completion(empty) }; return }
let clickId = json["clickId"] as? String
if let clickId = clickId { UserDefaults.standard.set(clickId, forKey: clickIdKey) }
let deep = (json["deepLink"] as? [String: Any])?["path"] as? String
let credited = ((json["install"] as? [String: Any])?["credited"] as? Bool) ?? false
let result = Result(clickId: clickId, deepLinkPath: deep, installCredited: credited)
DispatchQueue.main.async { completion(result) }
}.resume()
}
}
Android (Kotlin)
- Add the Play Install Referrer library to your app's
build.gradle:
dependencies {
implementation("com.android.installreferrer:installreferrer:2.2")
}
- Add
Bridgly.ktto your app (packageco.bridgly.sdk, Android 5.0+). - Call it at startup, and from the Activity your app links open:
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
Bridgly.configure(this, "pk_live_…")
}
}
// In the Activity that handles your app links:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Bridgly.handle(this, intent?.data)
}
Your app needs the INTERNET permission (most apps already have it).
The SDK file, Bridgly.kt:
package co.bridgly.sdk
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
import android.os.Handler
import android.os.Looper
import org.json.JSONObject
import java.net.HttpURLConnection
import java.net.URL
import java.net.URLDecoder
import java.util.UUID
import java.util.concurrent.Executors
/**
* Bridgly app SDK for Android.
*
* Counts installs from a partner's Bridgly link (so the brand can pay per
* install) and returns the click id so the app can include it when it reports
* the sign-up.
*
* // Application.onCreate
* Bridgly.configure(this, "pk_live_…")
*
* // Activity opened by a Bridgly app link
* Bridgly.handle(this, intent.data)
*
* Exact matching only: Bridgly adds bridgly_click_id to Play Store links and
* Google Play hands it to the app via the Install Referrer API. Add
* `com.android.installreferrer:installreferrer` to your app; the SDK uses it
* by reflection, so it isn't a hard dependency.
*/
object Bridgly {
data class Result(val clickId: String?, val deepLinkPath: String?, val installCredited: Boolean)
private const val PREFS = "co.bridgly.sdk"
private const val KEY_INSTALL_ID = "installId"
private const val KEY_FIRST_OPEN_DONE = "firstOpenDone"
private const val KEY_CLICK_ID = "clickId"
private val CLICK_ID_RE = Regex("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
private val io = Executors.newSingleThreadExecutor()
private val main = Handler(Looper.getMainLooper())
@Volatile private var appKey: String? = null
@Volatile private var baseUrl: String = "https://www.bridgly.co"
@JvmStatic
@JvmOverloads
fun configure(context: Context, appKey: String, baseUrl: String? = null, callback: ((Result) -> Unit)? = null) {
this.appKey = appKey
if (baseUrl != null) this.baseUrl = baseUrl.trimEnd('/')
val prefs = prefs(context)
if (prefs.getBoolean(KEY_FIRST_OPEN_DONE, false)) return
readInstallReferrer(context.applicationContext) { referrer ->
report(context, firstOpen = true, url = null, installReferrer = referrer) { result ->
prefs.edit().putBoolean(KEY_FIRST_OPEN_DONE, true).apply()
callback?.invoke(result)
}
}
}
/** Call from the Activity a Bridgly app link opens. Returns true if it was a Bridgly link. */
@JvmStatic
@JvmOverloads
fun handle(context: Context, uri: Uri?, callback: ((Result) -> Unit)? = null): Boolean {
val id = uri?.toString()?.let { clickIdFrom(it) } ?: return false
val prefs = prefs(context)
prefs.edit().putString(KEY_CLICK_ID, id).apply()
val firstOpen = !prefs.getBoolean(KEY_FIRST_OPEN_DONE, false)
report(context, firstOpen, uri.toString(), null) { result ->
if (firstOpen) prefs.edit().putBoolean(KEY_FIRST_OPEN_DONE, true).apply()
callback?.invoke(result)
}
return true
}
/** Click id of the Bridgly link this install came from (null if organic). */
@JvmStatic
fun clickId(context: Context): String? = prefs(context).getString(KEY_CLICK_ID, null)
/** Random per-install id; not tied to the device or user. */
@JvmStatic
fun installId(context: Context): String {
val prefs = prefs(context)
prefs.getString(KEY_INSTALL_ID, null)?.let { return it }
val id = UUID.randomUUID().toString().replace("-", "")
prefs.edit().putString(KEY_INSTALL_ID, id).apply()
return id
}
@JvmStatic
fun clickIdFrom(text: String): String? {
val decoded = try { URLDecoder.decode(text, "UTF-8") } catch (e: Exception) { text }
val idx = decoded.indexOf("bridgly_click_id=")
if (idx < 0) return null
val candidate = decoded.substring(idx + "bridgly_click_id=".length).take(36).lowercase()
return if (CLICK_ID_RE.matches(candidate)) candidate else null
}
private fun prefs(context: Context): SharedPreferences =
context.applicationContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
/** Play Install Referrer via reflection (optional dependency). */
private fun readInstallReferrer(context: Context, done: (String?) -> Unit) {
try {
val clientClass = Class.forName("com.android.installreferrer.api.InstallReferrerClient")
val listenerClass = Class.forName("com.android.installreferrer.api.InstallReferrerStateListener")
val builder = clientClass.getMethod("newBuilder", Context::class.java).invoke(null, context)
val client = builder.javaClass.getMethod("build").invoke(builder)
var finished = false
val proxy = java.lang.reflect.Proxy.newProxyInstance(listenerClass.classLoader, arrayOf(listenerClass)) { _, method, args ->
if (finished) return@newProxyInstance null
when (method.name) {
"onInstallReferrerSetupFinished" -> {
finished = true
val code = args?.getOrNull(0) as? Int
var referrer: String? = null
if (code == 0) {
try {
val details = client.javaClass.getMethod("getInstallReferrer").invoke(client)
referrer = details.javaClass.getMethod("getInstallReferrer").invoke(details) as? String
} catch (_: Exception) { }
}
try { client.javaClass.getMethod("endConnection").invoke(client) } catch (_: Exception) { }
done(referrer)
}
"onInstallReferrerServiceDisconnected" -> { finished = true; done(null) }
}
null
}
client.javaClass.getMethod("startConnection", listenerClass).invoke(client, proxy)
} catch (e: Exception) {
done(null) // library not added: organic open
}
}
private fun report(context: Context, firstOpen: Boolean, url: String?, installReferrer: String?, callback: (Result) -> Unit) {
val key = appKey
val empty = Result(null, null, false)
if (key == null) { main.post { callback(empty) }; return }
val body = JSONObject()
.put("installId", installId(context))
.put("platform", "android")
.put("firstOpen", firstOpen)
if (url != null) body.put("url", url)
if (installReferrer != null) body.put("installReferrer", installReferrer)
io.execute {
val result = try {
val conn = URL("$baseUrl/api/app/v1/open").openConnection() as HttpURLConnection
conn.requestMethod = "POST"
conn.connectTimeout = 10_000
conn.readTimeout = 10_000
conn.doOutput = true
conn.setRequestProperty("Authorization", "Bearer $key")
conn.setRequestProperty("Content-Type", "application/json")
conn.outputStream.use { it.write(body.toString().toByteArray()) }
val text = (if (conn.responseCode in 200..299) conn.inputStream else conn.errorStream)
?.bufferedReader()?.use { it.readText() } ?: "{}"
val json = JSONObject(text)
val clickId = json.optString("clickId").takeIf { it.isNotEmpty() && it != "null" }
if (clickId != null) prefs(context).edit().putString(KEY_CLICK_ID, clickId).apply()
Result(
clickId,
json.optJSONObject("deepLink")?.optString("path")?.takeIf { it.isNotEmpty() },
json.optJSONObject("install")?.optBoolean("credited") ?: false,
)
} catch (e: Exception) { empty }
main.post { callback(result) }
}
}
}
React Native
Save the file below as bridgly.ts in your app. It is plain TypeScript and works on iPhone and Android.
import AsyncStorage from "@react-native-async-storage/async-storage";
import { Linking, Platform } from "react-native";
import { Bridgly } from "./bridgly";
// At startup
const result = await Bridgly.configure({
appKey: "pk_live_…",
storage: AsyncStorage,
platform: Platform.OS,
// Android: pass the Play Install Referrer string if you read it
// (react-native-play-install-referrer). iPhone: pass the clipboard text
// only if you turned on the "Get the app" page.
});
// When a link opens the app
Linking.addEventListener("url", ({ url }) => Bridgly.handleUrl(url));
const initial = await Linking.getInitialURL();
if (initial) await Bridgly.handleUrl(initial);
// When the user signs up, send this to your server:
const clickId = await Bridgly.clickId();
The SDK file, bridgly.ts:
/**
* Bridgly app SDK for React Native (iOS + Android), pure JS.
*
* import { Bridgly } from "@bridgly/react-native";
* import AsyncStorage from "@react-native-async-storage/async-storage";
* import { Linking } from "react-native";
*
* await Bridgly.configure({ appKey: "pk_live_…", storage: AsyncStorage, platform: Platform.OS });
* Linking.addEventListener("url", ({ url }) => Bridgly.handleUrl(url));
*
* Android install referrer: pass it in if you read it
* (react-native-play-install-referrer): configure({ installReferrer }).
* iPhone clipboard (only if the brand turned on the "Get the app" page):
* configure({ clipboard: await Clipboard.getString() }).
*/
export type BridglyStorage = {
getItem(key: string): Promise<string | null>;
setItem(key: string, value: string): Promise<void>;
};
export type BridglyResult = {
clickId: string | null;
deepLinkPath: string | null;
installCredited: boolean;
};
const CLICK_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const K_INSTALL = "bridgly.installId";
const K_FIRST = "bridgly.firstOpenDone";
const K_CLICK = "bridgly.clickId";
export function clickIdFrom(text: string | null | undefined): string | null {
if (!text) return null;
let decoded = text;
try {
decoded = decodeURIComponent(text);
} catch {
/* keep */
}
const i = decoded.indexOf("bridgly_click_id=");
if (i < 0) return null;
const id = decoded.slice(i + 17, i + 17 + 36).toLowerCase();
return CLICK_ID_RE.test(id) ? id : null;
}
function randomId(): string {
let s = "";
for (let i = 0; i < 32; i++) s += Math.floor(Math.random() * 16).toString(16);
return s;
}
type State = {
appKey: string;
baseUrl: string;
storage: BridglyStorage;
platform: "ios" | "android";
fetchImpl: typeof fetch;
};
let state: State | null = null;
const EMPTY: BridglyResult = { clickId: null, deepLinkPath: null, installCredited: false };
async function installId(s: State): Promise<string> {
const existing = await s.storage.getItem(K_INSTALL);
if (existing) return existing;
const id = randomId();
await s.storage.setItem(K_INSTALL, id);
return id;
}
async function report(
s: State,
body: { firstOpen: boolean; url?: string; installReferrer?: string; clipboard?: string },
): Promise<BridglyResult> {
try {
const res = await s.fetchImpl(`${s.baseUrl}/api/app/v1/open`, {
method: "POST",
headers: { Authorization: `Bearer ${s.appKey}`, "Content-Type": "application/json" },
body: JSON.stringify({ installId: await installId(s), platform: s.platform, ...body }),
});
const json = (await res.json()) as {
clickId?: string | null;
deepLink?: { path?: string | null } | null;
install?: { credited?: boolean } | null;
};
const clickId = json.clickId ?? null;
if (clickId) await s.storage.setItem(K_CLICK, clickId);
return {
clickId,
deepLinkPath: json.deepLink?.path ?? null,
installCredited: Boolean(json.install?.credited),
};
} catch {
return EMPTY;
}
}
export const Bridgly = {
/** Call once at launch. Reports the first open (no-op afterwards). */
async configure(opts: {
appKey: string;
storage: BridglyStorage;
platform: "ios" | "android" | string;
baseUrl?: string;
installReferrer?: string | null;
clipboard?: string | null;
fetchImpl?: typeof fetch;
}): Promise<BridglyResult> {
state = {
appKey: opts.appKey,
storage: opts.storage,
platform: opts.platform === "android" ? "android" : "ios",
baseUrl: (opts.baseUrl ?? "https://www.bridgly.co").replace(/\/$/, ""),
fetchImpl: opts.fetchImpl ?? fetch,
};
if ((await state.storage.getItem(K_FIRST)) === "1") return EMPTY;
const clipboard = opts.clipboard && opts.clipboard.includes("bridgly_click_id=") ? opts.clipboard : undefined;
const result = await report(state, {
firstOpen: true,
installReferrer: opts.installReferrer ?? undefined,
clipboard,
});
await state.storage.setItem(K_FIRST, "1");
return result;
},
/** Call with any URL that opened the app. Returns null if it isn't a Bridgly link. */
async handleUrl(url: string): Promise<BridglyResult | null> {
if (!state || !clickIdFrom(url)) return null;
const firstOpen = (await state.storage.getItem(K_FIRST)) !== "1";
const result = await report(state, { firstOpen, url });
if (firstOpen) await state.storage.setItem(K_FIRST, "1");
return result;
},
/** Click id of the Bridgly link this install came from. Send it with the sign-up. */
async clickId(): Promise<string | null> {
return state ? state.storage.getItem(K_CLICK) : null;
},
};
Report the sign-up
The SDK only counts installs. The sign-up comes from your server, with your server key, so nobody can fake it from the app:
curl -X POST https://www.bridgly.co/api/track/lead \
-H "Authorization: Bearer ak_live_…" \
-H "Content-Type: application/json" \
-d '{"clickId":"<from the SDK>","customerExternalId":"user_123","eventName":"Sign up"}'
Check it works
- Create a partner link in Bridgly and open it on a phone.
- Install your app from the store it sends you to, then open it.
- The install appears under Events within a few seconds. If you pay per install, the commission appears under Payouts.
What the SDK sends
One request to POST https://www.bridgly.co/api/app/v1/open with your App key:
| Field | When |
|---|---|
installId | Always. A random id made once per install. |
platform | Always. ios or android. |
firstOpen | Always. true on the first launch. |
url | When a Bridgly link opened the app. |
installReferrer | Android first launch, from Google Play. |
clipboard | iPhone first launch, only with the "Get the app" page on and only if it contains a Bridgly link. |
Response: { "clickId": "…" | null, "deepLink": { "path": "…" } | null, "install": { "credited": true | false } }.
Stuck? Contact us and a person will reply.
