Docs menu · App SDKs

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

  1. Create an App key under Settings → Workspace → API keys → App key. It starts with pk_live_.
  2. 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 clickId when 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)

  1. Add the Play Install Referrer library to your app's build.gradle:
dependencies {
    implementation("com.android.installreferrer:installreferrer:2.2")
}
  1. Add Bridgly.kt to your app (package co.bridgly.sdk, Android 5.0+).
  2. 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

  1. Create a partner link in Bridgly and open it on a phone.
  2. Install your app from the store it sends you to, then open it.
  3. 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:

FieldWhen
installIdAlways. A random id made once per install.
platformAlways. ios or android.
firstOpenAlways. true on the first launch.
urlWhen a Bridgly link opened the app.
installReferrerAndroid first launch, from Google Play.
clipboardiPhone 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.