# DeepLinkNow: full documentation > Deep links and deferred deep links for iOS, Android, React Native and Flutter, without the attribution and ad tracking of Branch or AppsFlyer. Any path on your domain works as a link. - DeepLinkNow does deep linking only: no attribution, no ad tracking, no advertising IDs, no ATT prompt, no cross-app data. - Any path and query on an app's link domain (yourapp.deeplinknow.com, or a custom domain on paid plans) is a working deep link. Nothing has to be created in a dashboard first. - After an install, the SDK's checkDeferredDeepLink() returns the path and query the user originally tapped. - iOS: the link page copies the link to the clipboard before the App Store; the SDK reads it on first launch (iOS shows an Allow Paste prompt). A scored fingerprint match is the backup. - Android: the click ID goes through the Google Play Install Referrer, which is exact for Play Store installs. A scored fingerprint match is the backup. - Fingerprint matches come with confidence_score (0-100) and match_quality (HIGH, MEDIUM, LOW). An IP match is not required. The developer picks the threshold. - Retention: A deferred link can be claimed for 1 hour after the click, and stored fingerprints are deleted within 30 days. - SDKs: iOS (Swift, SPM and CocoaPods), Android (Kotlin, Maven Central com.deeplinknow:dln-android), React Native and Expo (npm @deeplinknow/react-native, with an Expo Router helper), Flutter (pub.dev deeplinknow, also in FlutterFlow). All on version 1.3; React Native and Flutter wrap the native SDKs. - MCP server for coding agents (npm @deeplinknow/mcp): Claude Code, Cursor, Claude Desktop, VS Code and Windsurf can read these docs, check a link domain and plan a migration. It uses a private key only for its optional link tools. - Apple App Site Association and assetlinks.json are generated and hosted for every app. - Link pages load no analytics or tracking scripts, and no DeepLinkNow domain is on the AdGuard or EasyPrivacy block lists. --- Source: https://deeplinknow.com/docs/quickstart # Quick start guide Get deferred deep linking working in a few minutes: one call to initialize, and one handler that receives the deferred link and every link that opens the installed app. ## Prerequisites 1. A DeepLinkNow account and an app created in the dashboard. Your app gets the link domain `.deeplinknow.com`. 2. Your app's **public** API key (`dln_pub_…`) from the dashboard's **API Keys** page. Creating an app issues one public key (for the SDKs) and one private key (for your server only). The public key is always shown on the API Keys page. The private key is shown once; if you lose it, regenerate it there. 3. Your iOS **Team ID** and **Bundle ID**, and your Android **package name** and **SHA-256 certificate fingerprints**, entered in the app's settings so DeepLinkNow can serve the `.well-known` files. Any path on your link domain works as a deep link with no setup: `https://.deeplinknow.com/products/123?ref=newsletter` delivers `/products/123` with `ref=newsletter` to your app. To try an SDK before creating an app, use the demo key `test_api_key`. It returns mock responses. ## Choose your platform - [React Native / Expo](#react-native) - [iOS (Swift)](#ios-swift) - [Android (Kotlin)](#android-kotlin) - [Flutter](#flutter) Building with a coding agent? Use DeepLinkNow from Claude Code, Cursor or VS Code: [install the MCP server](/docs/mcp) in one click or one command (no key needed), then ask the agent to add DeepLinkNow. It reads these docs and checks your link domain. ## React Native ```bash npx expo install @deeplinknow/react-native # Expo (development build) npm install @deeplinknow/react-native && cd ios && pod install # bare React Native ``` With Expo, add the config plugin with your link domains (`["@deeplinknow/react-native", { "domains": [".deeplinknow.com"] }]`), which sets up Associated Domains and the Android intent filter. In bare React Native, add `applinks:.deeplinknow.com` and an `autoVerify` https intent filter by hand (see the [React Native docs](/docs/react-native)). Then: ```typescript import DeepLinkNow from "@deeplinknow/react-native"; DeepLinkNow.onLink( (link) => { // link.type: "deferred" (first launch after install) or "direct" // link.source: "clipboard" (iOS), "referrer" (Android), "fingerprint", or how a direct open arrived router.push({ pathname: link.path, params: link.queryParams }); }, { apiKey: "dln_pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } ); ``` `onLink` initializes the SDK, runs the first-launch check and reads `Linking` itself, so there's no other code to write. In bare React Native, pass `schemes: ["myapp"]` if you also want your custom-scheme links. `checkDeferredDeepLink()` still works if you only want the deferred link. Using Expo Router? Resolve the link in `app/+native-intent.tsx` with `@deeplinknow/react-native/expo-router` instead, so the app opens straight on the right screen. See [Expo Router](/docs/react-native#expo-router). Next: [Full React Native documentation](/docs/react-native) ## iOS (Swift) ```ruby pod 'DeepLinkNow', '~> 1.4' ``` Or add `https://github.com/deeplinknow/dln-ios` with Swift Package Manager. Add `applinks:.deeplinknow.com` under **Signing & Capabilities → Associated Domains**. Then: ```swift import DeepLinkNow Task { await DeepLinkNow.initialize(config: DLNConfig(apiKey: "dln_pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")) } // Runs the first-launch check (the same as checkDeferredDeepLink()) and gets every later open DeepLinkNow.onLink { link in // Deferred: pasteboard first (exact), then fingerprint (scored) router.open(path: link.path, query: link.queryParams) } ``` Hand Universal Links that open the installed app to the SDK: ```swift .onOpenURL { url in DeepLinkNow.handle(url) } ``` Next: [Full iOS documentation](/docs/ios) ## Android (Kotlin) ```kotlin dependencies { implementation("com.deeplinknow:dln-android:1.4.1") } ``` Add an `autoVerify` https intent filter for `.deeplinknow.com` to your link-handling activity. Then: ```kotlin // Application.onCreate DLN.init(context = this, apiKey = "dln_pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") DLN.onLink { link -> // Deferred: Install Referrer first (exact), then fingerprint (scored) router.open(link.path, link.queryParams) } // The activity with your App Link intent filter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (savedInstanceState == null) DLN.handleIntent(intent) } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) DLN.handleIntent(intent) } ``` `DLN.checkDeferredDeepLink()` still works if you only want the deferred link. Next: [Full Android documentation](/docs/android) ## Flutter ```bash flutter pub add deeplinknow ``` The plugin wraps the native iOS and Android SDKs, which Swift Package Manager or CocoaPods and Gradle fetch for you. Add the Associated Domains entitlement and the `autoVerify` intent filter as for the native SDKs. Then: ```dart import 'package:deeplinknow/deeplinknow.dart'; DeepLinkNow.links.listen((link) { // link.type: deferred or direct router.go(link.fullPath); }); DeepLinkNow.listen(); await DeepLinkNow.initialize('dln_pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'); ``` Set `FlutterDeepLinkingEnabled` to `NO` in `Info.plist` and `flutter_deeplinking_enabled` to `false` in `AndroidManifest.xml` so Flutter doesn't route the same link too (see the [Flutter docs](/docs/flutter#every-open-in-one-stream)). `checkDeferredDeepLink()` still works if you only want the deferred link. Next: [Full Flutter documentation](/docs/flutter) ## How the deferred link is found | | iOS | Android | |---|---|---| | Step 1 (exact) | Pasteboard: the landing page copied the tapped link | Play Install Referrer: the landing page passed a click ID to the Play Store | | Step 2 (best effort) | Fingerprint match | Fingerprint match | | Result | `DeferredDeepLink` with `source`, `confidenceScore`, `matchQuality` | same | Every SDK returns the same `DeferredDeepLink` shape: `path`, `queryParams`, `fullPath`, `url`, `source`, `confidenceScore` (0–100) and `matchQuality` (`HIGH` / `MEDIUM` / `LOW`). Exact sources report `100` / `HIGH`. The `DLNLink` that `onLink` delivers adds `type` (deferred or direct), and links that open the installed app report `100` / `HIGH` too. ### About fingerprint matching - The server compares the device with clicks on your link pages from the last 1 hour and returns scored candidates. Each has a `confidence_score` (0–100) and a `match_quality`: `HIGH` (75+), `MEDIUM` (50–74), `LOW` (30–49). - An IP match is not required, because iCloud Private Relay and carrier NAT often give the app a different IP from the browser. - The time score decays across the 1-hour claim window, and missing data never counts as a match. - You choose the threshold. The optional `minConfidence` filter is off by default, so you get the server's best candidate and decide what to do with its `matchQuality`. ## Troubleshooting ### Links open the browser instead of the app - Check the dashboard settings (Team ID + Bundle ID, package name + SHA-256 fingerprints). - Open `https://.deeplinknow.com/.well-known/apple-app-site-association` and `/.well-known/assetlinks.json`. Changes can take up to 5 minutes to appear. - iOS caches the association file when the app is installed. Reinstall after changing it. - Android: run `adb shell pm get-app-links `. ### No deferred link on first launch - Make sure you register `onLink` (or call `checkDeferredDeepLink()`) at launch, and that the install happened within 1 hour of the click for fingerprint matching. - It returns a link only once per install. Call `resetDeferredDeepLink()` (or pass `force: true`) to test again. - Turn on `enableLogs` to see each step's outcome. - On iOS, tapping **Don't Allow** on the paste prompt skips the pasteboard step; fingerprint matching still runs. ## Need help? - Email: support@deeplinknow.com - [Integration checklist](/docs/integration-checklist) --- Source: https://deeplinknow.com/docs/ios # DeepLinkNow iOS SDK ## Overview When a user taps one of your links before installing your app, the DeepLinkNow iOS SDK (`DeepLinkNow`, Swift) hands your app the path and query they tapped on first launch. It is not an analytics or attribution SDK. It never reads the advertising identifier (IDFA), never triggers the App Tracking Transparency prompt, and needs no ATT consent. On iOS the deferred link comes from two sources, tried in this order: 1. **Pasteboard (exact).** The landing page copies the tapped link before sending the user to the App Store. On first launch the SDK reads it back. 2. **Fingerprint (best effort).** If the pasteboard has no link of yours, the SDK asks the server for scored candidates from clicks on your link pages in the last hour. You get both with one call: `checkDeferredDeepLink()`. From 1.4.0, `onLink` gives you the deferred link and every link that opens the installed app through one handler, so you write your routing once. ## Requirements - iOS 13.0+ - Swift 5.5+ (async/await) - Xcode 13.2+ - No `Info.plist` keys are required. iOS has no pasteboard usage-description key. ## Installation ### Swift Package Manager In Xcode choose **File > Add Package Dependencies...** and enter `https://github.com/deeplinknow/dln-ios`, or add it to `Package.swift`: ```swift .package(url: "https://github.com/deeplinknow/dln-ios.git", from: "1.4.1") ``` ### CocoaPods ```ruby pod 'DeepLinkNow', '~> 1.4' ``` Then run `pod install`. If it says no spec satisfies `DeepLinkNow (~> 1.4)`, your local CocoaPods spec cache is older than the release: run `pod install --repo-update` once. ### Universal Links (Associated Domains) 1. In the dashboard, set your app's **Team ID / App ID prefix** and **Bundle ID**. DeepLinkNow serves `https://.deeplinknow.com/.well-known/apple-app-site-association` (and the same file for a verified custom domain) from those values. Changes are live within 5 minutes. 2. In Xcode, open **Signing & Capabilities → Associated Domains** and add `applinks:.deeplinknow.com`, plus `applinks:` if you have one. 3. Pass incoming URLs to `DeepLinkNow.handle(url)` (see [Handle every link with one handler](#handle-every-link-with-one-handler)). ## Getting started ### Initialize the SDK Use your app's **public** API key (`dln_pub_…`) from the dashboard's **API Keys** page. The SDK endpoints (`/api/v1/sdk/*`) reject private keys, and a private key must never ship inside an app. ```swift import DeepLinkNow let config = DLNConfig( apiKey: "dln_pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", enableLogs: false // true while developing ) do { let app = try await DeepLinkNow.initializeThrowing(config: config) print("Link domains:", app.linkDomains) } catch let error as DeepLinkNowError { // .invalidApiKey, .accountInactive, .noCredits, .rateLimited, .network, .server, // .invalidResponse, .invalidConfiguration print("DeepLinkNow init failed:", error.code) } ``` `DLNConfig` options: | Option | Default | Meaning | |---|---|---| | `apiKey` | — | Your public key | | `enableLogs` | `false` | Debug logging with the `[DeepLinkNow]` prefix. The API key is never logged. | | `baseUrl` | `https://deeplinknow.com` | API base URL. A trailing `/` is fine. | | `timeout` | `10` s | Per request | | `retryAttempts` | `2` | Retries for transient failures (network, timeouts, 5xx, 429) | `await DeepLinkNow.initialize(config:)` never throws: it logs failures instead, and you can check `DeepLinkNow.isInitialized` and `DeepLinkNow.initializationError` (or `DeepLinkNow.lastError`). Initialization never reads the pasteboard and never asks for a permission. A non-https `baseUrl` is refused (http only for localhost). For local testing before you have created an app, you can use the demo key `test_api_key`. It returns mock responses and never touches real data. ### Handle every link with one handler From 1.4.0, `onLink` hands your app the same `DLNLink` whether the user tapped the link before installing (deferred) or the link opened the installed app (direct). This is the recommended setup for new integrations: ```swift // At launch Task { await DeepLinkNow.initialize(config: DLNConfig(apiKey: "dln_pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")) } DeepLinkNow.onLink { link in // runs on the main actor // link.type .deferred (first launch after install) or .direct // link.source .clipboard / .fingerprint for deferred, .universalLink / .scheme for direct router.open(path: link.path, query: link.queryParams) } ``` Then hand every URL that opens the app to the SDK. Where iOS delivers it depends on how your app is built. SwiftUI: ```swift WindowGroup { ContentView() .onOpenURL { url in if !DeepLinkNow.handle(url) { /* not one of your links: route it yourself */ } } .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in DeepLinkNow.handle(activity) } } ``` UIKit with a scene delegate. When the link launches the app from cold, iOS hands it over only in `scene(_:willConnectTo:options:)`, never in the other two methods, so you need all three: ```swift func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // … your window setup … DeepLinkNow.handle(connectionOptions) // Universal Link or custom-scheme URL from a cold launch } func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { DeepLinkNow.handle(userActivity) // Universal Link, app already running } func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { for context in URLContexts { DeepLinkNow.handle(context.url) } // custom scheme, app already running } ``` UIKit without scenes (app delegate only): ```swift func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { DeepLinkNow.handle(userActivity) // Universal Links, cold launch included } func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { DeepLinkNow.handle(url) // custom schemes, cold launch included } ``` `handle(_ userActivity:)` and `handle(_ connectionOptions:)` are new in 1.4.1. On 1.4.0, pass `userActivity.webpageURL`, or each URL in `connectionOptions.userActivities` and `connectionOptions.urlContexts`, to `handle(_ url:)`. Registering the handler runs the first-launch check for you, with the same pasteboard and fingerprint steps and the same once-per-install rule as `checkDeferredDeepLink()`. If you register before `initialize` has finished, the check waits for it. A link that arrives before you register is handed to the handler when you do, and the same URL handed in twice within 3 seconds is delivered once. `handle(_:)` returns `true` for a link on your link domains and for your own URL scheme, and `false` for anything else (`mailto:`, `tel:`, https links on other hosts), which you route yourself. A direct open has `confidenceScore` 100 and `matchQuality` `.high`. Options (`DLNLinkOptions`, passed as `DeepLinkNow.onLink(options: …) { … }`): | Option | Default | Meaning | |---|---|---| | `minConfidence`, `useClipboard`, `force` | `nil`, `true`, `false` | Passed to the first-launch check (see [Get the deferred deep link](#get-the-deferred-deep-link)) | | `hold` | `false` | Keep links in `DeepLinkNow.pendingLinks` instead of calling the handler | | `schemes` | your Info.plist schemes | The custom URL schemes `handle(_:)` accepts, for example `["myapp"]` | | `resolve` | `false` | Look up each Universal Link on the server before delivering it | **Hold a link until the user signs in.** A deferred link often arrives while the user is still on your sign-in screen. Register with `hold: true` and the SDK keeps the latest deferred link and the latest direct one in `DeepLinkNow.pendingLinks` (deferred first; `DeepLinkNow.pendingLink` is the first of them). You can read `pendingLink` to show "Sign in to open your invite". When sign-in finishes, `releasePendingLink()` delivers them to your handler and ends the hold, so later links go straight through. `clearPendingLink()` drops them and keeps holding, for example when the user signs out. ```swift DeepLinkNow.onLink(options: DLNLinkOptions(hold: true)) { link in router.open(path: link.path, query: link.queryParams) } // After sign-in DeepLinkNow.releasePendingLink() ``` **Custom URL schemes.** `myapp://products/123?x=1` arrives with `path` `/products/123` and `source` `.scheme`. By default the SDK accepts the schemes your Info.plist declares under `CFBundleURLTypes` → `CFBundleURLSchemes`. Pass `schemes: ["myapp"]` to list them yourself. **Short links imported from Firebase.** A short link such as `/abc123` means nothing to your app until it's looked up. With `resolve: true`, the SDK calls `GET /api/v1/sdk/resolve` for each Universal Link open and fills `deeplinkId`, `targetUrl`, `campaignId` and `metadata` before delivering it, so `link.targetUrl` holds the original Firebase deep link. It adds a request to every open, so it's off by default. If the lookup fails, times out or finds nothing, you get the link unresolved. `await DeepLinkNow.resolveLink(url)` does the same lookup for one URL. See [Import your old short links](/docs/firebase-migration#import-your-old-short-links). ### Get the deferred deep link `checkDeferredDeepLink()` keeps working, and it's what `onLink` runs on first launch. If you'd rather call it yourself, call it once on launch, after `initialize`: ```swift if let link = await DeepLinkNow.checkDeferredDeepLink() { // link.path "/products/123" // link.queryParams ["ref": .string("newsletter")] // link.source .clipboard or .fingerprint // link.confidenceScore 0-100, link.matchQuality HIGH / MEDIUM / LOW router.open(path: link.path, query: link.queryParams) } // No link? Carry on to your normal start screen. ``` `checkDeferredDeepLink()` never throws. `nil` means there was no link, or the check failed (not initialized, offline, invalid key…). A failure is logged as a warning and kept in `DeepLinkNow.lastError` (a `DeepLinkNowError`: `.notInitialized`, `.network`, `.rateLimited`, …) until the next successful check, and it doesn't use up the first launch, so the SDK tries again on the next call. If you'd rather handle the error yourself, use the throwing variant: ```swift do { if let link = try await DeepLinkNow.checkDeferredDeepLinkThrowing() { router.open(path: link.path, query: link.queryParams) } } catch let error as DeepLinkNowError { print("Deferred check failed:", error.code) } ``` How `checkDeferredDeepLink()` works: 1. **Pasteboard**: if the pasteboard holds a link on one of your link domains (`.deeplinknow.com` or your verified custom domain), it is parsed locally and returned with `source = .clipboard`, `confidenceScore = 100`, `matchQuality = .high`. No server match call is made. From 1.4.0, a copied link whose click has already been used (for example, one left on the pasteboard from an earlier install) isn't delivered, and the SDK moves on to fingerprinting. 2. **Fingerprint**: otherwise the SDK calls `POST /api/v1/sdk/match` and returns the best candidate the server sent, with the server's `confidence_score` and `match_quality` unchanged (`source = .fingerprint`). It returns a link at most once per install. After a definitive answer (a link, or nothing found) later calls return `nil` without touching the pasteboard or the network. Errors don't count, so an offline first launch can still succeed on the next launch. Options (`DLNDeferredOptions`): | Option | Default | Meaning | |---|---|---| | `minConfidence` | `nil` (off) | Discard fingerprint candidates whose `confidence_score` is below this value. Pasteboard results are exact and are never filtered. | | `useClipboard` | `true` | Read the pasteboard first. Set `false` to skip it and rely on fingerprinting only. | | `force` | `false` | Ignore the "already checked" flag (useful while testing). | ```swift let link = await DeepLinkNow.checkDeferredDeepLink( DLNDeferredOptions(minConfidence: 50, useClipboard: true) ) ``` `DeepLinkNow.resetDeferredDeepLink()` clears the first-launch flag so you can test the flow again without reinstalling. ### The result: `DLNDeferredDeepLink` | Field | Meaning | |---|---| | `path` | The tapped path up to the first `?`, not decoded, always starting with `/` | | `queryParams` | Query parameters with `URLSearchParams` semantics: a string, a flag (`?k`), or a list for repeated keys | | `fullPath` | Path and query exactly as tapped | | `url` | Absolute https URL of the link | | `source` | `.clipboard` or `.fingerprint` (`.referrer` exists for parity with Android) | | `confidenceScore` | `0`–`100`. Server score for fingerprint results, `100` for the pasteboard | | `matchQuality` | `HIGH` (75+), `MEDIUM` (50–74) or `LOW` (30–49) | | `deeplinkId`, `targetUrl`, `campaignId`, `metadata`, `matchedAt`, `expiresAt` | From the server (fingerprint results) | | `matchDetails` | Score breakdown (fingerprint results) | The `DLNLink` that `onLink` delivers has the same fields plus `type` (`.deferred` or `.direct`), and its `source` can also be `.universalLink` or `.scheme`. ## How fingerprint matching works Fingerprint matching is the backup for when the pasteboard is empty, the user denied the paste prompt, or you set `useClipboard: false`. - The landing page stores a small description of the click: IP address, device class, OS version, screen size, language, time zone, and the path and query that were tapped. - On first launch the SDK sends the same kind of description of the device. The server compares it with clicks on **your** app's link pages from the **last 1 hour** and returns up to 10 candidates, best first. - Each candidate has a `confidence_score` (0–100) and a `match_quality`: `HIGH` (75+), `MEDIUM` (50–74) or `LOW` (30–49). Candidates below 30 are not returned. - **An IP match is not required.** iCloud Private Relay and carrier NAT often give the app a different IP from Safari, so the server also considers clicks from the same device class with a matching screen or OS version. - Score weights: IP 40, device 27 (screen 15, OS version 12), time 18, locale 15 (time zone 10, language 5). Device points count only when the platform and device class agree (an iPad click never scores for an iPhone). When the browser couldn't report its real iOS version (Chrome for iOS, in-app browsers such as Instagram's), a compatible OS version earns 6 instead of 12. Missing data never counts as agreement. - The time score decays linearly across the 1-hour claim window: full marks within 5 minutes, half at about 32 minutes, zero at 60 minutes. - So without an IP match, a match where everything else agrees scores 60 (MEDIUM), or 54 from Chrome for iOS or an in-app browser. Each match in the `/sdk/match` response carries `score_version` (`2` since October 2026). If you tuned `minConfidence` on the old weights (IP 40, device 30, time 20, locale 10), check it again: Chrome for iOS and in-app browser matches without an IP match moved from 60 to 54. - **You choose the threshold.** `checkDeferredDeepLink()` returns the server's best candidate as is. The `minConfidence` filter is off by default. Use `matchQuality` or `confidenceScore` to decide, for example, to route `HIGH` matches straight away and confirm `MEDIUM` or `LOW` ones with the user. Once a device gets a `MEDIUM` or better match, the click is claimed for that device, so another phone on the same Wi-Fi doesn't receive it too. ## The pasteboard and the paste prompt On iOS 16+, reading a link that Safari copied shows the system prompt **"App would like to paste from Safari"**. The SDK keeps this to a minimum: - It reads the pasteboard only during `checkDeferredDeepLink()` on first launch (or when you call `checkClipboard()` yourself), never at init and never in the background. - On iOS 14+ it first checks for a web URL with `detectPatterns`, which shows no prompt. If there is no URL, it skips straight to fingerprinting. - If the user taps **Don't Allow**, that counts as "nothing found" and the SDK continues with fingerprinting. - It never writes to or clears the pasteboard, and never logs content that is not one of your links. If you don't want the prompt, pass `useClipboard: false`, or show your own "Paste your link" button with `UIPasteControl` (iOS 16+) and pass the result to `parseLink`. ## Handling links that open the app When the app is already installed, a Universal Link opens it directly. With `onLink`, pass the URL to `DeepLinkNow.handle(url)` and your handler gets it (see [Handle every link with one handler](#handle-every-link-with-one-handler)). Without a handler, parse it with `parseLink`, which keeps working and returns `nil` for URLs that are not on your link domains: ```swift // SwiftUI .onOpenURL { url in if let link = DeepLinkNow.parseLink(url) { router.open(path: link.path, query: link.queryParams) } } // UIKit (SceneDelegate) func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL, let link = DeepLinkNow.parseLink(url) else { return } router.open(path: link.path, query: link.queryParams) } ``` Deferred results and direct opens use the same parsing rules, so you can route both the same way. ## Low-level API The building blocks behind `checkDeferredDeepLink()` stay available: | Method | Purpose | |---|---| | `findDeferredUserThrowing()` / `findDeferredUser()` | Every fingerprint candidate from `/api/v1/sdk/match`, unfiltered: `confidence_score`, `match_quality`, `match_details` and `deeplink` (with `full_path` and `query_params`), plus `ttl_seconds`. `findDeferredUser()` returns `nil` on any error; `findDeferredUserThrowing()` throws a `DeepLinkNowError` | | `checkClipboard()` | The pasteboard text if it is exactly one URL on your link domains, else `nil` (may show the paste prompt) | | `hasDeepLinkToken()` / `hasDeepLinkTokenAsync()` | Cheap "is there probably a URL on the pasteboard" check that never reads the content and shows no prompt | | `parseLink(_:)` | Validate and parse a URL on your link domains into `DLNParsedLink` (`path`, `queryParams`, `fullPath`, `url`) | | `parseDeepLink(_:)` | Older tuple form `(path, parameters)` of `parseLink(_:)` | | `isValidDomain(_:)` | The host rule: exactly `.deeplinknow.com` or a verified custom domain, case-insensitive | | `createDeepLink(path:customParameters:)` | Build an https link on your primary link domain that `parseLink` round-trips (`nil` before init) | | `isInitialized`, `initializationError`, `appInfo`, `sdkVersion` | Init state and SDK version (`"1.4.1"`) | If you call these yourself, check the pasteboard before fingerprinting: ```swift if let copied = DeepLinkNow.checkClipboard(), let url = URL(string: copied), let link = DeepLinkNow.parseLink(url) { router.open(path: link.path, query: link.queryParams) } else if let response = try? await DeepLinkNow.findDeferredUserThrowing(), let best = response.matches.first(where: { $0.deeplink != nil }) { // best.confidenceScore and best.matchQuality: your call } ``` ## Privacy The SDK sends only what matching needs: a synthesized user agent, platform, OS version, device class (`iPhone`/`iPad`), screen size in points with scale, language, time zone, and the app-scoped vendor ID (IDFV), which the server uses only to claim a click for this install. `advertising_id` is always `null`. The server reads the IP from the connection. The SDK stores nothing on the device except the first-launch flag, and ships a privacy manifest (`PrivacyInfo.xcprivacy`) with tracking set to false. ## API endpoints used | Endpoint | Key | Used by | |---|---|---| | `POST /api/v1/sdk/init` | public | `initialize`: validates the key and account, returns your link domains | | `POST /api/v1/sdk/match` | public | `checkDeferredDeepLink` (fingerprint step), `findDeferredUser` | | `GET /api/v1/sdk/resolve` | public | `onLink` with `resolve: true`, `resolveLink` | `init`, `match` and `resolve` are exempt from per-minute rate limits, and `init` counts toward monthly active users. ## Support - Email: support@deeplinknow.com - Source and issues: [github.com/deeplinknow/dln-ios](https://github.com/deeplinknow/dln-ios) --- Source: https://deeplinknow.com/docs/android # DeepLinkNow Android SDK ## Overview When a user taps one of your links before installing your app, the DeepLinkNow Android SDK (`com.deeplinknow:dln-android`, Kotlin, class `com.deeplinknow.DLN`) hands your app the path and query they tapped on first launch. It is not an analytics or attribution SDK. It never reads the Google Advertising ID (GAID) and needs no ad-related permissions. On Android the deferred link comes from two sources, tried in this order: 1. **Play Install Referrer (exact).** The landing page adds a click ID to the Play Store link. After install, the SDK reads it from the Install Referrer API and looks up the exact click. 2. **Fingerprint (best effort).** If there is no usable referrer, the SDK asks the server for scored candidates from clicks on your link pages in the last hour. You get both with one call: `DLN.checkDeferredDeepLink()`. The Android SDK does not read the clipboard by default. From 1.4.0, `DLN.onLink` gives you the deferred link and every link that opens the installed app through one handler, so you write your routing once. ## Requirements - Android API level 24+ - Kotlin 1.9+, AndroidX - `android.permission.INTERNET` (declared by the library) ## Installation ```kotlin // build.gradle.kts dependencies { implementation("com.deeplinknow:dln-android:1.4.1") } ``` ```gradle // build.gradle dependencies { implementation 'com.deeplinknow:dln-android:1.4.1' } ``` The library declares `android.permission.INTERNET` itself and ships consumer R8/ProGuard rules for its model classes. The Install Referrer library adds its own permission. ### App Links 1. In the dashboard, set your **package name** and your **SHA-256 signing certificate fingerprints** (debug, upload and Play App Signing). DeepLinkNow serves `https://.deeplinknow.com/.well-known/assetlinks.json` (and the same file for a verified custom domain). Changes are live within 5 minutes. 2. Add an intent filter to the activity that handles links: ```xml ``` 3. Verify with `adb shell pm get-app-links `. ## Getting started ### Initialize the SDK Use your app's **public** API key (`dln_pub_…`) from the dashboard's **API Keys** page. The SDK endpoints (`/api/v1/sdk/*`) reject private keys, and a private key must never ship inside an app. ```kotlin import com.deeplinknow.DLN class MyApplication : Application() { override fun onCreate() { super.onCreate() // Returns immediately. Init and the Install Referrer read run in the background. DLN.init(this, "dln_pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", enableLogs = BuildConfig.DEBUG) { result -> // Optional, called once on the main thread with Result result.onFailure { Log.w("App", "DeepLinkNow init failed: ${it.message}") } } } } ``` `DLN.init` returns immediately and never blocks the main thread. It validates the key, loads your link domains and starts reading the Install Referrer in the background. Observe the outcome with `onComplete`, `DLN.awaitInitialization()` or `DLN.isInitialized`. Optional parameters: `baseUrl` (default `https://deeplinknow.com`), `timeoutMs` (default 10 000), `retryAttempts` (default 2), and `overrideScreenWidth` / `overrideScreenHeight` / `overridePixelRatio` for reproducible matching on emulators. For local testing before you have created an app, you can use the demo key `test_api_key`. It returns mock responses and never touches real data. ### Handle every link with one handler From 1.4.0, `DLN.onLink` hands your app the same `DLNLink` whether the user tapped the link before installing (deferred) or the link opened the installed app (direct). This is the recommended setup for new integrations: ```kotlin // Application.onCreate, after DLN.init DLN.onLink { link -> // link.type DEFERRED (first launch after install) or DIRECT // link.source REFERRER / FINGERPRINT for deferred, APP_LINK / SCHEME for direct router.open(link.path, link.queryParams) } // The activity with your App Link intent filter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // A re-created activity (back from Recents after the process died) gets its old intent again. if (savedInstanceState == null) DLN.handleIntent(intent) } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) DLN.handleIntent(intent) } ``` Registering the handler runs the first-launch check for you, with the same Install Referrer and fingerprint steps and the same once-per-install rule as `checkDeferredDeepLink()`. A link that arrives before a handler exists is passed to it when it registers. The handler runs on the main thread. `handleIntent` returns `true` for a link on your link domains or your custom scheme, and `false` for anything else. The same intent is delivered once, even when both `onCreate` and `onNewIntent` see it, the user reopens the app from Recents, or Android re-creates the activity after killing the process. From 1.4.1 the SDK spots a re-created activity itself (when `DLN.init` runs in `Application.onCreate`), so the `savedInstanceState == null` check is a second guard, not a requirement. A direct open has `confidenceScore` 100 and `matchQuality` `HIGH`. `onLink` returns a `DLNLinkSubscription`; call `remove()` on it if you register from an activity. Options (`DLNLinkOptions`, passed as `DLN.onLink(DLNLinkOptions(...)) { ... }`): | Option | Default | Meaning | |---|---|---| | `minConfidence`, `force` | `null`, `false` | Passed to the first-launch check (see [Get the deferred deep link](#get-the-deferred-deep-link)) | | `hold` | `false` | Keep links in `DLN.pendingLinks` instead of calling the handler | | `schemes` | your manifest's schemes | The custom URL schemes `handleIntent` accepts, for example `setOf("myapp")` | | `resolve` | `false` | Look up each App Link on the server before delivering it | **Hold a link until the user signs in.** A deferred link often arrives while the user is still on your sign-in screen. Register with `hold = true` and the SDK keeps the latest deferred link and the latest direct one in `DLN.pendingLinks` (deferred first; `DLN.pendingLink` is the first of them). When sign-in finishes, `DLN.releasePendingLink()` delivers them to your handler and ends the hold, so later links go straight through. `DLN.clearPendingLink()` drops them and keeps holding, for example when the user signs out. ```kotlin DLN.onLink(DLNLinkOptions(hold = true)) { link -> router.open(link.path, link.queryParams) } // After sign-in DLN.releasePendingLink() ``` **Custom URL schemes.** `myapp://products/123?x=1` arrives with `path` `/products/123` and `source` `SCHEME`. By default the SDK accepts the schemes your manifest declares in a VIEW intent filter. Pass `DLNLinkOptions(schemes = setOf("myapp"))` to list them yourself. **Short links imported from Firebase.** A short link such as `/abc123` means nothing to your app until it's looked up. With `resolve = true`, the SDK calls `GET /api/v1/sdk/resolve` for each App Link open and fills `deeplinkId`, `targetUrl`, `campaignId` and `metadata` before delivering it, so `link.targetUrl` holds the original Firebase deep link. It adds a request to every open, so it's off by default. If the lookup fails, times out or finds nothing, you get the link unresolved. `DLN.resolveLink(url)` (a suspend function) does the same lookup for one URL. See [Import your old short links](/docs/firebase-migration#import-your-old-short-links). From Java: `DLN.onLink(link -> route(link))`, `DLN.onLink(new DLNLinkOptions(null, false, true), handler)` and `DLN.resolveLink(url, link -> { ... })`. ### Get the deferred deep link `checkDeferredDeepLink()` keeps working, and it's what `onLink` runs on first launch. If you'd rather call it yourself, call it once on launch, after `DLN.init`. It never throws: it returns a Kotlin `Result`. ```kotlin lifecycleScope.launch { DLN.checkDeferredDeepLink() .onSuccess { link -> if (link != null) { // link.path "/products/123" // link.queryParams {"ref": QueryValue.Single("newsletter")} // link.source REFERRER or FINGERPRINT // link.confidenceScore 0-100, link.matchQuality HIGH / MEDIUM / LOW router.open(link.path, link.queryParams) } } .onFailure { e -> val error = e as DLNException // error.code: NOT_INITIALIZED, NETWORK, RATE_LIMITED, ... // Errors are not "no link": the SDK tries again on the next call. } } ``` `checkDeferredDeepLink` is a static method on `DLN`. From Java, use the callback overload: ```java DLN.checkDeferredDeepLink(null, false, new DeferredDeepLinkCallback() { @Override public void onResult(DeferredDeepLink link, DLNException error) { /* main thread */ } }); ``` How `checkDeferredDeepLink()` works: 1. **Install Referrer**: the SDK waits for the referrer, extracts the `fp_id` (or `deeplink_id`) the landing page added, and calls `GET /api/v1/sdk/referrer-lookup`. A hit is returned with `source = REFERRER`, `confidenceScore = 100`, `matchQuality = HIGH`. 2. **Fingerprint**: otherwise it calls `POST /api/v1/sdk/match` and returns the best candidate the server sent, with the server's `confidence_score` and `match_quality` unchanged (`source = FINGERPRINT`). It returns a link at most once per install. After a definitive answer (a link, or nothing found) later calls return `null` without network access. Errors such as being offline don't count, so the next launch tries again. The Play Store keeps the referrer for 90 days. Parameters: | Parameter | Default | Meaning | |---|---|---| | `minConfidence` | `null` (off) | Discard fingerprint candidates whose `confidence_score` is below this value. Referrer results are exact and never filtered. | | `force` | `false` | Ignore the "already checked" flag (useful while testing). | `DLN.resetDeferredDeepLink()` clears the first-launch flag and the cached referrer result so you can test the flow again without reinstalling. ### The result: `DeferredDeepLink` | Field | Meaning | |---|---| | `path` | The tapped path up to the first `?`, not decoded, always starting with `/` | | `queryParams` | `Map` with `URLSearchParams` semantics: `Single(value)`, `Flag` (`?k` or `?k=`), `Multiple(values)` (repeated key). `toPlain()` gives `String`, `true` or `List` | | `fullPath` | Path and query exactly as tapped | | `url` | Absolute https URL of the link | | `source` | `DeferredDeepLinkSource.REFERRER` or `FINGERPRINT` (`.value` gives `"referrer"` / `"fingerprint"`) | | `confidenceScore` | `0`–`100`. Server score for fingerprint results, `100` for the referrer | | `matchQuality` | `MatchQuality.HIGH` (75+), `MEDIUM` (50–74) or `LOW` (30–49) | | `deeplinkId`, `targetUrl`, `campaignId`, `metadata`, `matchedAt`, `expiresAt` | From the server | | `matchDetails` | Score breakdown (fingerprint results) | The `DLNLink` that `onLink` delivers has the same fields plus `type` (`DEFERRED` or `DIRECT`), and its `source` can also be `APP_LINK` or `SCHEME`. ## How fingerprint matching works Fingerprint matching is the backup for installs without a usable referrer (for example, an install that didn't come through your link's Play Store button). - The landing page stores a small description of the click: IP address, device class, OS version, screen size, language, time zone, and the path and query that were tapped. - The SDK sends the same kind of description of the device. The server compares it with clicks on **your** app's link pages from the **last 1 hour** and returns up to 10 candidates, best first. - Each candidate has a `confidence_score` (0–100) and a `match_quality`: `HIGH` (75+), `MEDIUM` (50–74) or `LOW` (30–49). Candidates below 30 are not returned. - An IP match is not required. Clicks from the same device class with a matching screen or OS version are considered too. - Score weights: IP 40, device 27 (screen 15, OS version 12), time 18, locale 15 (time zone 10, language 5). Device points count only when the platform and device class agree. Chrome on Android always reports "Android 10", so a compatible OS version earns 6, not 12. Missing data never counts as agreement. The time score decays linearly across the 1-hour claim window. - Each match in the `/sdk/match` response carries `score_version` (`2` since October 2026). If you tuned `minConfidence` on the old weights (IP 40, device 30, time 20, locale 10), check it again: the same device without an IP match now scores 54 rather than 60. - **You choose the threshold.** The `minConfidence` filter is off by default. Use `matchQuality` or `confidenceScore` to decide what to route automatically and what to confirm with the user. ## Handling links that open the app With `onLink`, call `DLN.handleIntent(intent)` in `onCreate` (when `savedInstanceState` is null) and in `onNewIntent` (see [Handle every link with one handler](#handle-every-link-with-one-handler)). Without a handler, pass incoming URLs to `DLN.parseDeepLink(url)` in both `onCreate` and `onNewIntent`. It returns `ParsedDeepLink(path, queryParams, fullPath, url)`, or `null` for URLs that are not on your link domains: ```kotlin override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) routeIntent(intent) } private fun routeIntent(intent: Intent) { val url = intent.data?.toString() ?: return DLN.parseDeepLink(url)?.let { link -> router.open(link.path, link.queryParams) } } ``` Alternatively register a callback with `DLN.setDeepLinkCallback(...)` and call `DLN.handleDeepLink(url)`. That callback is separate from `onLink`: `handleDeepLink` doesn't reach the `onLink` handler. Deferred results and direct opens use the same parsing rules, so you can route both the same way. The host rule accepts exactly `.deeplinknow.com` and your verified custom domain, case-insensitively. Other `deeplinknow.com` subdomains and lookalike hosts are rejected. ## Low-level API | Method | Purpose | |---|---| | `DLN.findDeferredUserResult()` | `Result`: every fingerprint candidate from `/api/v1/sdk/match`, unfiltered, in server order, each with `confidenceScore`, `matchQuality`, `matchDetails` and `deeplink` (`queryParams`, `fullPath`) | | `getInstance().findDeferredUser()` | 1.0 API: `MatchResponse?` (null on any error) | | `getInstance().getDeferredDeepLink()` | 1.0 API: the cached referrer result, returned on every launch. Prefer `DLN.checkDeferredDeepLink()` | | `getInstance().getReferrerData()` | The raw Install Referrer data (`referrerString`, `fpId`, `deeplinkId`, `processedAt`) | | `getInstance().checkClipboard()` | The clipboard text if, trimmed, it is exactly one URL on your link domains, else `null`. On Android 12+ reading shows a system "pasted" toast | | `getInstance().hasDeepLinkToken()` | Checks the clip description only (no toast) | | `DLN.parseDeepLink(url)` | `ParsedDeepLink(path, queryParams, fullPath, url)` or `null` | | `DLN.isValidDomain(host)` | The host rule | | `getInstance().parseDeepLink(url)` | Deprecated 1.0 API returning a `Pair` (decoded path, first value of repeated keys); use `DLN.parseDeepLink(url)` | ## Privacy The SDK sends only what matching needs: a synthesized user agent, platform, OS version, device model, screen size, language, time zone, and `ANDROID_ID` (app-scoped), which the server uses only to claim a click for this install. `advertising_id` is always `null`. The server reads the IP from the connection. On the device the SDK stores only the first-launch flag and the referrer result, excluded from backups. ## API endpoints used | Endpoint | Key | Used by | |---|---|---| | `POST /api/v1/sdk/init` | public | `DLN.init` | | `GET /api/v1/sdk/referrer-lookup` | public | `checkDeferredDeepLink` (referrer step) | | `POST /api/v1/sdk/match` | public | `checkDeferredDeepLink` (fingerprint step), `findDeferredUser` | | `GET /api/v1/sdk/resolve` | public | `onLink` with `resolve = true`, `resolveLink` | `init`, `referrer-lookup`, `match` and `resolve` are exempt from per-minute rate limits, and `init` counts toward monthly active users. ## Support - Email: support@deeplinknow.com - Source and issues: [github.com/deeplinknow/dln-android](https://github.com/deeplinknow/dln-android) --- Source: https://deeplinknow.com/docs/react-native # DeepLinkNow React Native SDK ## Overview `@deeplinknow/react-native` gives React Native and Expo apps the same two calls as the native SDKs: initialize, and get the deferred deep link. It wraps the native iOS and Android SDKs, so it behaves the same way and returns the same shapes on both platforms. When a user taps one of your links before installing your app, `checkDeferredDeepLink()` returns the path and query they tapped: - **iOS**: 1. the link the landing page copied to the pasteboard (exact), then 2. fingerprint matching (best effort). - **Android**: 1. the Play Install Referrer (exact), then 2. fingerprint matching (best effort). From 1.4.0, `onLink` (or the `useDeepLink()` hook) gives you the deferred link and every link that opens the installed app through one handler, so you write your routing once. It is not an analytics or attribution SDK. It never reads advertising identifiers and never triggers the App Tracking Transparency prompt. ## Requirements - React Native 0.81+ (old or new architecture), or Expo SDK 54+ with a development build - iOS 13+, Android API 24+ - Expo Go has no native code, so only the fingerprint step works there (the SDK logs a warning). Use a development build. ## Installation ### Expo ```sh npx expo install @deeplinknow/react-native ``` Add the config plugin with your link domains. It adds `applinks:` to the iOS Associated Domains and an auto-verified App Links intent filter on Android for each domain: ```json { "expo": { "plugins": [ ["@deeplinknow/react-native", { "domains": ["myapp.deeplinknow.com", "go.myapp.com"] }] ] } } ``` Then rebuild the native projects: `npx expo prebuild` (or let EAS Build do it), then `npx expo run:ios` / `npx expo run:android`. ### Bare React Native ```sh npm install @deeplinknow/react-native cd ios && pod install ``` If `pod install` (or `npx expo prebuild`) says no spec satisfies `DeepLinkNow (~> 1.4.1)`, your local CocoaPods spec cache is older than the release. Run `cd ios && pod install --repo-update` once. Autolinking picks the right native module (TurboModule on the new architecture, bridge module on the old one). Add the Associated Domains entitlement (`applinks:myapp.deeplinknow.com`) on iOS and an `autoVerify` https intent filter for your link domains on Android. See the [iOS](/docs/ios) and [Android](/docs/android) docs for details and the dashboard settings that serve the `.well-known` files. ## Getting started ### Initialize the SDK Use your app's **public** API key (`dln_pub_…`) from the dashboard's **API Keys** page. The SDK endpoints (`/api/v1/sdk/*`) reject private keys, and a private key must never ship inside an app. ```typescript import DeepLinkNow from "@deeplinknow/react-native"; const app = await DeepLinkNow.initialize("dln_pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", { enableLogs: __DEV__, }); // app: DLNAppInfo { id, alias, domains, accountStatus, creditsRemaining }, or null if it failed ``` Config options: `enableLogs` (default `false`), `baseUrl` (default `https://deeplinknow.com`), `timeoutMs` (default 10 000), `retryAttempts` (default 2). `initialize` makes one `/init` request per call and resolves with `DLNAppInfo`. It never rejects: if it fails it resolves `null`, logs a warning and keeps the error in `DeepLinkNow.lastError`, so no `try`/`catch` is needed. `DeepLinkNow.isInitialized()` tells you whether it succeeded. For local testing before you have created an app, you can use the demo key `test_api_key`. It returns mock responses and never touches real data. ### Handle every link with one handler From 1.4.0, `onLink` hands your app the same `DLNLink` whether the user tapped the link before installing (deferred) or the link opened the installed app (a Universal Link, an App Link or your URL scheme). The SDK reads `Linking.getInitialURL()` and `url` events itself, so you don't write any `Linking` code. This is the recommended setup for new integrations without Expo Router: ```typescript import DeepLinkNow from "@deeplinknow/react-native"; const unsubscribe = DeepLinkNow.onLink( (link) => { // link.type "deferred" (first launch after install) or "direct" // link.source "referrer" | "clipboard" | "fingerprint" | "universalLink" | "appLink" | "scheme" navigate(link.path, link.queryParams); }, { apiKey: "dln_pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } ); ``` Registering the handler starts the first-launch check, the same single check as `checkDeferredDeepLink()`, so calling both doesn't run it twice. With `apiKey` set, `onLink` initializes the SDK first. A link that arrives before you register is handed to the first handler once, and the same URL within 3 seconds counts as one open. A direct open has `confidenceScore` 100 and `matchQuality` `"HIGH"`. In a React component, `useDeepLink()` gives you the same thing as state: `{ link, pendingLink, pendingLinks, isLoading, release, clear }`. It takes the same options as `onLink`, plus `enabled`. Options: | Option | Default | Meaning | |---|---|---| | `apiKey`, `config` | unset | Initialize first when the SDK isn't initialized yet | | `minConfidence`, `useClipboard` | off, `true` | Passed to the first-launch check (see [Get the deferred deep link](#get-the-deferred-deep-link)) | | `hold` | `false` | Keep links in `DeepLinkNow.pendingLinks` instead of calling the handler | | `schemes` | the Expo config's `scheme` | Your custom URL schemes, for example `["myapp"]` | | `resolve` | `false` | Look up each Universal Link or App Link on the server before delivering it | **Hold a link until the user signs in.** A deferred link often arrives while the user is still on your sign-in screen. With `hold: true` the SDK keeps the latest deferred link and the latest direct one in `DeepLinkNow.pendingLinks` (deferred first; `DeepLinkNow.pendingLink` is the first of them). When sign-in finishes, `DeepLinkNow.releasePendingLink()` delivers them and ends the hold, so later links go straight to your handler. `DeepLinkNow.clearPendingLink()` drops them and keeps holding. With the hook, pass `hold: !isSignedIn` and the link is delivered once when the user signs in (see [Open the link after sign-in](#open-the-link-after-sign-in)). **Custom URL schemes.** `myapp://products/1?x=1` arrives with `path` `/products/1` and `source` `"scheme"`. Expo apps get the `scheme` from the Expo config by default. Bare React Native apps must pass the list themselves, for example `schemes: ["myapp"]`, or scheme links aren't delivered. **Short links imported from Firebase.** A short link such as `/abc123` means nothing to your app until it's looked up. With `resolve: true`, the SDK calls `GET /api/v1/sdk/resolve` for each Universal Link or App Link open and fills `deeplinkId`, `targetUrl`, `campaignId` and `metadata` before delivering it, so `link.targetUrl` holds the original Firebase deep link. It adds a request to every open, bounded by `timeoutMs`, so it's off by default. If the lookup fails, times out or finds nothing, you get the link unresolved. `await DeepLinkNow.resolveLink(url)` does the same lookup for one URL. See [Import your old short links](/docs/firebase-migration#import-your-old-short-links). ### Get the deferred deep link `checkDeferredDeepLink()` keeps working, and it's what `onLink` runs on first launch. Call it yourself if you'd rather not use a handler: ```typescript const link = await DeepLinkNow.checkDeferredDeepLink(); if (link) { // link.path "/products/123" // link.queryParams { ref: "newsletter" } // link.source "clipboard" | "referrer" | "fingerprint" // link.confidenceScore 0-100 // link.matchQuality "HIGH" | "MEDIUM" | "LOW" router.push({ pathname: link.path, params: link.queryParams }); } ``` `checkDeferredDeepLink` never throws or rejects. "No link" and a failure (offline, invalid key, not initialized) both resolve `null`, so the app just carries on to its normal start screen. To tell them apart, check `DeepLinkNow.lastError`: a `DLNError` with `code` (`notInitialized`, `network`, `rateLimited`, …), plus `status` and `serverMessage` when the server sent one. It is cleared by the next successful call. It returns a link at most once per install. After a definitive answer (a link, or nothing found) later calls resolve to `null` without network, clipboard or referrer access. Errors don't count, so an offline first launch can still succeed on the next launch. Options: | Option | Default | Meaning | |---|---|---| | `minConfidence` | unset (off) | Discard fingerprint candidates whose `confidence_score` is below this value. Clipboard and referrer results are exact and never filtered. | | `useClipboard` | `true` | iOS only: read the pasteboard first (may show the iOS 16+ paste prompt). Ignored on Android. | | `force` | `false` | Ignore the "already checked" flag (useful while testing). | ```typescript const link = await DeepLinkNow.checkDeferredDeepLink({ minConfidence: 50 }); ``` `await DeepLinkNow.resetDeferredDeepLink()` clears the first-launch state and the cached referrer result so you can test again without reinstalling. `clearReferrerCache()` is a deprecated alias. Using Expo Router? See [Expo Router](#expo-router). ### The result: `DeferredDeepLink` | Field | Type | Meaning | |---|---|---| | `path` | `string` | The tapped path up to the first `?`, not decoded, always starting with `/` | | `queryParams` | `Record` | Query parameters with `URLSearchParams` semantics; `true` for a bare flag, an array for repeated keys | | `fullPath` | `string` | Path and query exactly as tapped | | `url` | `string` | Absolute https URL of the link | | `source` | `"clipboard" \| "referrer" \| "fingerprint"` | Which signal produced the answer | | `confidenceScore` | `number` | `0`–`100`. Server score for fingerprint results, `100` for clipboard and referrer | | `matchQuality` | `"HIGH" \| "MEDIUM" \| "LOW"` | `HIGH` 75+, `MEDIUM` 50–74, `LOW` 30–49 | | `deeplinkId`, `targetUrl`, `campaignId`, `metadata`, `matchedAt`, `expiresAt` | optional | From the server | | `matchDetails` | optional | Score breakdown (fingerprint results) | The `DLNLink` that `onLink` delivers has the same fields plus `type` (`"deferred"` or `"direct"`), and its `source` can also be `"universalLink"`, `"appLink"` or `"scheme"`. ## Expo Router If your app uses Expo Router, get the deferred deep link in `app/+native-intent.tsx`. Expo Router calls this file with the URL that launched the app, before the first screen mounts. Resolving the link there means the app opens straight on the screen the user tapped: the home screen doesn't flash first, and your layouts' redirects (an auth gate, onboarding) see the right route from the start. The minimal setup is two files. Create `app/+native-intent.tsx`: ```tsx import { createRedirectSystemPath } from "@deeplinknow/react-native/expo-router"; export const redirectSystemPath = createRedirectSystemPath({ apiKey: "dln_pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", }); ``` And handle late links in your root layout, `app/_layout.tsx`: ```tsx import { useDeferredDeepLink } from "@deeplinknow/react-native"; import { Stack, useRouter } from "expo-router"; import { useEffect } from "react"; export default function RootLayout() { const { link, clear } = useDeferredDeepLink(); const router = useRouter(); useEffect(() => { if (link) { clear(); // so it opens once router.push(link.fullPath); } }, [link]); return ; } ``` **Don't skip the second file.** The helper waits at most `timeoutMs` (3 seconds by default) before the first screen mounts. If the link is found after that, for example on a slow connection, it reaches your app only through `useDeferredDeepLink()`. Without the hook, that link is dropped. The hook shares the helper's check, so the network calls run once, and a link the helper already opened comes back as `null`. You don't need to call `DeepLinkNow.initialize` anywhere else: the helper does it. What happens on each launch: | The app was opened… | The helper returns | |---|---| | for the first time after install, from the store or the home screen | The link the user tapped before installing, e.g. `/products/123?ref=newsletter`. If there is none, or the check takes longer than `timeoutMs`, the app starts on its normal route | | by a link on your DeepLinkNow domains (`yourapp.deeplinknow.com` or a verified custom domain) | That link's path and query, e.g. `/products/123?ref=newsletter` | | any other way (a later launch, another URL, your own `myapp://` links) | The URL unchanged, so Expo Router handles it as usual | The helper never throws. If something fails (offline, a wrong API key), the app starts where it would have without DeepLinkNow, and `DeepLinkNow.lastError` says why. ### Options | Option | Default | Description | |---|---|---| | `apiKey` | required | Your public API key (`dln_pub_…`) | | `config` | `{}` | Passed to `initialize`: `enableLogs`, `baseUrl`, `timeoutMs`, `retryAttempts` | | `minConfidence` | off | Ignore fingerprint matches scoring below this (0–100). See [How fingerprint matching works](#how-fingerprint-matching-works) | | `useClipboard` | `true` | iOS: check the link the landing page copied first | | `timeoutMs` | `3000` | The longest the first launch waits for the link, in milliseconds. `0` never waits | | `mapUniversalLinks` | `true` | Rewrite links on your DeepLinkNow domains to their in-app path | | `routeDeferredLink` | `true` | Open the app on the deferred link at launch. Set `false` to start on the normal route and deliver the link through the hook instead (1.4.0, see [Open the link after sign-in](#open-the-link-after-sign-in)) | | `mapLink` | the link's `fullPath` | `(link, source) => string \| null`. Use it when your screen paths differ from your link paths. `source` is `"deferred"` or `"universal"`. Return `null` to keep the incoming URL | For example, if your links look like `/p/123` but the screen is `app/products/[id].tsx`: ```tsx export const redirectSystemPath = createRedirectSystemPath({ apiKey: "dln_pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", minConfidence: 50, mapLink: (link) => link.path.startsWith("/p/") ? `/products/${link.path.slice(3)}` : link.fullPath, }); ``` If you already have a `redirectSystemPath` with your own rewrites, call `redirectSystemPathWithDeferredLink(event, options)` from it and apply your rewrites to the result. ### Open the link after sign-in If a deep-linked screen needs a signed-in user, hold the link until sign-in finishes, then open it once. From 1.4.0, set `routeDeferredLink: false` in `+native-intent.tsx`. The helper still starts the check at launch, so the link is found inside its 1-hour claim window, but the app starts on its normal route: ```tsx // app/+native-intent.tsx import { createRedirectSystemPath } from "@deeplinknow/react-native/expo-router"; export const redirectSystemPath = createRedirectSystemPath({ apiKey: "dln_pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", routeDeferredLink: false, }); ``` Then hold the link in your root layout with `useDeepLink()`: ```tsx // app/_layout.tsx import { useDeepLink } from "@deeplinknow/react-native"; import { Stack, useRouter } from "expo-router"; import { useEffect } from "react"; export default function RootLayout() { const { isSignedIn } = useAuth(); // your auth const router = useRouter(); const { link, pendingLink } = useDeepLink({ hold: !isSignedIn }); useEffect(() => { if (link) router.push(link.fullPath); // a new object for every link, delivered once }, [link]); // On the sign-in screen, `pendingLink` can say "Sign in to open your invite". return ; } ``` While `hold` is true, a new link waits in `pendingLink`. When the user signs in it moves to `link`, once. `release()` delivers it early and ends the hold, and `clear()` drops it while still holding (on sign-out, or when the user declines the invite). Expo Router opens Universal Links, App Links and your scheme links itself through `+native-intent`, so `useDeepLink()` doesn't deliver those a second time; protect those routes with your layout's auth redirect as usual. The held link lives in memory only, so if the app is killed before sign-in it's gone. `useDeferredDeepLink()` also takes `hold` and an `onLink` callback from 1.4.0, and the 1.3 pattern (`timeoutMs: 0` in the helper, then `if (link && isSignedIn) { clear(); router.push(link.fullPath); }` in an effect) still works. ### Without Expo Router `useDeferredDeepLink()` works in any React Native app. Pass your API key, and it initializes the SDK and checks once: ```tsx import { useDeferredDeepLink } from "@deeplinknow/react-native"; import { useEffect } from "react"; function App() { const { link, isLoading, clear } = useDeferredDeepLink({ apiKey: "dln_pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }); useEffect(() => { if (link) { clear(); navigation.navigate("Product", { path: link.path, params: link.queryParams }); } }, [link]); // … } ``` It returns `{ link, isLoading, clear }`. `link` is `null` until a link is found, after `clear()`, and on every launch after the first (in Expo Go, which has no native first-launch flag, the check runs again on each launch while the click can still be claimed). ## How fingerprint matching works - The landing page stores a small description of the click: IP address, device class, OS version, screen size, language, time zone, and the path and query that were tapped. - The SDK sends the same kind of description of the device. The server compares it with clicks on your app's link pages from the **last 1 hour** and returns up to 10 candidates, best first, each with a `confidence_score` (0–100) and a `match_quality` (`HIGH`, `MEDIUM`, `LOW`). Candidates below 30 are not returned. - An IP match is not required (iCloud Private Relay and carrier NAT often change the IP between browser and app). - Missing data never counts as agreement, and the time score decays linearly across the 1-hour claim window. - **You choose the threshold.** `minConfidence` is off by default. Use `matchQuality` or `confidenceScore` to decide what to route automatically and what to confirm with the user. ## Handling links that open the app With `onLink` or `useDeepLink()` you don't need this: the SDK reads the URLs itself (see [Handle every link with one handler](#handle-every-link-with-one-handler)). Without them, read the URL with `Linking` and parse it: ```typescript import { Linking } from "react-native"; function handleUrl(url: string | null) { if (!url) return; const link = DeepLinkNow.parseDeepLink(url); if (link) router.push({ pathname: link.path, params: link.queryParams }); } Linking.getInitialURL().then(handleUrl); Linking.addEventListener("url", ({ url }) => handleUrl(url)); ``` `parseDeepLink(url)` returns `{ path, queryParams, fullPath, url }`, or `null` for URLs that are not on your link domains (exactly `.deeplinknow.com` or your verified custom domain). The older `parameters` field is kept as a deprecated alias of `queryParams`. ## Low-level API | Method | Purpose | |---|---| | `findDeferredUser()` | Every fingerprint candidate from `/api/v1/sdk/match` in the server's snake_case shape: `matches[]` with `confidence_score`, `match_quality`, `match_details`, `deeplink` (`target_url`, `full_path`, `query_params`, …), plus `ttl_seconds`. Resolves `null` on failure (never rejects), with the error in `DeepLinkNow.lastError` | | `getReferrerData()` | Android only: raw Install Referrer data (`null` on iOS) | | `checkClipboard()` | The clipboard text if it is exactly one URL on your link domains, else `null` | | `hasDeepLinkToken()` | Cheap "is there probably a URL on the clipboard" check that shows no paste prompt | | `parseDeepLink(url)` | Validate and parse a URL on your link domains | | `isValidDomain(host)` | The host rule | | `getModuleType()` | `"expo" \| "turbo" \| "bridge" \| "none"`: which native module is in use | ## Privacy The native SDKs send only what matching needs (platform, OS version, device class, screen, language, time zone, and an app-scoped install ID used only to claim a click). `advertising_id` is always `null`. The server reads the IP from the connection. ## API endpoints used | Endpoint | Key | Used by | |---|---|---| | `POST /api/v1/sdk/init` | public | `initialize` (exactly one call per `initialize`) | | `GET /api/v1/sdk/referrer-lookup` | public | `checkDeferredDeepLink` on Android | | `POST /api/v1/sdk/match` | public | `checkDeferredDeepLink` (fingerprint step), `findDeferredUser` | | `GET /api/v1/sdk/resolve` | public | `onLink` with `resolve: true`, `resolveLink` | ## Support - Email: support@deeplinknow.com - Package: [npmjs.com/package/@deeplinknow/react-native](https://www.npmjs.com/package/@deeplinknow/react-native) --- Source: https://deeplinknow.com/docs/flutter # DeepLinkNow Flutter SDK ## Overview [`deeplinknow`](https://pub.dev/packages/deeplinknow) is the Flutter plugin for DeepLinkNow. It gives Flutter and FlutterFlow apps the same two calls as the native SDKs: initialize, and get the deferred deep link. The plugin is a thin wrapper over the native [iOS](/docs/ios) and [Android](/docs/android) SDKs (1.4). All deferred-link logic runs natively: the pasteboard and Install Referrer steps, fingerprint matching, first-launch handling and link parsing. So a Flutter app gets the same answers as a Swift, Kotlin or React Native app. When a user taps one of your links before installing your app, `checkDeferredDeepLink()` returns the path and query they tapped: - **iOS**: 1. the link the landing page copied to the pasteboard (exact), then 2. fingerprint matching (best effort). - **Android**: 1. the Play Install Referrer (exact), then 2. fingerprint matching (best effort). From 1.4.0, the `DeepLinkNow.links` stream gives you the deferred link and every link that opens the installed app in one place, so you write your routing once. It is not an analytics or attribution SDK. It never reads advertising identifiers and never triggers the App Tracking Transparency prompt. ## Requirements - Flutter 3.24+ (Dart 3.5+) - iOS 13+, with Swift Package Manager or CocoaPods - Android API 24+ (`minSdk 24`) - Your app's **public** API key (`dln_pub_…`) from the dashboard's **API Keys** page ## Installation ```sh flutter pub add deeplinknow ``` Or add `deeplinknow: ^1.4.1` to `pubspec.yaml` yourself. This also installs `deeplinknow_ios`, the plugin's iOS implementation. You don't add it yourself. - **iOS**: nothing else to install. With Swift Package Manager (the default in recent Flutter) Xcode fetches `dln-ios`; with CocoaPods, `pod install` (which `flutter run` and `flutter build` run for you) fetches the `DeepLinkNow` pod. With CocoaPods, set at least `platform :ios, '13.0'` in `ios/Podfile`. No `Info.plist` keys are needed. - **Android**: the native SDK (`com.deeplinknow:dln-android:1.4.1`) comes from Maven Central, which new Flutter projects already list in `android/settings.gradle(.kts)`. Make sure `minSdk` is at least 24 in `android/app/build.gradle(.kts)`. Stop the app and rebuild it after adding the package. Hot restart doesn't load native code. ## Set up your link domains DeepLinkNow serves the `.well-known` files for your domains from your dashboard settings, so links open your app when it's installed. 1. In the dashboard, set your **Team ID** and **Bundle ID** (iOS) and your **package name** and **SHA-256 certificate fingerprints** (Android). 2. **iOS**: open `ios/Runner.xcworkspace` in Xcode, go to **Runner → Signing & Capabilities → + Capability → Associated Domains**, and add `applinks:.deeplinknow.com` (and `applinks:` if you have one). 3. **Android**: add an App Links intent filter to the `MainActivity` in `android/app/src/main/AndroidManifest.xml`: ```xml ``` Replace `` with your app's alias, and add one `` line per custom domain. ## Getting started ### Initialize and get the deferred deep link ```dart import 'package:deeplinknow/deeplinknow.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); // Never throws: null means it failed (see DeepLinkNow.lastError). await DeepLinkNow.initialize( 'dln_pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', config: const DLNConfig(enableLogs: kDebugMode), ); // Delivered once per install; null on later launches. final link = await DeepLinkNow.checkDeferredDeepLink(); runApp(MyApp(initialRoute: link?.path ?? '/')); } ``` `DLNConfig` options: `enableLogs` (default `false`), `baseUrl` (default `https://deeplinknow.com`), `timeoutMs` and `retryAttempts`. Leave them unset to use the native defaults. `initialize` makes one native init per call and resolves `DLNAppInfo` (`id`, `alias`, `domains`, `primaryDomain`, …), or `null` if it failed. `DeepLinkNow.isInitialized` tells you whether the last `initialize` succeeded. `link.path` is the path the user tapped (`/products/123`) and `link.queryParams` holds the query (`{ref: newsletter}`). Route on them the way you route any other link. With go_router: ```dart final link = await DeepLinkNow.checkDeferredDeepLink(); if (link != null) router.go(link.fullPath); // path + query, as tapped ``` For local testing before you have created an app, you can use the demo key `test_api_key`. It returns mock responses and never touches real data. ### No link, or an error? `checkDeferredDeepLink` never throws. "No link" and a failure (offline, invalid key, not initialized) both resolve `null`, so the app carries on to its normal start screen. To tell them apart, check `DeepLinkNow.lastError`: a `DLNError` with a `code` (`notInitialized`, `invalidApiKey`, `accountInactive`, `noCredits`, `rateLimited`, `badRequest`, `server`, `network` or `invalidResponse`, the same codes as every DeepLinkNow SDK), plus `status`, `serverMessage`, `message` and `cause` when present. It is cleared by the next successful call. ```dart final link = await DeepLinkNow.checkDeferredDeepLink(); if (link == null && DeepLinkNow.lastError != null) { // Offline or misconfigured. Errors don't use up the first launch, // so the next launch tries again. } ``` It returns a link at most once per install. After a definitive answer (a link, or nothing found) later calls resolve `null` without network, clipboard or referrer access. Options: | Option | Default | Meaning | |---|---|---| | `minConfidence` | unset (off) | Discard fingerprint candidates whose score is below this value. Clipboard and referrer results are exact and never filtered. | | `useClipboard` | `true` | iOS only: read the pasteboard first (may show the iOS 16+ paste prompt). Ignored on Android. | | `force` | `false` | Ignore the "already checked" flag (useful while testing). | ```dart final link = await DeepLinkNow.checkDeferredDeepLink(minConfidence: 50); ``` `await DeepLinkNow.resetDeferredDeepLink()` clears the first-launch state so you can test again without reinstalling. It is a test call, and it throws a `DLNError` if the native reset fails. ### The result: `DeferredDeepLink` | Field | Type | Meaning | |---|---|---| | `path` | `String` | The tapped path up to the first `?`, always starting with `/` | | `queryParams` | `Map` | Query parameters (see below) | | `fullPath` | `String` | Path and query exactly as tapped | | `url` | `String` | Absolute https URL of the link | | `source` | `DeferredDeepLinkSource` | `clipboard`, `referrer` or `fingerprint`: which signal produced the answer | | `confidenceScore` | `double` | `0`–`100`. Server score for fingerprint results, `100` for clipboard and referrer | | `matchQuality` | `MatchQuality` | `HIGH` 75+, `MEDIUM` 50–74, `LOW` 30–49 | | `deeplinkId`, `targetUrl`, `campaignId`, `metadata`, `matchedAt`, `expiresAt` | optional | From the server | | `matchDetails` | `MatchDetails?` | Score breakdown (fingerprint results) | A `QueryValue` is one of: - `SingleQueryValue` for `?ref=newsletter` - `FlagQueryValue` for `?debug` or `?debug=` - `MultipleQueryValue` for a repeated key, `?tag=a&tag=b` Use `.stringValue` for the common case (`link.queryParams['ref']?.stringValue`), `.stringValues` for lists, or switch on the type. ### Every open in one stream From 1.4.0, `DeepLinkNow.links` hands you the same `DLNLink` whether the user tapped the link before installing (deferred) or the link opened the installed app (a Universal Link, an App Link or your app's URL scheme). The plugin reads the launch URL and later URLs itself, so you don't need `app_links`. This is the recommended setup for new integrations: ```dart DeepLinkNow.links.listen((link) => router.go(link.fullPath)); DeepLinkNow.listen(); // starts the stream; before or after initialize is fine await DeepLinkNow.initialize('dln_pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'); ``` `link.type` is `DLNLinkType.deferred` or `.direct`, and `link.source` says how it arrived (`clipboard`, `referrer`, `fingerprint`, `universalLink`, `appLink` or `scheme`). `listen()` runs the first-launch check for you, with the same steps and the same once-per-install rule as `checkDeferredDeepLink()`. Links arrive after `initialize` finishes, deferred first. If a link arrives before you subscribe, the latest one of each kind is replayed to your first listener, and the same URL twice within 3 seconds counts as one open. Turn off Flutter's own deep linking so the engine doesn't also push the path as a route: set `FlutterDeepLinkingEnabled` to `NO` in `ios/Runner/Info.plist`, and add `` inside the `` in `AndroidManifest.xml`. `listen` takes `DLNLinkOptions(minConfidence, useClipboard, force, hold, resolve, schemes)`. The first three go to the first-launch check. **Hold a link until the user signs in.** A deferred link often arrives while the user is still on your sign-in screen. With `hold: true` the links wait in `DeepLinkNow.pendingLinks` (deferred first; `DeepLinkNow.pendingLink` is the first of them) instead of reaching the stream: ```dart DeepLinkNow.listen(const DLNLinkOptions(hold: true)); // After sign-in: deliver them and stop holding DeepLinkNow.releasePendingLink(); // Or drop them and keep holding (on sign-out, say) DeepLinkNow.clearPendingLink(); ``` **Custom URL schemes.** `myapp://products/123?x=1` arrives with `path` `/products/123` and `source` `scheme`. By default the schemes in your Info.plist (`CFBundleURLSchemes`) or your Android manifest count. Pass `DLNLinkOptions(schemes: ['myapp'])` to list them yourself. **Short links imported from Firebase.** A short link such as `/abc123` means nothing to your app until it's looked up. With `resolve: true`, the plugin calls `GET /api/v1/sdk/resolve` for each https open on your domains and fills `deeplinkId`, `targetUrl`, `campaignId` and `metadata`, so `link.targetUrl` holds the original Firebase deep link. It adds a request to every open, bounded by your timeout, so it's off by default. If the lookup fails or finds nothing, the link still arrives, unresolved. `DeepLinkNow.resolveLink(url)` does the same lookup for one URL. See [Import your old short links](/docs/firebase-migration#import-your-old-short-links). `checkDeferredDeepLink()` keeps working as shown above, so you can move to the stream when it suits you. ## How fingerprint matching works - The landing page stores a small description of the click: IP address, device class, OS version, screen size, language, time zone, and the path and query that were tapped. - The SDK sends the same kind of description of the device. The server compares it with clicks on your app's link pages from the **last 1 hour** and returns up to 10 candidates, best first, each with a `confidence_score` (0–100) and a `match_quality` (`HIGH`, `MEDIUM`, `LOW`). Candidates below 30 are not returned. - An IP match is not required (iCloud Private Relay and carrier NAT often change the IP between browser and app). - Missing data never counts as agreement, and the time score decays linearly across the 1-hour claim window. - **You choose the threshold.** `minConfidence` is off by default. Use `matchQuality` or `confidenceScore` to decide what to route automatically and what to confirm with the user. ## Handling links that open the app When the app is already installed, a tap on your link opens it directly (Universal Link / App Link). From 1.4.0 the [`links` stream](#every-open-in-one-stream) delivers these opens for you. Without it, receive the URL with a package such as [`app_links`](https://pub.dev/packages/app_links), then parse it with the same rules as the deferred result: ```dart final parsed = await DeepLinkNow.parseDeepLink(uri.toString()); if (parsed != null) router.go(parsed.fullPath); ``` `parseDeepLink(url)` returns `{path, queryParams, fullPath, url}`, or `null` for URLs that are not on your link domains (exactly `.deeplinknow.com` or your verified custom domain). It returns a `Future` because Flutter platform channels are asynchronous; the parse itself runs in the native SDK. ### iOS apps on the UIScene lifecycle (Flutter 3.41+ by default) If links that open the installed app never reach your Dart code on iOS, check these: 1. **Use `app_links` 7.0.0 or later.** Flutter 3.38 added UIScene support, Flutter 3.41 turns it on by default (and migrates an unmodified app delegate automatically), and apps built with Xcode 27 must use it. Once your app is on UIScene, iOS delivers links to the scene delegate, not the app delegate. `app_links` 6.x only hooks the app-delegate callbacks, so links never reach Dart and nothing reports an error. 7.0.0 added UISceneDelegate support. The same applies to any plugin that only calls `addApplicationDelegate`. 2. **Set `FlutterDeepLinkingEnabled` to `false`** in `ios/Runner/Info.plist` when a plugin handles your links. Otherwise the engine hands links it doesn't claim back to iOS, and the user bounces from the app to Safari and back. 3. **Subscribe to `uriLinkStream` synchronously at the top of `initState`**, before any `await`. The stream doesn't replay links that arrived before you subscribed. Queue the link until your startup is ready. 4. **Put a timeout on every external `await` in startup** (for example `FirebaseMessaging.getInitialMessage()`), and drain the link queue in a `finally` block, so one slow call can't hold the link forever. ## API All members are static on `DeepLinkNow`. | Member | Returns | Notes | |---|---|---| | `initialize(apiKey, {config})` | `Future` | One native init. `null` on failure, never throws | | `isInitialized` | `bool` | Whether the last `initialize` succeeded | | `appInfo` | `DLNAppInfo?` | App id, alias, link domains, primary domain | | `checkDeferredDeepLink({minConfidence, useClipboard, force})` | `Future` | `null` = no link or failure; never throws | | `resetDeferredDeepLink()` | `Future` | Clears the first-launch state, for testing | | `findDeferredUser()` | `Future` | Every fingerprint candidate from `/api/v1/sdk/match`, in server order, as typed classes. `null` on failure | | `getReferrerData()` | `Future` | Android only: the raw Install Referrer result. `null` on iOS | | `checkClipboard()` | `Future` | The clipboard text if it is one of your links. May show the iOS paste prompt | | `hasDeepLinkToken()` | `Future` | Cheap check that shows no paste prompt | | `parseDeepLink(url)` | `Future` | Validate and parse a URL on your link domains | | `isValidDomain(host)` | `Future` | Whether the host is one of your link domains | | `lastError` | `DLNError?` | Last failure of `initialize`, `checkDeferredDeepLink` or `findDeferredUser` | | `sdkVersion` | `String` | `"1.4.1"` | | `links` | `Stream` | 1.4.0: every open, deferred or direct (a broadcast stream) | | `listen([options])` | `void` | 1.4.0: starts `links`, with `DLNLinkOptions` | | `pendingLink` / `pendingLinks` | `DLNLink?` / `List` | 1.4.0: links kept by `hold: true`, deferred first | | `releasePendingLink()` / `clearPendingLink()` | `void` | 1.4.0: deliver them and end the hold / drop them | | `resolveLink(url)` | `Future` | 1.4.0: looks the link up on the server; unresolved on a miss or failure, `null` if it isn't your link | Not in the Flutter plugin: `createDeepLink`. Web, macOS, Windows and Linux aren't supported: there, every call resolves `null` or `false` and `lastError` is `notInitialized`. ## FlutterFlow Add `deeplinknow: ^1.4.1` under a custom action's **Pubspec Dependencies**. The [FlutterFlow guide](/docs/flutterflow) has the custom action that opens the right FlutterFlow page from the link, plus the entitlement and intent filter steps in FlutterFlow's Configuration Files. ## Testing your integration - A link is delivered once per install. To try again without reinstalling, call `DeepLinkNow.resetDeferredDeepLink()` or pass `force: true`. - iOS: open your link in Safari on the device, tap the App Store button (this copies the link), then run the app from Xcode. - Android: test the Install Referrer with an internal test track install from a link. A sideloaded build has no referrer and falls back to fingerprint matching. - `DLNConfig(enableLogs: true)` turns on the native debug logs (`[DeepLinkNow]` in the Xcode console, tag `DLN` in Logcat). The API key is never logged. - Universal Links not opening the app: check Apple's CDN copy of your association file at `https://app-site-association.cdn-apple.com/a/v1/`, or run the free [AASA validator](/tools/aasa-validator). - `lastError` says "native plugin is not available": stop the app and rebuild it after adding the package. ## Privacy The plugin adds nothing to what the native SDKs collect: the device fields used for matching (platform, OS version, device model, screen size, language, time zone, an app-scoped install ID), sent only to DeepLinkNow, and no advertising identifiers. It adds no Android permissions. See the [iOS](/docs/ios) and [Android](/docs/android) docs for the App Store privacy labels and the Play Data safety form. ## API endpoints used | Endpoint | Key | Used by | |---|---|---| | `POST /api/v1/sdk/init` | public | `initialize` (one call per `initialize`) | | `GET /api/v1/sdk/referrer-lookup` | public | `checkDeferredDeepLink` on Android | | `POST /api/v1/sdk/match` | public | `checkDeferredDeepLink` (fingerprint step), `findDeferredUser` | | `GET /api/v1/sdk/resolve` | public | `listen` with `resolve: true`, `resolveLink` | ## Support - Email: support@deeplinknow.com - Package: [pub.dev/packages/deeplinknow](https://pub.dev/packages/deeplinknow) --- Source: https://deeplinknow.com/docs/flutterflow # Deferred deep links in FlutterFlow Send people to the right page of your FlutterFlow app from any link, including after they install it from the App Store or Google Play. You add one custom action, paste one file, and set one On Page Load action. A DeepLinkNow link's path is a FlutterFlow route. `https://yourapp.deeplinknow.com/productPage?productId=42` opens the page whose route is `/productPage`, with its `productId` parameter set to 42. There's nothing to create in a dashboard first: every path on your link domain works. ## Before you start - A DeepLinkNow account (free up to 25,000 monthly active users) and an app created in the dashboard. - Your iOS Team ID and Bundle ID, and your Android package name and Play signing SHA-256 certificate. The dashboard's setup page says where to find each one, and fills most of them in from your App Store or Play link. - A build on a real phone to test. Deep links don't run in FlutterFlow's Run or Test mode, because the plugin has no web version. ## 1. Set up your app in DeepLinkNow 1. In the dashboard, open your app's setup page (or **Settings › iOS** and **Settings › Android**) and fill in the Team ID, Bundle ID and App Store ID, and the package name and SHA-256 certificate. 2. Open **Settings › API keys** and copy the **public** key (`dln_pub_…`). It's safe to put in your app. Your link domain is `.deeplinknow.com`, shown on the app's Overview. ## 2. Add the custom action 1. In FlutterFlow, open **Custom Code** and add an **Action** named `dlnOpenDeferredLink`. 2. In its settings: - turn on **Include BuildContext**; - add one argument, `publicKey`, of type **String**; - set the return value to **String**, nullable; - under **Pubspec Dependencies**, add `deeplinknow: ^1.4.1`. 3. Keep the automatic imports FlutterFlow puts at the top of the file, and replace everything below `// DO NOT REMOVE OR MODIFY THE CODE ABOVE!` with: ```dart import 'package:deeplinknow/deeplinknow.dart'; import 'package:go_router/go_router.dart'; /// Opens the page for the link the user tapped before installing, and /// returns the location it opened (or null). Add it as the first On Page /// Load action of your entry page. The link's path is a FlutterFlow route: /// /productPage?productId=42 opens the page whose route is /productPage. Future dlnOpenDeferredLink( BuildContext context, String publicKey, ) async { if (!DeepLinkNow.isInitialized) { final info = await DeepLinkNow.initialize(publicKey); if (info == null) return null; } final link = await DeepLinkNow.checkDeferredDeepLink(); if (link == null || !context.mounted) return null; var value = link.fullPath.trim(); if (value.isEmpty) return null; if (!value.startsWith('/')) value = '/$value'; final queryAt = value.indexOf('?'); var path = queryAt < 0 ? value : value.substring(0, queryAt); var query = queryAt < 0 ? '' : value.substring(queryAt); if (query == '?') query = ''; while (path.length > 1 && path.endsWith('/')) { path = path.substring(0, path.length - 1); } if (path == '/' && query.isEmpty) return null; final location = '$path$query'; context.push(location); return location; } ``` 4. Save and compile. `go_router` is already part of every FlutterFlow project. ## 3. Run it on the entry page 1. Open your app's entry page (the first page people see). 2. In **Actions › On Page Load**, add **Custom Action › dlnOpenDeferredLink** as the first action, and set `publicKey` to your public key. The first time the app opens after an install, the action asks DeepLinkNow which link the person tapped and opens that page. A link to `/`, or no link, leaves them on the entry page. Every later launch skips the check without a network call. A page's route is under the page's settings. Page parameters come from the link's query string, so a page with a `productId` parameter reads it from `?productId=42`. ## 4. Open the installed app from links For a tap to open the app when it's already installed, iOS and Android need to know your app owns the link domain. In each snippet, replace `yourapp.deeplinknow.com` with your link domain. 1. **Settings › App Details › Deep Linking**: turn deep linking on. 2. **Custom Code › Configuration Files › Runner.entitlements**: add ```xml com.apple.developer.associated-domains applinks:yourapp.deeplinknow.com ``` 3. **Custom Code › Configuration Files › AndroidManifest.xml**, in **Activity Tags**: add ```xml ``` 4. In the Apple Developer portal, open your App ID under **Certificates, Identifiers & Profiles** and turn on **Associated Domains**. If you add a custom domain in DeepLinkNow later, add it to both snippets and release an update. ## 5. Test it 1. Delete the app from your phone. 2. On the phone, tap a link such as `https://yourapp.deeplinknow.com/productPage?productId=42`. The link page sends you to the store. 3. Install your build (TestFlight, an internal test track, or a local run) and open it. It lands on the product page. To test again without reinstalling, add a second custom action, `dlnResetDeferredLink` (no arguments, no return value, the same dependency), call it from a debug-only button, then restart the app: ```dart import 'package:deeplinknow/deeplinknow.dart'; /// For testing only: the next dlnOpenDeferredLink asks again, as after a /// fresh install. Future dlnResetDeferredLink() async { try { await DeepLinkNow.resetDeferredDeepLink(); } catch (_) { // Already logged by the SDK. } } ``` ## Troubleshooting - **Nothing opens.** Check the device logs for `[DeepLinkNow]` (iOS) or `DLN` (Android). An invalid key, or an app with no App Store ID or package name, shows up there. - **It opens the wrong page, or an error page.** The link's path has to match a page's route exactly, including case. - **Taps open the browser instead of the installed app.** Check your domain with the [AASA validator](/tools/aasa-validator) and the [assetlinks validator](/tools/assetlinks-validator), and that Associated Domains is on for the App ID. - **Coming from Branch?** The [switch check](/tools/switch-check) shows how your existing links move. The plugin underneath is [`deeplinknow` on pub.dev](https://pub.dev/packages/deeplinknow); the [Flutter SDK docs](/docs/flutter) cover the rest of its API. --- Source: https://deeplinknow.com/docs/mcp # DeepLinkNow MCP server for coding agents ## Overview [`@deeplinknow/mcp`](https://www.npmjs.com/package/@deeplinknow/mcp) is an MCP server for coding agents such as Claude Code, Cursor, Claude Desktop, VS Code and Windsurf. With it installed, the agent can read DeepLinkNow's current docs, check your link domain and plan a migration while it writes the integration, instead of working from memory. It covers iOS, Android, React Native, Expo and Flutter apps. Like DeepLinkNow itself, it does deep linking only: it never adds attribution, ad tracking or advertising IDs to your app. The server runs on your machine over stdio and needs Node.js 20 or later. The source is MIT licensed: [github.com/deeplinknow/dln-mcp](https://github.com/deeplinknow/dln-mcp). ## Install One-click install, with no key: [Add to Cursor](https://cursor.com/install-mcp?name=deeplinknow&config=eyJjb21tYW5kIjoibnB4IC15IEBkZWVwbGlua25vdy9tY3AifQ%3D%3D) · [Install in VS Code](https://insiders.vscode.dev/redirect?url=vscode%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522deeplinknow%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522-y%2522%252C%2522%2540deeplinknow%252Fmcp%2522%255D%257D). Claude Code: `claude mcp add deeplinknow -- npx -y @deeplinknow/mcp`. Once it's installed, try asking your agent: - "Use DeepLinkNow to add deferred deep linking to this Expo app. Our link domain is myapp.deeplinknow.com." (`get_setup_guide`) - "Check whether links.example.com's AASA and assetlinks.json files are set up correctly." (`check_domain`) - "We use Firebase Dynamic Links. Plan our move to DeepLinkNow for this app." (`migrate_from`) - "A DeepLinkNow match came back MEDIUM with a score of 58. Should we route it?" (`explain_match_result`) - "Search the DeepLinkNow docs: how do I test a deferred deep link on Android?" (`search_docs`) The one-click links and the commands above install the server without a key. To add the private key for the link tools, use the per-client configs below. ### Claude Code ```sh claude mcp add deeplinknow -- npx -y @deeplinknow/mcp ``` With the link tools (`--scope user` keeps the key out of the project's `.mcp.json`): ```sh claude mcp add deeplinknow --scope user --env DEEPLINKNOW_API_KEY=YOUR_PRIVATE_KEY -- npx -y @deeplinknow/mcp ``` ### Cursor Add to `.cursor/mcp.json` in the project, or `~/.cursor/mcp.json` for every project: ```json { "mcpServers": { "deeplinknow": { "command": "npx", "args": ["-y", "@deeplinknow/mcp"], "env": { "DEEPLINKNOW_API_KEY": "${env:DEEPLINKNOW_API_KEY}" } } } } ``` Leave out `env` if you don't need the link tools. ### Claude Desktop Open **Settings → Developer → Edit Config**, which opens `claude_desktop_config.json` (`~/Library/Application Support/Claude/` on macOS, `%APPDATA%\Claude\` on Windows), and add: ```json { "mcpServers": { "deeplinknow": { "command": "npx", "args": ["-y", "@deeplinknow/mcp"] } } } ``` Restart Claude Desktop. To use the link tools, add `"env": { "DEEPLINKNOW_API_KEY": "YOUR_PRIVATE_KEY" }` next to `args`. ### VS Code Add to `.vscode/mcp.json`. VS Code asks for the key once and stores it securely: ```json { "inputs": [ { "type": "promptString", "id": "deeplinknow-api-key", "description": "DeepLinkNow private API key (dln_priv_…), only for list_links and create_link. Leave empty otherwise.", "password": true } ], "servers": { "deeplinknow": { "type": "stdio", "command": "npx", "args": ["-y", "@deeplinknow/mcp"], "env": { "DEEPLINKNOW_API_KEY": "${input:deeplinknow-api-key}" } } } } ``` ### Windsurf Add to `~/.codeium/windsurf/mcp_config.json`: ```json { "mcpServers": { "deeplinknow": { "command": "npx", "args": ["-y", "@deeplinknow/mcp"], "env": { "DEEPLINKNOW_API_KEY": "YOUR_PRIVATE_KEY" } } } } ``` Leave out `env` if you don't need the link tools. ### Other clients Any MCP client that runs stdio servers: command `npx`, arguments `-y @deeplinknow/mcp`. ## What to ask ```text Add DeepLinkNow deferred deep linking to this app. Our link domain is myapp.deeplinknow.com. ``` The agent reads the setup guide for your platform, installs the SDK, adds the Associated Domains entitlement and the Android intent filter, calls `checkDeferredDeepLink()` on launch and routes the result through your router. Then it checks your domain. Other requests it handles: - "Check myapp.deeplinknow.com" (or your custom domain). It validates `apple-app-site-association` against Apple's CDN copy and `assetlinks.json` against Google's Digital Asset Links API, and looks the domain up on the EasyPrivacy and AdGuard block lists. It works for any domain, including the one you use today. - "We're on Firebase Dynamic Links. Move us to DeepLinkNow." Branch, AppsFlyer and Adjust work too. You get a plan and a code diff for your platform. If you use one of those for attribution, the plan keeps it for that and moves only the deep links. - "This match came back MEDIUM with a score of 58. Should we route it?" The agent explains each part of the score and what a `minConfidence` of 50 or 75 would do. The threshold stays your decision. ## Tools | Tool | Needs a key | What it does | |---|---|---| | `get_setup_guide` | No | Setup steps and code for `ios`, `android`, `react-native`, `expo` or `flutter`, from these docs (a bundled copy when offline) | | `search_docs` | No | Searches the full DeepLinkNow docs and returns the matching sections with links | | `check_domain` | No | Checks a domain's `apple-app-site-association` (and Apple's CDN copy), `assetlinks.json` (and Google's verdict), and whether the domain or its CNAME chain is on the EasyPrivacy or AdGuard block lists | | `explain_match_result` | No | Explains a fingerprint match's `confidence_score` and `match_quality`, and how to choose `minConfidence` | | `migrate_from` | No | A migration plan and code diff from `firebase-dynamic-links`, `branch`, `appsflyer` or `adjust` | | `list_links` | Private key, paid plan | Lists the app's links (dashboard, API and on-demand), with search and paging | | `create_link` | Private key, paid plan | Creates a link with a custom path, link-page text, social preview, metadata or campaign | It also exposes `deeplinknow://llms.txt` and `deeplinknow://docs/{page}` as resources, and two prompts: `add_deferred_deep_linking` and `migrate_to_deeplinknow`. The first `check_domain` run downloads the block lists (about 22 MB). Later checks in the same session reuse them for a day. ## Keys You don't need an account or a key for the first five tools. `list_links` and `create_link` call the [management API](/docs/api/deeplinks), which needs your app's **private** key (`dln_priv_…`, from the app's API Keys page in the dashboard) as `DEEPLINKNOW_API_KEY`. The management API is on paid plans ([pricing](/pricing)). A private key acts on one app. You never have to create links: any path on your link domain already works. The link tools are for listing links, or for creating one with a custom preview or campaign. The private key stays in your MCP config. Your app only ever contains the public key (`dln_pub_…`). The server refuses a public key for the link tools without sending it, and it never prints, logs or returns the private key: it sends it only in the `x-api-key` header to the DeepLinkNow API. | Variable | Default | Meaning | |---|---|---| | `DEEPLINKNOW_API_KEY` | unset | Your app's private key, only for `list_links` and `create_link` | | `DEEPLINKNOW_API_URL` | `https://deeplinknow.com` | API base URL. Must be https (http only for localhost) | ## What it doesn't do - It never writes files or runs commands. The server only returns text to the agent. Your agent makes the code changes, through its own tools and permission prompts. - It stores nothing on disk. Docs, block lists and check results are cached in memory while the server runs (docs for an hour, block lists for a day, domain results for 5 minutes). - No telemetry: it collects no usage data. - It sends nothing about your code. `get_setup_guide`, `search_docs`, `migrate_from` and the resources fetch public docs from `https://deeplinknow.com`. `check_domain` fetches only the domain you name (over https, refusing private and local addresses), Apple's CDN, Google's Digital Asset Links API and the four public block lists. `explain_match_result` makes no requests. Only the link tools send your key and the link fields, to the DeepLinkNow API. ## Keep agents on track Paste this into your repo's `AGENTS.md` or `CLAUDE.md` so every session follows the same rules: ```markdown ## Deep linking: DeepLinkNow - Link domain: myapp.deeplinknow.com. Any path and query on it is a deep link; nothing has to be created first. - The app contains only the public key (dln_pub_…). Never commit or ship the private key. - Initialize once at launch. Call checkDeferredDeepLink() once on launch and route to link.path with link.queryParams. Send links that open the installed app through the SDK's parse method and the same router. - Fingerprint matches are scored (confidenceScore, matchQuality). minConfidence is off by default; decide how to treat MEDIUM and LOW. - Use the DeepLinkNow MCP tools (get_setup_guide, check_domain, search_docs) instead of guessing. ``` ## Support - Email: support@deeplinknow.com - Package: [npmjs.com/package/@deeplinknow/mcp](https://www.npmjs.com/package/@deeplinknow/mcp) - Source: [github.com/deeplinknow/dln-mcp](https://github.com/deeplinknow/dln-mcp) --- Source: https://deeplinknow.com/docs/firebase-migration # Migrating from Firebase Dynamic Links Firebase Dynamic Links shut down on 25 August 2025: every `page.link` URL now returns a 404, and the `.page.link` domains are gone. Nobody can bring those URLs back, DeepLinkNow included. This guide moves the *behaviour* to DeepLinkNow: links that open your app if it's installed, survive an install if it isn't, and hand your app the original link on first launch. There are two ways to do it. Pick one, or start with the first and move to the second over time. ## Option 1: keep your long links, change the host If you built Dynamic Links in code as long links, like ``` https://yourapp.page.link/?link=https%3A%2F%2Fyourapp.com%2Finvite%2F8Fk2&apn=com.yourapp&ibi=com.yourapp.ios ``` the same URL works on your DeepLinkNow domain with only the host changed: ``` https://yourapp.deeplinknow.com/?link=https%3A%2F%2Fyourapp.com%2Finvite%2F8Fk2&apn=com.yourapp&ibi=com.yourapp.ios ``` DeepLinkNow accepts any path and query on your domain without creating links first, and passes the query through untouched. Your app gets the Firebase-style `link` parameter back, already decoded, in `queryParams.link`, both when the link opens the installed app and after an install. All long links on `/` count as **one** link against your plan's link limit, because the limit counts distinct paths and ignores the query string. What's different from Firebase: - `apn`, `ibi`, `isi` and `efr` aren't needed. DeepLinkNow knows your app from the domain, and it sends users to the App Store or Google Play listing you set in the dashboard. They're harmless to leave in. - `afl`, `ifl` and `ofl` (fallback URLs) aren't followed. Phones go to the store; desktop shows a QR code and store buttons. - `st`, `sd` and `si` (social preview) aren't read from the URL. Set the preview title, description and image in the dashboard (paid plans). - Short links (`yourapp.page.link/abc`) have no equivalent code to keep: use a path instead (Option 2). If your short links were on your own domain, you can import them (see below). ### React Native Before (`@react-native-firebase/dynamic-links`): ```typescript const initial = await dynamicLinks().getInitialLink(); if (initial) handle(initial.url); dynamicLinks().onLink((link) => handle(link.url)); ``` After (`@deeplinknow/react-native`): ```typescript import DeepLinkNow from "@deeplinknow/react-native"; import { Linking } from "react-native"; await DeepLinkNow.initialize("dln_pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); const linkParam = (q: Record) => typeof q.link === "string" ? q.link : null; // After an install: the link the user tapped before installing. const deferred = await DeepLinkNow.checkDeferredDeepLink(); const fromInstall = deferred && linkParam(deferred.queryParams); if (fromInstall) handle(fromInstall); // App already installed: the Universal Link / App Link that opened it. const onUrl = (url: string | null) => { const parsed = url ? DeepLinkNow.parseDeepLink(url) : null; const value = parsed && linkParam(parsed.queryParams); if (value) handle(value); }; Linking.getInitialURL().then(onUrl); Linking.addEventListener("url", ({ url }) => onUrl(url)); ``` ### iOS (Swift) ```swift import DeepLinkNow await DeepLinkNow.initialize(config: DLNConfig(apiKey: "dln_pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")) // After an install if let deferred = await DeepLinkNow.checkDeferredDeepLink(), let value = deferred.queryParams["link"]?.stringValue { handle(URL(string: value)) } // App already installed (SwiftUI) .onOpenURL { url in if let link = DeepLinkNow.parseLink(url), let value = link.queryParams["link"]?.stringValue { handle(URL(string: value)) } } ``` ### Android (Kotlin) ```kotlin // After an install (inside a coroutine; it returns a Result and never throws) DLN.checkDeferredDeepLink().onSuccess { deferred -> (deferred?.queryParams?.get("link") as? QueryValue.Single)?.value?.let(::handle) } // App already installed (onCreate and onNewIntent) intent.data?.toString()?.let { url -> DLN.parseDeepLink(url)?.let { link -> (link.queryParams["link"] as? QueryValue.Single)?.value?.let(::handle) } } ``` ## Option 2: put the path in the link Firebase needed the `link=` wrapper because `page.link` wasn't your domain. On DeepLinkNow it is. Instead of wrapping `https://yourapp.com/invite/8Fk2`, use the path directly: ``` https://yourapp.deeplinknow.com/invite/8Fk2 ``` Your app gets `path: "/invite/8Fk2"` and any query parameters, and routes on them. This is shorter, reads better in a message, and works with your own domain (`go.yourapp.com/invite/8Fk2`) on paid plans. Each distinct path counts once against your link limit, so put per-user values in the query (`/invite?code=8Fk2`) if you generate a lot of them. ## Import your old short links If your Dynamic Links used your own domain (such as `links.yourapp.com`) rather than `page.link`, the short links people already shared can keep working. Point that domain at DeepLinkNow as your app's custom domain, then import the links Firebase let you export. 1. Export your Dynamic Links with Google Takeout. You get one CSV file per domain. 2. In the dashboard, open your app's **Deep Links** page and choose **Import from Firebase**. 3. Choose the CSV files. The preview shows how many links each file holds, which domains they use, and any rows that can't be imported. 4. Choose **Import**. When it finishes you can download the rows that weren't imported as a CSV, fix them and import that file again. Each short link becomes a link on the same path, so `https://links.yourapp.com/abc123` is stored as `abc123`. Its target URL is the original Firebase deep link, its link name becomes the title, and the social title, description and image carry over. The other Firebase parameters (`apn`, `ibi`, `utm_source` and so on) are kept in the link's metadata under `fdl`. When someone taps an imported link and installs your app, the deferred result carries the original deep link in `targetUrl`. When the app is already installed, the link opens it directly without reaching DeepLinkNow. From SDK 1.4.0, register `onLink` with `resolve: true` and the SDK looks each open up for you, so `link.targetUrl` holds the original deep link (see the [iOS](/docs/ios#handle-every-link-with-one-handler), [Android](/docs/android#handle-every-link-with-one-handler), [React Native](/docs/react-native#handle-every-link-with-one-handler) and [Flutter](/docs/flutter#every-open-in-one-stream) docs). Otherwise, look the path up yourself: ```typescript const res = await fetch( `https://deeplinknow.com/api/v1/sdk/resolve?path=${encodeURIComponent("/abc123")}`, { headers: { "x-api-key": "dln_pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } } ); if (res.ok) { const { deeplink } = await res.json(); handle(deeplink.target_url); } ``` A path your app already has is skipped, never overwritten. Every imported link counts toward your plan's link limit, and links past it are reported so you can import them again after upgrading. Archived links are left out unless you ask for them. ## Checklist 1. Create the app in the DeepLinkNow dashboard and enter your iOS Team ID and Bundle ID and your Android package name and SHA-256 fingerprints. DeepLinkNow serves `apple-app-site-association` and `assetlinks.json` for your domain. 2. Add `applinks:yourapp.deeplinknow.com` to Associated Domains (iOS) and an `autoVerify` https intent filter for it (Android). With Expo, the config plugin does both. 3. Replace the Firebase Dynamic Links SDK calls as shown above, and remove the `firebase_dynamic_links` / `@react-native-firebase/dynamic-links` dependency. 4. Change the host in the code that builds links (Option 1), or switch to paths (Option 2). 5. Check your domain with the free [deep link domain checker](/tools). ## What DeepLinkNow doesn't replace - Old `page.link` URLs no longer work. Update links you control (emails, QR codes, your site), and accept that old shared links won't open. - Firebase Auth email-link sign-in used Dynamic Links for the sign-in email; Firebase's own documentation covers moving that flow off Dynamic Links. DeepLinkNow doesn't handle sign-in links. - Firebase reported clicks into Google Analytics. DeepLinkNow shows click and install counts, and hands your app the link so you can send it to your own analytics. --- Source: https://deeplinknow.com/docs/integration-checklist # Integration checklist An end-to-end checklist for integrating DeepLinkNow into your mobile app. ## Dashboard setup ### Account and app - [ ] Create a DeepLinkNow account and an app in the dashboard - [ ] Choose your app alias (e.g. `myapp` → `myapp.deeplinknow.com`) - [ ] Copy your keys from the **API Keys** page. Each app gets one **public** key (`dln_pub_…`, for the SDKs) and one **private** key (`dln_priv_…`, for your server only). The public key is always shown there. The private key is shown once; regenerate it there if you lose it, and choose how long the old one keeps working (24 hours by default, or immediately if it leaked). - [ ] Optional: configure a fallback URL for desktop visitors ### iOS settings - [ ] Bundle ID (e.g. `com.company.myapp`) - [ ] Team ID / App ID prefix (from your Apple Developer account) - [ ] App Store ID (once the app is published) ### Android settings - [ ] Package name (e.g. `com.company.myapp`) - [ ] SHA-256 certificate fingerprints: Play App Signing, upload key and debug (`keytool -list -v -keystore ~/.android/debug.keystore`) ### Verify the .well-known files - [ ] `https://myapp.deeplinknow.com/.well-known/apple-app-site-association` contains your Team ID and Bundle ID - [ ] `https://myapp.deeplinknow.com/.well-known/assetlinks.json` contains your package name and fingerprints These files are served from your saved settings and cached for up to 5 minutes. ## SDK installation - [ ] **React Native / Expo**: `npx expo install @deeplinknow/react-native` (development build, not Expo Go) or `npm install @deeplinknow/react-native` + `pod install` - [ ] **iOS**: `pod 'DeepLinkNow', '~> 1.4'` or Swift Package Manager `https://github.com/deeplinknow/dln-ios` - [ ] **Android**: `implementation("com.deeplinknow:dln-android:1.4.1")` (the library declares `INTERNET` itself) - [ ] **Flutter**: `flutter pub add deeplinknow` (iOS 13+, Android `minSdk` 24; rebuild the app after adding it) - [ ] iOS: Associated Domains `applinks:myapp.deeplinknow.com` (and your custom domain) - [ ] Android: `autoVerify` https intent filter for `myapp.deeplinknow.com` (and your custom domain) - [ ] No ATT prompt, IDFA, or advertising-ID permission is needed for DeepLinkNow ## Initialization - [ ] Initialize once at app start with your **public** key (`dln_pub_…`) - [ ] Never ship your private key in an app - [ ] Handle an initialization failure (invalid key, inactive account, offline) separately from "no deferred link" - [ ] Use `enableLogs` only in development builds ## Deferred deep linking - [ ] Register `onLink` at launch (Flutter: `DeepLinkNow.links` and `listen()`), or call `checkDeferredDeepLink()` once on launch, after initialization - [ ] Route to `link.path` with `link.queryParams` when a link arrives - [ ] If linked screens need a signed-in user, register with `hold` and call `releasePendingLink()` after sign-in - [ ] Decide your threshold for fingerprint results. Every result carries `confidenceScore` (0–100) and `matchQuality` (`HIGH` 75+, `MEDIUM` 50–74, `LOW` 30–49). The optional `minConfidence` filter is off by default. - [ ] Consider confirming `MEDIUM` or `LOW` fingerprint matches with the user before routing - [ ] iOS: decide whether to keep the pasteboard step (`useClipboard`, default `true`), which can show the iOS 16+ paste prompt - [ ] Treat errors as "try again later", not as "no link". The SDK retries on the next call. ## Links that open the installed app - [ ] With `onLink`: pass incoming URLs to `DeepLinkNow.handle(url)` (iOS) or intents to `DLN.handleIntent(intent)` in `onCreate` (when `savedInstanceState` is null) and `onNewIntent` (Android). React Native and Flutter read them for you. Without `onLink`, pass them to `parseDeepLink(url)` - [ ] Custom URL schemes: bare React Native apps list them in `schemes`; the other SDKs read them from Info.plist, the Android manifest or the Expo config - [ ] Imported Firebase short links: turn on `resolve` so each open carries the original deep link in `targetUrl` - [ ] Route deferred and direct links through the same function ```typescript function route(path: string, query: Record) { switch (path) { case "/product": navigation.navigate("Product", { id: query.id }); break; case "/category": navigation.navigate("Category", { name: query.name }); break; default: navigation.navigate("Home"); } } ``` - [ ] Handle unknown paths gracefully ## Testing ### Direct links - [ ] Tap `https://myapp.deeplinknow.com/product?id=123` from Notes or Messages with the app installed: the app opens on the right screen - [ ] Android: `adb shell pm get-app-links ` shows your domains as verified ### Deferred links - [ ] Uninstall the app, tap a link, install, open: the app lands on the right screen - [ ] iOS: test both **Allow** and **Don't Allow** on the paste prompt (Don't Allow falls back to fingerprint matching) - [ ] Android: test an install through the link's Play Store button (Install Referrer) and one without it (fingerprint) - [ ] Check the logs for each step's outcome, `source`, `confidenceScore` and `matchQuality` - [ ] Use `resetDeferredDeepLink()` or `force: true` to repeat the test without reinstalling - [ ] A click older than 1 hour is not offered as a fingerprint match ### Edge cases - [ ] First launch offline: no crash, and the next launch still finds the link - [ ] Unknown path: falls back to a sensible screen - [ ] A link copied from another app's domain is ignored ## Production readiness - [ ] Only the public key is in the app binary - [ ] Logging is off in release builds - [ ] `.well-known` files are correct for your release signing certificates - [ ] Privacy labels: the SDKs collect device characteristics and an app-scoped install ID for app functionality only, not for tracking ## Optional - [ ] Custom domain (paid plans): add it under **Domains**, create the DNS record, wait for verification, then add it to Associated Domains and your intent filters - [ ] Customize the landing page (title, subtitle, button text, social preview) - [ ] Forward deferred-link events to your own analytics tool (DeepLinkNow does not do analytics) ## Need help? - [Quick Start](/docs/quickstart) - [iOS](/docs/ios), [Android](/docs/android), [React Native](/docs/react-native), [Flutter](/docs/flutter) - [MCP server for coding agents](/docs/mcp) - Email: support@deeplinknow.com --- Source: https://deeplinknow.com/pricing # DeepLinkNow pricing Prices in USD. Startup, Growth and Scale are billed monthly, or yearly at 15% off: cancel any time, and paid plans keep their features until the end of the billing period. Enterprise is billed annually, by invoice. No free trial is needed: the Developer plan is free, with no card. ## Developer: $0/month Deep links and deferred deep links for one app, free. - 25k SDK monthly active users - 1k deep links (distinct paths, on-demand included) - Single app on yourapp.deeplinknow.com - Just you (1 seat) - Universal Links, App Links and deferred deep links - iOS, Android, React Native and Flutter SDKs - Click and install counts - Community support on Discord ## Startup: $49/month Your own domain and a landing page that looks like your app. - 100k SDK monthly active users - 10k deep links (distinct paths, on-demand included) - Custom domains: your links on go.yourapp.com (up to 3) - Up to 3 apps - Up to 3 team members, with admin and member roles - Your branding on the link page - Custom link page text and buttons, per app or per link - Custom social previews for iMessage, Slack, WhatsApp and more - Link page translated into 14 languages - Link rules: allowed paths and a not-found redirect - Android: skip the link page, straight to Google Play - Fallback URL for visitors on a platform with no store listing - Management API (10k requests/mo) - Email support ## Growth: $149/month For larger apps, or several apps under one account. - 500k SDK monthly active users - 50k deep links (distinct paths, on-demand included) - Unlimited custom domains - Unlimited apps - Up to 5 team members - Everything in Startup - Management API (50k requests/mo) - Email support ## Scale: $499/month For apps with millions of users, without a sales call or a contract. - 2M SDK monthly active users - 200k deep links (distinct paths, on-demand included) - Unlimited apps and custom domains - Up to 10 team members - Everything in Growth - Management API (200k requests/mo) - Priority email support ## Enterprise: Starting at $500/month, billed annually For companies whose IT, legal and procurement teams have to sign off. - Everything in Scale - Custom MAU, link and API volume, including per-minute rate limits - Pay by invoice, against your purchase order - Unlimited team members - Single sign-on with Microsoft Entra ID, Okta or Google Workspace, and SSO required for your domain - Audit log of who changed what in your workspace (coming soon) - Data processing agreement (DPA) - Priority support ## Every plan, side by side | | Developer | Startup | Growth | Scale | Enterprise | |---|---|---|---|---|---| | SDK monthly active users | 25,000 | 100,000 | 500,000 | 2,000,000 | Custom | | Deep links (distinct paths, on-demand included) | 1,000 | 10,000 | 50,000 | 200,000 | Custom | | On-demand links: any path on your domain works, no setup | Yes | Yes | Yes | Yes | Yes | | Apps | 1 | 3 | Unlimited | Unlimited | Unlimited | | Team members (including you) | 1 | 3 | 5 | 10 | Unlimited | | Custom domains: your links on go.yourapp.com (one per app) | No | 3 | Unlimited | Unlimited | Unlimited | | Universal Links + App Links, hosted .well-known files | Yes | Yes | Yes | Yes | Yes | | Deferred deep links (Install Referrer, clipboard, fingerprint) | Yes | Yes | Yes | Yes | Yes | | iOS, Android, React Native and Flutter SDKs | Yes | Yes | Yes | Yes | Yes | | Your branding on the link page (DLN branding removed) | No | Yes | Yes | Yes | Yes | | Custom title, subtitle and button text, per app or per link | No | Yes | Yes | Yes | Yes | | Custom social previews (title, description, image) for iMessage, Slack, WhatsApp, X and more | No | Yes | Yes | Yes | Yes | | Link page translated automatically into 14 languages | No | Yes | Yes | Yes | Yes | | Android: skip the link page and go straight to Google Play | No | Yes | Yes | Yes | Yes | | Fallback URL for visitors on a platform with no store listing | No | Yes | Yes | Yes | Yes | | Link rules: turn off auto-created links, allow paths by regex, not-found redirect | No | Yes | Yes | Yes | Yes | | Management API (requests per month) | No | 10,000 | 50,000 | 200,000 | Custom | | API requests per minute, per key | 600 (SDK key only) | 600 | 1,200 | 1,200 | Custom | | Sign in with an email code, Google or GitHub | Yes | Yes | Yes | Yes | Yes | | Support | Discord | Email | Email | Priority email | Priority | | Single sign-on (Entra ID, Okta, Google Workspace), SSO required | No | No | No | No | Yes | | Audit log | No | No | No | No | Coming soon | | Billing | Free | Monthly or yearly, by card | Monthly or yearly, by card | Monthly or yearly, by card | Invoice, billed annually | | Purchase orders | No | No | No | No | Yes | --- Source: https://deeplinknow.com/compare # DeepLinkNow compared with every deep linking vendor As of October 2026. Each vendor's page links the public source for every claim. | | Free plan with deferred deep links | Plan with a custom domain | Bills on | Any path works without setup | Reads advertising IDs | Sales call | |---|---|---|---|---|---|---| | DeepLinkNow | Yes, up to 25,000 MAU | $49/mo (Startup) | Monthly active users | Yes, no prefix | Never | Enterprise only | | Branch | No, a 30-day trial with a card | $199/mo (Activation Basics) | Volume credits, links and clicks | Long links only | By default | Essentials and Enterprise | | AppsFlyer | No (Zero plan) | Sales only (Advanced tier) | Not published | No, short links are created | IDFA, with ATT support on | Every paid tier | | Adjust | In TrueLink Base | Not offered (a brand.go.link name only) | Attributions | No, links are created | By default (Android AD_ID) | Paid plans, by contract | | Singular | Yes, 15,000 paid conversions | Sales only (Growth and up) | Paid conversions | No, links are created | IDFA when allowed | Growth and Enterprise | | Kochava | Yes, 10,000 conversions | From $500/mo (Foundation), via your CSM | Attributed conversions | No, SmartLinks are created | IDFA and GAID modules | Trial form for paid plans | | Airbridge | No, a 30-day trial | About $134/mo at 100,000 users (Core, billed by data points) | Data points | No, links are created | By default | Above Core | | Detour | Yes, 1,500 clicks a month | $79/mo (Scale) | Link clicks | After a fixed // segment | No | Enterprise only | | LinkTrace | One-time 1,000 installs | Not offered (a linktrace.in subdomain only) | Links and attributed installs | No, links are created | No | None | | AppsOnAir | No | $54.97/mo (Pro with AppLink) | Plan plus modules | No, links are created | Not stated | Enterprise only | | URLgenius | No | $49/mo and $0.02 a click (Growth) | Clicks | No, links are created | No (no SDK) | Custom plan (deferred links) | ## Attribution platforms Measurement platforms with deep links inside. If you need attribution, these are the real choice. If you don't, you pay for it anyway, in money, advertising IDs or both. - [DeepLinkNow vs Branch](https://deeplinknow.com/compare/branch.md): Branch dropped its free tier. Custom domains start at $199/mo. - [DeepLinkNow vs AppsFlyer](https://deeplinknow.com/compare/appsflyer.md): No deferred deep links on the free plan. Paid tiers need a sales call. - [DeepLinkNow vs Adjust](https://deeplinknow.com/compare/adjust.md): Attribution first. Deferred links may be limited after an ATT opt-out. - [DeepLinkNow vs Singular](https://deeplinknow.com/compare/singular.md): Free to 15,000 paid conversions. Every deep link is an attribution link. - [DeepLinkNow vs Kochava](https://deeplinknow.com/compare/kochava.md): Free attribution with deferred links. Universal Links and your domain start at $500/mo. - [DeepLinkNow vs Airbridge](https://deeplinknow.com/compare/airbridge.md): An MMP billed by data points: about $134/mo at 100,000 users. ## Developer-first deep linking tools Smaller tools that do deep links without the full attribution suite, most of them started after Firebase Dynamic Links shut down. - [DeepLinkNow vs Detour](https://deeplinknow.com/compare/detour.md): 1,500 free clicks, custom domains at $79/mo, and no match score in the app. - [DeepLinkNow vs LinkTrace](https://deeplinknow.com/compare/linktrace.md): No SDK, fingerprint only, and no custom domain. Universal Links need its top plan. - [DeepLinkNow vs AppsOnAir](https://deeplinknow.com/compare/appsonair.md): $54.97/mo for AppLink on your own domain, and no AppLink on the free plan. - [DeepLinkNow vs URLgenius](https://deeplinknow.com/compare/urlgenius.md): Billed per click. Deferred links start at $199/mo on its Custom plan. - [Firebase Dynamic Links alternative](https://deeplinknow.com/firebase-dynamic-links-alternative): Google shut it down on August 25, 2025. What DeepLinkNow does in its place, and what it doesn't. DeepLinkNow is not affiliated with the vendors compared here. --- Source: https://deeplinknow.com/compare/branch # DeepLinkNow vs Branch Deep linking and attribution platform. As of October 2026, with a public source for every claim. I used Branch on a real startup. It worked, mostly, until the bill and the ad blockers showed up. Branch is a fine product if you're a marketing team that wants attribution and campaign analytics. If you just want a link to open the right screen after install, you're paying for a lot you'll never open. | | DeepLinkNow | Branch | Stronger | Sources | |---|---|---|---|---| | Free plan | Free for up to 25,000 monthly active users, deferred deep links included, and you don't need a card to start | There isn't one. The 30-day trial needs a work email and a card, then rolls onto Intro at $39/mo. | DeepLinkNow | [1] [2] | | Usage over the plan | Links and SDK calls keep working, and I'll ask you to upgrade | Billed per extra volume credit, link and click, uncapped. | DeepLinkNow | [3] | | Plan with a custom domain | $49/mo (Startup) | $199/mo (Activation Basics) | DeepLinkNow | [4] | | iOS clipboard handoff | On every plan, including the free one | NativeLink, a premium feature | DeepLinkNow | [5] | | Making links | Any path on your domain is already a link, with nothing to create first. | Created in the dashboard or API, or long links with /a/key_live_… on a custom domain. Routing by path needs $deeplink_path and custom logic. | DeepLinkNow | [6] [7] | | Advertising IDs | The SDKs never read them, so there's no ATT prompt | Collected by default (FULL protection level: advertising IDs and device IDs) | DeepLinkNow | [8] | | Ad-blocker lists | Not on any of them | Branch domains and its web SDK are on AdGuard and EasyPrivacy, and EasyPrivacy blocks about 4,195 Branch customer link domains. | DeepLinkNow | [9] [10] | | Typical contract | Published on the site: $0, $49, $149 or $499 a month, paid monthly or yearly, and Enterprise from $500 a month | Median about $55K a year (Vendr, 61 deals) | DeepLinkNow | [11] | ## Where DeepLinkNow is ahead - A free plan for up to 25,000 monthly active users, deferred deep links included, and you don't need a card to start. - Your own domain from $49 a month on Startup, set up in the dashboard. - No DeepLinkNow domain is on AdGuard or EasyPrivacy, so ad blockers don't stop the link page. - iOS clipboard handoff on every plan: the link page copies the link and the SDK reads it on first launch. - Any path on your link domain is already a link, with no prefix and nothing to create first. - There's no attribution, and the SDKs never read advertising IDs or need the ATT prompt. A deferred link can be claimed for 1 hour after the click, and stored fingerprints are deleted within 30 days. - Free AASA, assetlinks.json and ad-block checkers you can use without signing up. ## Pick Branch if… - You need attribution, ad network integrations and campaign analytics, and someone else's budget pays for it. - You want Journeys web banners and a marketing team running links without developers. If you just need links that open the right screen after install, don't pay for the rest. ## The pricing math Branch's Intro plan is $39 a month with 5,000 volume credits, 500 links or QR codes and 25,000 clicks. Past that it bills $0.01 a click, $0.05 a link and $0.05 a volume credit, with no cap. I bill on monthly active users instead. [1] [3] Say your links get 40,000 clicks a month. On Intro that's $39 plus 15,000 extra clicks at $0.01, so $189 before any volume-credit overage, and still no custom domain. On DeepLinkNow it's free while the app stays under 25,000 monthly users and 1,000 distinct paths, or $49 on Startup with your own domain. A custom domain on Branch means Activation Basics, $199 a month. ## Switching from Branch 1. Add the DLN SDK next to the one you have and ship it. 2. Point your link domain at DLN. Paths on it work straight away. 3. Read the link with checkDeferredDeepLink() on first launch. 4. Remove the Branch SDK in the next release. ## Questions ### How does Branch's pricing compare with DeepLinkNow's? Branch's Intro plan is $39 a month with 5,000 volume credits, 500 links or QR codes and 25,000 clicks. Past that it bills $0.01 a click, $0.05 a link and $0.05 a volume credit, with no cap. I bill on monthly active users instead. Say your links get 40,000 clicks a month. On Intro that's $39 plus 15,000 extra clicks at $0.01, so $189 before any volume-credit overage, and still no custom domain. On DeepLinkNow it's free while the app stays under 25,000 monthly users and 1,000 distinct paths, or $49 on Startup with your own domain. A custom domain on Branch means Activation Basics, $199 a month. ### When is Branch the better choice? You need attribution, ad network integrations and campaign analytics, and someone else's budget pays for it. You want Journeys web banners and a marketing team running links without developers. ### Can I run DeepLinkNow next to Branch? Yes. The SDK doesn't depend on any other deep linking SDK, so you can ship both, move your link domain over, and remove the old one later. ## Sources 1. [Branch: Intro plan](https://help.branch.io/docs/branch-intro-plan) 2. [Branch: trial](https://help.branch.io/docs/free-trial) 3. [Branch: overages and billing](https://help.branch.io/docs/overages-and-billing) 4. [Branch pricing](https://www.branch.io/pricing/) 5. [Branch: NativeLink](https://help.branch.io/docs/nativelink-deferred-deep-linking) 6. [Branch: creating a deep link](https://help.branch.io/docs/creating-a-deep-link) 7. [Branch: routing](https://help.branch.io/using-branch/docs/routing) 8. [Branch: consumer protection preferences](https://help.branch.io/developer-hub/docs/consumer-protection-preferences) 9. [EasyPrivacy filter list](https://easylist.to/easylist/easyprivacy.txt) 10. [AdGuard CNAME trackers: Branch](https://github.com/AdguardTeam/cname-trackers/blob/master/data/clickthroughs/branch_io.txt) 11. [Vendr: Branch](https://vendr.com/marketplace/branch) DeepLinkNow is not affiliated with the vendors compared here. --- Source: https://deeplinknow.com/compare/appsflyer # DeepLinkNow vs AppsFlyer Mobile measurement platform (MMP). As of October 2026, with a public source for every claim. AppsFlyer is the biggest attribution platform there is, and if you spend serious money on paid installs it's very good at its job. Deep linking is one feature inside that, and it's no longer on the free plan, so deferred links mean a sales call and an attribution contract. | | DeepLinkNow | AppsFlyer | Stronger | Sources | |---|---|---|---|---| | Free plan | Free for up to 25,000 monthly active users, deferred deep links included, and you don't need a card to start | Zero plan: links and QR codes, but no deferred deep linking since its 2026 update | DeepLinkNow | [1] [2] | | Sales call | Only for Enterprise (from $500/mo). Developer, Startup, Growth and Scale are self-serve. | Every paid tier is "Request a demo" | DeepLinkNow | [1] | | Plan with a custom domain | $49/mo (Startup) | Only through sales. Branded domains start on the Advanced tier and deferred deep links on Deep Linking Standard, both at custom prices. | DeepLinkNow | [1] | | Making links | Any path on your domain is already a link, with nothing to create first. | Short links are created through the OneLink API (each gets an ID of up to 50 characters) or the dashboard. API links don't show up in the dashboard list. | DeepLinkNow | [5] | | Ad-blocker lists | Not on any of them | 54 rules on AdGuard Tracking Protection, including onelink.me and the OneLink Smart Script | DeepLinkNow | [3] | | Advertising IDs | The SDKs never read them, so there's no ATT prompt | With ATT support on, the iOS SDK collects the IDFA on iOS 14.5 and later | DeepLinkNow | [6] | | Typical contract | Published on the site: $0, $49, $149 or $499 a month, paid monthly or yearly, and Enterprise from $500 a month | Median about $88.5K a year (Vendr, 130 deals) | DeepLinkNow | [4] | ## Where DeepLinkNow is ahead - A free plan for up to 25,000 monthly active users, deferred deep links included, and you don't need a card to start. - Published prices: $0, $49, $149 or $499 a month. You only talk to me if you want Enterprise. - Your own domain from $49 a month on Startup, set up in the dashboard. - No DeepLinkNow domain is on AdGuard or EasyPrivacy, so ad blockers don't stop the link page. - Any path on your link domain is already a link, with no prefix and nothing to create first. - There's no attribution, and the SDKs never read advertising IDs or need the ATT prompt. A deferred link can be claimed for 1 hour after the click, and stored fingerprints are deleted within 30 days. - iOS clipboard handoff on every plan: the link page copies the link and the SDK reads it on first launch. ## Pick AppsFlyer if… - You spend real money on paid user acquisition and need attribution, fraud protection and SKAdNetwork reporting. If you just need links that open the right screen after install, don't pay for the rest. ## The pricing math AppsFlyer bills on conversions: its last published rate was $0.07 for every install or action its links drove, after a first-year welcome package of 12,000. Deferred deep links now sit in the Deep Linking Suite at a custom price on top, and buyers on Vendr report a median contract of about $88.5K a year. [7] [1] [4] Say your links bring in 10,000 installs a month. At $0.07 a conversion that's $700 a month to AppsFlyer before the Deep Linking Suite, which is extra and priced by sales. On DeepLinkNow the same app is $49 a month on Startup for up to 100,000 monthly users with your own domain, and installs aren't charged at all. ## Switching from AppsFlyer 1. Add the DLN SDK next to the one you have and ship it. 2. Point your link domain at DLN. Paths on it work straight away. 3. Read the link with checkDeferredDeepLink() on first launch. 4. Keep AppsFlyer for attribution if you still need it. The DLN SDK runs next to it. ## Questions ### How does AppsFlyer's pricing compare with DeepLinkNow's? AppsFlyer bills on conversions: its last published rate was $0.07 for every install or action its links drove, after a first-year welcome package of 12,000. Deferred deep links now sit in the Deep Linking Suite at a custom price on top, and buyers on Vendr report a median contract of about $88.5K a year. Say your links bring in 10,000 installs a month. At $0.07 a conversion that's $700 a month to AppsFlyer before the Deep Linking Suite, which is extra and priced by sales. On DeepLinkNow the same app is $49 a month on Startup for up to 100,000 monthly users with your own domain, and installs aren't charged at all. ### When is AppsFlyer the better choice? You spend real money on paid user acquisition and need attribution, fraud protection and SKAdNetwork reporting. ### Can I run DeepLinkNow next to AppsFlyer? Yes. The SDK doesn't depend on any other deep linking SDK, so you can ship both, move your link domain over, and remove the old one later. ## Sources 1. [AppsFlyer deep linking pricing](https://www.appsflyer.com/pricing/deep-linking/) 2. [AppsFlyer Zero plan update](https://www.appsflyer.com/zero-update-2026/) 3. [AdGuard Tracking Protection filter](https://filters.adtidy.org/extension/ublock/filters/3.txt) 4. [Vendr: AppsFlyer](https://www.vendr.com/marketplace/appsflyer) 5. [AppsFlyer OneLink API](https://dev.appsflyer.com/hc/reference/onelinkapi_v2_overview) 6. [AppsFlyer iOS SDK integration](https://dev.appsflyer.com/hc/docs/integrate-ios-sdk) 7. [AppsFlyer plans and pricing (2025)](https://www.appsflyer.com/pricing-2025/) DeepLinkNow is not affiliated with the vendors compared here. --- Source: https://deeplinknow.com/compare/adjust # DeepLinkNow vs Adjust Mobile measurement platform (MMP). As of October 2026, with a public source for every claim. Adjust is built for teams buying ads, and deep linking (TrueLink) lives inside its attribution product. You create links in Campaign Lab, the SDK collects advertising IDs, and Adjust's own docs say deferred links may be limited for anyone who opts out of tracking. If all you want is the right screen after an install, that's a lot of machinery, plus a sales contract, to get it. | | DeepLinkNow | Adjust | Stronger | Sources | |---|---|---|---|---| | Deferred deep linking | On every plan, including the free one | In TrueLink Base. The deep link generator API and the faster ODDL path need TrueLink Core or Enterprise. | DeepLinkNow | [1] [9] | | Published prices | Published on the site: $0, $49, $149 or $499 a month, paid monthly or yearly, and Enterprise from $500 a month | Through sales only. Buyers on Vendr report a median of $44,000 a year (31 deals), ranging from $10,800 to $370,080. | DeepLinkNow | [8] | | After an ATT opt-out | Nothing changes, because no advertising IDs are involved. | "Deferred deep linking may be limited for users that opt-out." | DeepLinkNow | [2] | | iOS clipboard handoff | On every plan, including the free one | LinkMe: iOS 15 and up, SDK 4.31.0, and only if the user is in Safari | DeepLinkNow | [3] | | Making links | Any path on your domain is already a link, with nothing to create first. | Created in Campaign Lab or through the API, as short links on yourbrand.go.link/ | DeepLinkNow | [4] [5] | | Plan with a custom domain | $49/mo (Startup) | Not offered: you get a branded name in front of .go.link, which can't be changed after you save it. | DeepLinkNow | [5] | | Advertising IDs | The SDKs never read them, so there's no ATT prompt | The Android SDK adds the AD_ID permission by default. The iOS SDK's privacy manifest declares tracking, with eight tracking domains. | DeepLinkNow | [6] [7] | | Ad-blocker lists | Not on any of them | adj.st, the legacy link domain, is on AdGuard Tracking Protection, and app.adjust.com is on EasyPrivacy | DeepLinkNow | [10] [11] | ## Where DeepLinkNow is ahead - Published prices: $0, $49, $149 or $499 a month. You only talk to me if you want Enterprise. - There's no attribution, and the SDKs never read advertising IDs or need the ATT prompt. A deferred link can be claimed for 1 hour after the click, and stored fingerprints are deleted within 30 days. - iOS clipboard handoff on every plan: the link page copies the link and the SDK reads it on first launch. - Any path on your link domain is already a link, with no prefix and nothing to create first. - No DeepLinkNow domain is on AdGuard or EasyPrivacy, so ad blockers don't stop the link page. - Your own domain from $49 a month on Startup, set up in the dashboard. - Hosted in Sydney (Vercel syd1, database in AWS ap-southeast-2), if that matters to your users or your lawyers. ## Pick Adjust if… - You buy ads and need attribution, including SKAdNetwork when users opt out of tracking. - You ship games or less common platforms: Adjust has SDKs for Unity, Cocos2d-x, Steamworks, Windows and more. Sources: [2] [12] If you just need links that open the right screen after install, don't pay for the rest. ## The pricing math Adjust bills on attributions and sells its paid plans through sales. Buyers on Vendr report a median contract of $44,000 a year. [8] Vendr's median Adjust contract is $44,000 a year, about $3,670 a month, and the smallest it lists is $10,800 a year, about $900 a month. My Growth plan is $149 a month for up to 500,000 monthly active users. If what you need from Adjust is links that open the right screen, that's all you'd pay. ## Switching from Adjust 1. Add the DLN SDK next to the one you have and ship it. 2. Point your link domain at DLN. Paths on it work straight away. 3. Read the link with checkDeferredDeepLink() on first launch. 4. Keep Adjust for attribution if you still need it. The DLN SDK runs next to it. ## Questions ### How does Adjust's pricing compare with DeepLinkNow's? Adjust bills on attributions and sells its paid plans through sales. Buyers on Vendr report a median contract of $44,000 a year. Vendr's median Adjust contract is $44,000 a year, about $3,670 a month, and the smallest it lists is $10,800 a year, about $900 a month. My Growth plan is $149 a month for up to 500,000 monthly active users. If what you need from Adjust is links that open the right screen, that's all you'd pay. ### When is Adjust the better choice? You buy ads and need attribution, including SKAdNetwork when users opt out of tracking. You ship games or less common platforms: Adjust has SDKs for Unity, Cocos2d-x, Steamworks, Windows and more. ### Can I run DeepLinkNow next to Adjust? Yes. The SDK doesn't depend on any other deep linking SDK, so you can ship both, move your link domain over, and remove the old one later. ## Sources 1. [Adjust: TrueLink](https://help.adjust.com/en/article/truelink-adjusts-deep-linking-solution) 2. [Adjust: App Tracking Transparency](https://help.adjust.com/en/article/app-tracking-transparency-att-framework) 3. [Adjust: LinkMe](https://help.adjust.com/en/article/linkme) 4. [Adjust: deep links](https://help.adjust.com/en/article/deep-links) 5. [Adjust: branded domain](https://help.adjust.com/en/article/set-up-branded-domain) 6. [Adjust iOS SDK privacy manifest](https://github.com/adjust/ios_sdk/blob/master/Adjust/PrivacyInfo.xcprivacy) 7. [Adjust Android SDK](https://dev.adjust.com/en/sdk/android/) 8. [Vendr: Adjust](https://www.vendr.com/marketplace/adjust) 9. [Adjust: optimized deferred deep linking](https://help.adjust.com/en/article/optimized-deferred-deep-linking-oddl) 10. [AdGuard Tracking Protection filter](https://filters.adtidy.org/extension/ublock/filters/3.txt) 11. [EasyPrivacy filter list](https://easylist.to/easylist/easyprivacy.txt) 12. [Adjust developer docs index](https://dev.adjust.com/llms.txt) DeepLinkNow is not affiliated with the vendors compared here. --- Source: https://deeplinknow.com/compare/singular # DeepLinkNow vs Singular Mobile measurement platform (MMP). As of October 2026, with a public source for every claim. Singular is a solid attribution platform, and to its credit the free plan includes deferred deep links. The catch is that every Singular deep link is also an attribution link: you make it on their link page and it carries their tracking. If you don't run ads, you'd be adopting an MMP just to get links. | | DeepLinkNow | Singular | Stronger | Sources | |---|---|---|---|---| | Free plan | Free for up to 25,000 monthly active users, deferred deep links included, and you don't need a card to start | Free: 15,000 paid conversions, deferred deep linking included, no card | Level | [1] [7] | | Paid plans | Published on the site: $0, $49, $149 or $499 a month, paid monthly or yearly, and Enterprise from $500 a month | Growth at $0.05 per conversion through "Let's Talk", and Enterprise by demo | DeepLinkNow | [1] | | Plan with a custom domain | $49/mo (Startup) | Only through sales, on Growth ($0.05 per conversion, through "Let's Talk") or Enterprise | DeepLinkNow | [1] [4] | | Making links | Any path on your domain is already a link, with nothing to create first. | Created on the Create Link page as sng.link//, with the target in _dl and _ddl. Every deep link is an attribution link. | DeepLinkNow | [2] [3] | | iOS clipboard handoff | The link page copies the link before the App Store opens and the SDK reads it on first launch, with a scored match as the backup | Needs your own landing page with Singular's Web SDK and a download button | DeepLinkNow | [5] | | Deferred click window | 1 hour: long enough for an install and first launch, short enough that an old click can't go to the wrong person | 6 hours by default, 24 on request through your Customer Success Manager | Level | [3] | | Advertising IDs | The SDKs never read them, so there's no ATT prompt | The iOS SDK's privacy manifest declares tracking, and it collects the IDFA when the user allows it | DeepLinkNow | [6] | | Ad-blocker lists | Not on any of them | singular.net on EasyPrivacy; its SDK and web SDK hosts on AdGuard Tracking Protection | DeepLinkNow | [8] [9] | ## Where DeepLinkNow is ahead - Any path on your link domain is already a link, with no prefix and nothing to create first. - There's no attribution, and the SDKs never read advertising IDs or need the ATT prompt. A deferred link can be claimed for 1 hour after the click, and stored fingerprints are deleted within 30 days. - Your own domain from $49 a month on Startup, set up in the dashboard. - iOS clipboard handoff on every plan: the link page copies the link and the SDK reads it on first launch. - No DeepLinkNow domain is on AdGuard or EasyPrivacy, so ad blockers don't stop the link page. - Every fingerprint match comes back with a 0 to 100 score and a HIGH, MEDIUM or LOW label, so you decide what to trust. - Hosted in Sydney (Vercel syd1, database in AWS ap-southeast-2), if that matters to your users or your lawyers. ## Pick Singular if… - You run paid user acquisition and want attribution, cost aggregation (Singular lists 1,200+ connectors), SKAdNetwork and fraud prevention. Sources: [1] [10] If you just need links that open the right screen after install, don't pay for the rest. ## The pricing math Singular bills on paid conversions: 15,000 free, then $0.05 each on Growth. I bill on monthly active users and don't count conversions at all. [1] A custom domain on Singular means Growth, which starts with a sales conversation. If you run ads and get 30,000 paid conversions a month, that's up to $1,500 a month on Growth at $0.05 each. An app with 100,000 monthly users is $49 a month on my Startup plan, however many conversions it gets. ## Switching from Singular 1. Add the DLN SDK next to the one you have and ship it. 2. Point your link domain at DLN. Paths on it work straight away. 3. Read the link with checkDeferredDeepLink() on first launch. 4. Keep Singular for attribution if you still need it. The DLN SDK runs next to it. ## Questions ### How does Singular's pricing compare with DeepLinkNow's? Singular bills on paid conversions: 15,000 free, then $0.05 each on Growth. I bill on monthly active users and don't count conversions at all. A custom domain on Singular means Growth, which starts with a sales conversation. If you run ads and get 30,000 paid conversions a month, that's up to $1,500 a month on Growth at $0.05 each. An app with 100,000 monthly users is $49 a month on my Startup plan, however many conversions it gets. ### When is Singular the better choice? You run paid user acquisition and want attribution, cost aggregation (Singular lists 1,200+ connectors), SKAdNetwork and fraud prevention. ### Can I run DeepLinkNow next to Singular? Yes. The SDK doesn't depend on any other deep linking SDK, so you can ship both, move your link domain over, and remove the old one later. ## Sources 1. [Singular pricing](https://www.singular.net/pricing/) 2. [Singular: deep linking](https://www.singular.net/deep-linking/) 3. [Singular Links FAQ](https://support.singular.net/hc/en-us/articles/360030934212) 4. [Singular: branded link domain](https://support.singular.net/hc/en-us/articles/36288141843739) 5. [Singular: clipboard-based deferred deep linking](https://support.singular.net/hc/en-us/articles/5626161387419) 6. [Singular iOS SDK privacy manifest](https://github.com/singular-labs/Singular-iOS-SDK/blob/main/Sources/Singular/PrivacyInfo.xcprivacy) 7. [Singular: Firebase Dynamic Links post](https://www.singular.net/blog/firebase-dynamic-links-deep-links/) 8. [AdGuard Tracking Protection filter](https://filters.adtidy.org/extension/ublock/filters/3.txt) 9. [EasyPrivacy filter list](https://easylist.to/easylist/easyprivacy.txt) 10. [Singular](https://www.singular.net/) DeepLinkNow is not affiliated with the vendors compared here. --- Source: https://deeplinknow.com/compare/kochava # DeepLinkNow vs Kochava Mobile measurement platform (MMP). As of October 2026, with a public source for every claim. Kochava's free plan includes deferred deep links, which is generous for an attribution platform. What it leaves out is Universal Links, App Links and your own domain. Those start on Foundation at $500 a month, and the domain gets set up by your Client Success Manager. | | DeepLinkNow | Kochava | Stronger | Sources | |---|---|---|---|---| | Free plan | Free for up to 25,000 monthly active users, deferred deep links included, and you don't need a card to start | Free App Analytics: 10,000 attributed conversions a month, with deferred deep linking and SmartLinks, but no Universal Links, App Links or custom domain | DeepLinkNow | [1] [2] | | Paid plans | Published on the site: $0, $49, $149 or $499 a month, paid monthly or yearly, and Enterprise from $500 a month | Foundation from $500/mo (33,000 conversions) and Enterprise from $2,000/mo, both starting with a trial request form | DeepLinkNow | [1] | | Plan with a custom domain | $49/mo (Startup) | From $500/mo (Foundation): a your-brand.smart.link subdomain, set up through your Client Success Manager | DeepLinkNow | [1] [6] | | Making links | Any path on your domain is already a link, with nothing to create first. | Create a SmartLink first, then pass the destination in a parameter you configure, such as ?deeplink= | DeepLinkNow | [3] | | Deferred matching | The link page copies the link before the App Store opens and the SDK reads it on first launch, with a scored match as the backup | IP address and user agent by default. Kochava's test guide tells testers to turn off iCloud Private Relay. | DeepLinkNow | [4] | | Match lookback | 1 hour: long enough for an install and first launch, short enough that an old click can't go to the wrong person | 7 days for modeled (IP and user agent) matches, by default | Level | [5] | | Advertising IDs | The SDKs never read them, so there's no ATT prompt | IDFA after the ATT prompt (the SDK can show it) and the Google advertising ID, through optional modules | DeepLinkNow | [7] | | Ad-blocker lists | Not on any of them | kochava.com/track/ on EasyPrivacy, smart.link/imp/ on AdGuard Tracking Protection | DeepLinkNow | [8] [9] | ## Where DeepLinkNow is ahead - Your own domain from $49 a month on Startup, set up in the dashboard. - There's no attribution, and the SDKs never read advertising IDs or need the ATT prompt. A deferred link can be claimed for 1 hour after the click, and stored fingerprints are deleted within 30 days. - Any path on your link domain is already a link, with no prefix and nothing to create first. - iOS clipboard handoff on every plan: the link page copies the link and the SDK reads it on first launch. - Every fingerprint match comes back with a 0 to 100 score and a HIGH, MEDIUM or LOW label, so you decide what to trust. - No DeepLinkNow domain is on AdGuard or EasyPrivacy, so ad blockers don't stop the link page. - Hosted in Sydney (Vercel syd1, database in AWS ap-southeast-2), if that matters to your users or your lawyers. ## Pick Kochava if… - You run paid campaigns and want attribution and fraud prevention. - You ship to TVs and consoles as well as phones: Kochava has SDKs and SmartLinks for connected TV. Sources: [1] If you just need links that open the right screen after install, don't pay for the rest. ## The pricing math Kochava bills on attributed conversions: installs or re-engagements credited to a media source. 10,000 a month are free, Foundation includes 33,000 from $500 a month. [1] [2] An app with 50,000 monthly users that wants Universal Links, App Links and its own link domain: on Kochava that's Foundation, from $500 a month, with the domain set up through your Client Success Manager. On DeepLinkNow the same app is $49 on Startup, own domain included, and Universal Links and App Links work on every plan, free one too. ## Switching from Kochava 1. Add the DLN SDK next to the one you have and ship it. 2. Point your link domain at DLN. Paths on it work straight away. 3. Read the link with checkDeferredDeepLink() on first launch. 4. Keep Kochava for attribution if you still need it. The DLN SDK runs next to it. ## Questions ### How does Kochava's pricing compare with DeepLinkNow's? Kochava bills on attributed conversions: installs or re-engagements credited to a media source. 10,000 a month are free, Foundation includes 33,000 from $500 a month. An app with 50,000 monthly users that wants Universal Links, App Links and its own link domain: on Kochava that's Foundation, from $500 a month, with the domain set up through your Client Success Manager. On DeepLinkNow the same app is $49 on Startup, own domain included, and Universal Links and App Links work on every plan, free one too. ### When is Kochava the better choice? You run paid campaigns and want attribution and fraud prevention. You ship to TVs and consoles as well as phones: Kochava has SDKs and SmartLinks for connected TV. ### Can I run DeepLinkNow next to Kochava? Yes. The SDK doesn't depend on any other deep linking SDK, so you can ship both, move your link domain over, and remove the old one later. ## Sources 1. [Kochava pricing](https://www.kochava.com/get-started/) 2. [Kochava Free App Analytics](https://www.kochava.com/product/free-app-analytics/) 3. [Kochava: dynamic deep linking](https://support.kochava.com/articles/links-overview/42172-dynamic-deep-linking/) 4. [Kochava: testing a deferred deeplink](https://support.kochava.com/articles/reference-information/48738-testing-a-deferred-deeplink/) 5. [Kochava: attribution overview](https://support.kochava.com/articles/reference-information/223-attribution-overview/) 6. [Kochava: Universal Link and App Link support](https://support.kochava.com/articles/links-overview/16919-adding-universal-link-or-app-link-support/) 7. [Kochava Apple SDK](https://support.kochava.com/articles/apple-sdk/) 8. [EasyPrivacy filter list](https://easylist.to/easylist/easyprivacy.txt) 9. [AdGuard Tracking Protection filter](https://filters.adtidy.org/extension/ublock/filters/3.txt) DeepLinkNow is not affiliated with the vendors compared here. --- Source: https://deeplinknow.com/compare/airbridge # DeepLinkNow vs Airbridge Mobile measurement platform (MMP). As of October 2026, with a public source for every claim. Airbridge is a full measurement platform with deep linking built in, and it's priced fairly for one. It still works like an MMP though: you create links up front, the SDK reads advertising IDs, and on iOS your app can be left waiting on the tracking prompt before it gets the deferred link. | | DeepLinkNow | Airbridge | Stronger | Sources | |---|---|---|---|---| | Free plan | Free for up to 25,000 monthly active users, deferred deep links included, and you don't need a card to start | There isn't one. Core starts at $40/mo after a 30-day trial. | DeepLinkNow | [1] [2] | | What you pay for | Monthly active users, one flat price per plan | Data points: 500K a month on Core, then $0.0001 each | Level | [2] | | Making links | Any path on your domain is already a link, with nothing to create first. | Created first, in the dashboard, Google Sheets or the API. Custom domains must be a dedicated subdomain. | DeepLinkNow | [3] | | Deferred link on iOS | Handed over on first launch, with no prompt to wait for. | The SDK waits for the ATT answer (30 seconds by default) before delivering it | DeepLinkNow | [4] | | Advertising IDs | The SDKs never read them, so there's no ATT prompt | IDFA and GAID collected by default | DeepLinkNow | [5] | | Ad-blocker lists | Not on any of them | SDK, attribution and event domains, plus a deferred-deeplink rule, on AdGuard's tracking list | DeepLinkNow | [6] | ## Where DeepLinkNow is ahead - A free plan for up to 25,000 monthly active users, deferred deep links included, and you don't need a card to start. - There's no attribution, and the SDKs never read advertising IDs or need the ATT prompt. A deferred link can be claimed for 1 hour after the click, and stored fingerprints are deleted within 30 days. - Any path on your link domain is already a link, with no prefix and nothing to create first. - No DeepLinkNow domain is on AdGuard or EasyPrivacy, so ad blockers don't stop the link page. - iOS clipboard handoff on every plan: the link page copies the link and the SDK reads it on first launch. - Every fingerprint match comes back with a 0 to 100 score and a HIGH, MEDIUM or LOW label, so you decide what to trust. ## Pick Airbridge if… - You want a full MMP, with ad network integrations and attribution. If you just need links that open the right screen after install, don't pay for the rest. ## The pricing math Airbridge's Core plan starts at $40 a month for 500,000 data points, then $0.0001 per data point. Its pricing calculator counts about 14.4 data points per monthly user at its default setting: 8,000 users is 115,200 data points and 50,000 users is 720,000. [1] [2] At 100,000 monthly users, Airbridge's calculator works out to about 1.44 million data points: $40 plus 940,000 extra at $0.0001, so about $134 a month, and more as you track more events. On DeepLinkNow, Startup is a flat $49 a month for up to 100,000 monthly users with your own domain, and below 25,000 monthly users it's free. ## Switching from Airbridge 1. Add the DLN SDK next to the one you have and ship it. 2. Point your link domain at DLN. Paths on it work straight away. 3. Read the link with checkDeferredDeepLink() on first launch. 4. Remove the Airbridge SDK, and its ATT prompt if deep linking was its only job. ## Questions ### How does Airbridge's pricing compare with DeepLinkNow's? Airbridge's Core plan starts at $40 a month for 500,000 data points, then $0.0001 per data point. Its pricing calculator counts about 14.4 data points per monthly user at its default setting: 8,000 users is 115,200 data points and 50,000 users is 720,000. At 100,000 monthly users, Airbridge's calculator works out to about 1.44 million data points: $40 plus 940,000 extra at $0.0001, so about $134 a month, and more as you track more events. On DeepLinkNow, Startup is a flat $49 a month for up to 100,000 monthly users with your own domain, and below 25,000 monthly users it's free. ### When is Airbridge the better choice? You want a full MMP, with ad network integrations and attribution. ### Can I run DeepLinkNow next to Airbridge? Yes. The SDK doesn't depend on any other deep linking SDK, so you can ship both, move your link domain over, and remove the old one later. ## Sources 1. [Airbridge pricing](https://www.airbridge.io/en/pricing) 2. [Airbridge Core plan](https://airbridge.io/en/plans/core) 3. [Airbridge: tracking links](https://help.airbridge.io/en/deeplink-guides/understanding-tracking-link.md) 4. [Airbridge React Native SDK](https://help.airbridge.io/en/deeplink-developers/react-native-sdk-v4.md) 5. [Airbridge: identifiers](https://help.airbridge.io/en/guides/identifiers.md) 6. [AdGuard Tracking Protection filter](https://filters.adtidy.org/extension/ublock/filters/3.txt) DeepLinkNow is not affiliated with the vendors compared here. --- Source: https://deeplinknow.com/compare/detour # DeepLinkNow vs Detour Deferred deep linking for React Native and Expo. As of October 2026, with a public source for every claim. Detour is the closest thing to what I built: deferred deep links for React Native and Expo, without an attribution suite. What I like about it: the Expo Router support is solid, it has a nice pattern for holding a link until the user gets past your sign-in screen, and it's made by Software Mansion, who know React Native as well as anyone. Where we differ is the free plan, what your own domain costs, URLs without an app hash in them, and how much your app gets told about each match. | | DeepLinkNow | Detour | Stronger | Sources | |---|---|---|---|---| | Free plan | Free for up to 25,000 monthly active users, deferred deep links included, and you don't need a card to start | 1,500 clicks a month and 50 short links per app. Past the limit, deep link matching stops and plain redirects keep working. | DeepLinkNow | [1] | | What you pay for | Monthly active users (devices that open your app with the SDK). Each distinct link path counts once toward the link limit (1,000 on the free plan). | Link clicks: Starter $19/mo for 70,000, Scale $79/mo for 350,000, then optional pay-as-you-go per 1,000 clicks | Level | [1] | | Plan with a custom domain | $49/mo (Startup) | $79/mo (Scale). Free and Starter links live on a .godetour.link subdomain. | DeepLinkNow | [1] [4] | | Making links | Any path on your domain is already a link, with nothing to create first. | Any path and query after the app's fixed // segment, which stays in the URL on a custom domain too | DeepLinkNow | [2] [4] | | What the app gets | A confidence score from 0 to 100 with a HIGH, MEDIUM or LOW label, and you pick the threshold | A link, or nothing. You set one threshold in the dashboard (850 by default, 700 to 1200), and the app never sees the score, so it can't treat a near miss differently. | DeepLinkNow | [3] [4] | | Match window | 1 hour: long enough for an install and first launch, short enough that an old click can't go to the wrong person | 15 minutes by default, configurable from 5 to 180 | Level | [3] | | Expo Router | A +native-intent helper (@deeplinknow/react-native/expo-router) and a useDeferredDeepLink() hook | A +native-intent handler and a DetourProvider, with examples that keep the link through sign-in | Level | [4] [6] | | How long click data is kept | A deferred link can be claimed for 1 hour after the click, and stored fingerprints are deleted within 30 days. | The privacy policy says data is kept "as long as needed to provide" the service and gives no period | DeepLinkNow | [7] | | Instagram and TikTok | On iOS the link page gets out of Instagram, Threads and Facebook to the App Store, and walks TikTok users through opening it in their browser | Hands off out of Instagram, Threads and Facebook on iOS, with a browser prompt for TikTok | Level | [4] | ## Where DeepLinkNow is ahead - A free plan for up to 25,000 monthly active users, deferred deep links included, and you don't need a card to start. - Your own domain from $49 a month on Startup, set up in the dashboard. - Every fingerprint match comes back with a 0 to 100 score and a HIGH, MEDIUM or LOW label, so you decide what to trust. - Any path on your link domain is already a link, with no prefix and nothing to create first. - Free AASA, assetlinks.json and ad-block checkers you can use without signing up. - Hosted in Sydney (Vercel syd1, database in AWS ap-southeast-2), if that matters to your users or your lawyers. ## Pick Detour if… - You'd rather depend on Software Mansion, a well-known React Native company with a big open-source track record. - You want a smart banner for your website, or analytics in the same tool as your links, like install counts and scheduled exports to your own server. I keep DeepLinkNow out of analytics and hand your app the link to send wherever you already measure things. Sources: [5] [4] If you want a free plan sized for a real launch and a score you can set your own threshold on, try mine first. ## The pricing math Detour bills on link clicks: 1,500 free, 70,000 for $19, 350,000 for $79. I bill on monthly active users, the devices that open your app with the SDK. The two don't convert one to one, so it depends on how many clicks each user makes. [1] Say 15,000 people use your app each month and your links get 40,000 clicks across a few hundred paths. Detour: Starter, $19. DeepLinkNow: free. Now put the links on your own domain: Detour needs Scale, $79 a month, and the // segment stays in every URL. On DeepLinkNow it's $49 on Startup for up to 100,000 monthly users, however many clicks they make. ## Switching from Detour 1. Add the DLN SDK next to Detour's and ship it. 2. Point your link domain at DLN. Paths on it work without the // segment. 3. Swap createDetourNativeIntentHandler for createRedirectSystemPath from @deeplinknow/react-native/expo-router, and DetourProvider's link for useDeferredDeepLink(). 4. Keep Detour until the links you've already shared stop getting clicks. Its custom-domain links stop resolving when the subscription ends. ## Questions ### How does Detour's pricing compare with DeepLinkNow's? Detour bills on link clicks: 1,500 free, 70,000 for $19, 350,000 for $79. I bill on monthly active users, the devices that open your app with the SDK. The two don't convert one to one, so it depends on how many clicks each user makes. Say 15,000 people use your app each month and your links get 40,000 clicks across a few hundred paths. Detour: Starter, $19. DeepLinkNow: free. Now put the links on your own domain: Detour needs Scale, $79 a month, and the // segment stays in every URL. On DeepLinkNow it's $49 on Startup for up to 100,000 monthly users, however many clicks they make. ### When is Detour the better choice? You'd rather depend on Software Mansion, a well-known React Native company with a big open-source track record. You want a smart banner for your website, or analytics in the same tool as your links, like install counts and scheduled exports to your own server. I keep DeepLinkNow out of analytics and hand your app the link to send wherever you already measure things. ### Can I run DeepLinkNow next to Detour? Yes. The SDK doesn't depend on any other deep linking SDK, so you can ship both, move your link domain over, and remove the old one later. ## Sources 1. [Detour: billing](https://detour.swmansion.com/docs/platform/fundamentals/billing) 2. [Detour: click handling](https://detour.swmansion.com/docs/platform/architecture/click-handling) 3. [Detour: matching](https://detour.swmansion.com/docs/platform/architecture/matching) 4. [Detour docs (full text)](https://detour.swmansion.com/docs/llms-full.txt) 5. [Detour React Native SDK](https://github.com/software-mansion-labs/react-native-detour) 6. [Expo blog: deferred deep linking the right way](https://expo.dev/blog/deferred-deep-linking-the-right-way) 7. [Detour privacy policy](https://godetour.dev/privacy-policy) DeepLinkNow is not affiliated with the vendors compared here. --- Source: https://deeplinknow.com/compare/linktrace # DeepLinkNow vs LinkTrace Install attribution and referral links over REST. As of October 2026, with a public source for every claim. LinkTrace is a REST API for install attribution and referral links, so there's no SDK to add, which is appealing if you're SDK-averse. The trade-off is that it matches on fingerprints alone, and Universal Links and App Links only come on its top plan, on a linktrace.in subdomain. | | DeepLinkNow | LinkTrace | Stronger | Sources | |---|---|---|---|---| | Free plan | Free for up to 25,000 monthly active users, deferred deep links included, and you don't need a card to start | 100 links and 1,000 attributed installs, once (the paid plans reset monthly) | DeepLinkNow | [1] | | Paid plans | Published on the site: $0, $49, $149 or $499 a month, paid monthly or yearly, and Enterprise from $500 a month | Growth $9.99/mo (1,000 links, 10,000 installs), Scale $29.99/mo (unlimited) | Level | [1] | | Plan with a custom domain | $49/mo (Startup) | There isn't one. Its top plan, Scale ($29.99/mo), puts Universal Links on a yourbrand.linktrace.in subdomain. | DeepLinkNow | [1] [2] | | Universal Links and App Links | Every plan, on yourapp.deeplinknow.com or your own domain | Scale only, on a yourbrand.linktrace.in subdomain | DeepLinkNow | [1] [2] | | Deferred matching | The link page copies the link before the App Store opens and the SDK reads it on first launch, with a scored match as the backup | Fingerprints only, for 24 hours, with no install referrer, and the result is a plain attributed true or false | DeepLinkNow | [2] | | SDK | iOS, Android, React Native (with Expo) and a Flutter SDK (pub.dev `deeplinknow`) | There's no SDK, just a REST API with copy-paste examples | DeepLinkNow | [3] | | Advertising IDs | The SDKs never read them, so there's no ATT prompt | Not used | Level | [2] | ## Where DeepLinkNow is ahead - iOS clipboard handoff on every plan: the link page copies the link and the SDK reads it on first launch. - Native iOS (Swift) and Android (Kotlin) SDKs, a Flutter SDK (pub.dev `deeplinknow`), a React Native SDK with an Expo config plugin, an Expo Router helper and a useDeferredDeepLink() hook, and an MCP server so coding agents can set it up. - A free plan for up to 25,000 monthly active users, deferred deep links included, and you don't need a card to start. - Every fingerprint match comes back with a 0 to 100 score and a HIGH, MEDIUM or LOW label, so you decide what to trust. - Any path on your link domain is already a link, with no prefix and nothing to create first. - Hosted in Sydney (Vercel syd1, database in AWS ap-southeast-2), if that matters to your users or your lawyers. ## Pick LinkTrace if… - You don't want an SDK in the app, and fingerprint matching alone is enough. - You want referral tracking with referrer IDs over a REST API. Sources: [3] [1] [2] If you want the install referrer on Android and the clipboard on iOS, which are exact when they work, try mine. ## The pricing math LinkTrace bills flat per plan on links and attributed installs: $9.99 for 10,000 installs a month, $29.99 unlimited. I bill on monthly active users. [1] LinkTrace has no plan with your own domain: Universal Links and App Links need Scale, and even there they run on yourbrand.linktrace.in. On DeepLinkNow, Universal Links work on yourapp.deeplinknow.com on every plan, and your own domain is $49 on Startup. An app with 20,000 monthly users is free, with no one-time cap on installs. ## Switching from LinkTrace 1. Add the DLN SDK next to the one you have and ship it. 2. Point your link domain at DLN. Paths on it work straight away. 3. Read the link with checkDeferredDeepLink() on first launch. 4. Remove the LinkTrace SDK once the links you've already shared stop getting clicks. ## Questions ### How does LinkTrace's pricing compare with DeepLinkNow's? LinkTrace bills flat per plan on links and attributed installs: $9.99 for 10,000 installs a month, $29.99 unlimited. I bill on monthly active users. LinkTrace has no plan with your own domain: Universal Links and App Links need Scale, and even there they run on yourbrand.linktrace.in. On DeepLinkNow, Universal Links work on yourapp.deeplinknow.com on every plan, and your own domain is $49 on Startup. An app with 20,000 monthly users is free, with no one-time cap on installs. ### When is LinkTrace the better choice? You don't want an SDK in the app, and fingerprint matching alone is enough. You want referral tracking with referrer IDs over a REST API. ### Can I run DeepLinkNow next to LinkTrace? Yes. The SDK doesn't depend on any other deep linking SDK, so you can ship both, move your link domain over, and remove the old one later. ## Sources 1. [LinkTrace pricing](https://linktrace.in/pricing/) 2. [LinkTrace llms.txt](https://linktrace.in/llms.txt) 3. [LinkTrace FAQ](https://linktrace.in/faq/) 4. [LinkTrace API reference](https://linktrace.in/documentation/api-reference/) DeepLinkNow is not affiliated with the vendors compared here. --- Source: https://deeplinknow.com/compare/appsonair # DeepLinkNow vs AppsOnAir App platform with an AppLink deep linking module. As of October 2026, with a public source for every claim. AppsOnAir sells CodePush, app distribution and push, and AppLink is its deep linking module. It has a real Firebase importer that reads Google Takeout exports. There's no AppLink on the free plan, and the custom domain is an add-on. | | DeepLinkNow | AppsOnAir | Stronger | Sources | |---|---|---|---|---| | Free plan | Free for up to 25,000 monthly active users, deferred deep links included, and you don't need a card to start | No AppLink on the free plan | DeepLinkNow | [1] | | Plan with a custom domain | $49/mo (Startup) | $54.97/mo for Pro with AppLink and the custom domain | DeepLinkNow | [1] | | Deferred window | 1 hour: long enough for an install and first launch, short enough that an old click can't go to the wrong person | 1 hour by default; longer windows depend on the plan | Level | [2] | | What the app gets | A confidence score from 0 to 100 with a HIGH, MEDIUM or LOW label, and you pick the threshold | Organic or non-organic, with no score | DeepLinkNow | [2] | | Firebase links | Imports your Google Takeout export, so old short links keep their targets, and FDL-style long links on your domain hand the app the link parameter | Imports Google Takeout CSV exports of your Firebase links | Level | [3] | ## Where DeepLinkNow is ahead - A free plan for up to 25,000 monthly active users, deferred deep links included, and you don't need a card to start. - Published prices: $0, $49, $149 or $499 a month. You only talk to me if you want Enterprise. - Your own domain from $49 a month on Startup, set up in the dashboard. - Every fingerprint match comes back with a 0 to 100 score and a HIGH, MEDIUM or LOW label, so you decide what to trust. - Any path on your link domain is already a link, with no prefix and nothing to create first. ## Pick AppsOnAir if… - You already use AppsOnAir for CodePush or push and want one vendor. Sources: [1] If deep links are all you need, you don't need the bundle around them. ## The pricing math AppsOnAir prices AppLink as a module on a base plan, and the custom domain is billed on top: Pro with AppLink (50,000 app links) and a custom domain comes to $54.97 a month. [1] One app with deep links on its own domain: $54.97 a month on AppsOnAir, and $49 on DeepLinkNow. Below 25,000 monthly users DeepLinkNow is free, deferred deep links included, while AppsOnAir has no AppLink on its free plan. ## Switching from AppsOnAir 1. Add the DLN SDK next to the one you have and ship it. 2. Point your link domain at DLN. Paths on it work straight away. 3. Read the link with checkDeferredDeepLink() on first launch. 4. Remove the AppsOnAir SDK once the links you've already shared stop getting clicks. ## Questions ### How does AppsOnAir's pricing compare with DeepLinkNow's? AppsOnAir prices AppLink as a module on a base plan, and the custom domain is billed on top: Pro with AppLink (50,000 app links) and a custom domain comes to $54.97 a month. One app with deep links on its own domain: $54.97 a month on AppsOnAir, and $49 on DeepLinkNow. Below 25,000 monthly users DeepLinkNow is free, deferred deep links included, while AppsOnAir has no AppLink on its free plan. ### When is AppsOnAir the better choice? You already use AppsOnAir for CodePush or push and want one vendor. ### Can I run DeepLinkNow next to AppsOnAir? Yes. The SDK doesn't depend on any other deep linking SDK, so you can ship both, move your link domain over, and remove the old one later. ## Sources 1. [AppsOnAir pricing](https://www.appsonair.com/plans-pricing) 2. [AppsOnAir AppLink overview](https://documentation.appsonair.com/MobileQuickstart/AppLink/overview.html) 3. [AppsOnAir AppLink FAQs](https://documentation.appsonair.com/MobileQuickstart/AppLink/applink-faqs.html) DeepLinkNow is not affiliated with the vendors compared here. --- Source: https://deeplinknow.com/compare/urlgenius # DeepLinkNow vs URLgenius SDK-free app linking. As of October 2026, with a public source for every claim. URLgenius works differently from everything else here. There's no SDK: it opens apps through URL schemes, and a lot of its customers link into other companies' apps, like Amazon listings, which it's good at. Deferred deep linking is only on its Custom plan. | | DeepLinkNow | URLgenius | Stronger | Sources | |---|---|---|---|---| | Free plan | Free for up to 25,000 monthly active users, deferred deep links included, and you don't need a card to start | Starter: the first 500 clicks free during a qualified trial, no deferred deep links | DeepLinkNow | [1] | | Deferred deep links | On every plan, including the free one | Custom plan only, from $199/mo for brands, through Contact us | DeepLinkNow | [1] | | What you pay for | Monthly active users (devices that open your app with the SDK). Each distinct link path counts once toward the link limit (1,000 on the free plan). | Clicks: $0.02 each on Growth, with discounts after 10,000 | DeepLinkNow | [1] | | Plan with a custom domain | $49/mo (Startup) | $49/mo and $0.02 a click (Growth), with no deferred deep links. Deferred links need Custom, from $199/mo. | DeepLinkNow | [1] | | How links open the app | Universal Links and App Links, with the AASA and assetlinks files hosted for you | URL schemes rather than Universal Links, so every screen you link to needs a URL scheme | DeepLinkNow | [2] | | API | Management API on Startup and up | Enterprise plans | DeepLinkNow | [2] | ## Where DeepLinkNow is ahead - A free plan for up to 25,000 monthly active users, deferred deep links included, and you don't need a card to start. - Published prices: $0, $49, $149 or $499 a month. You only talk to me if you want Enterprise. - Any path on your link domain is already a link, with no prefix and nothing to create first. - Native iOS (Swift) and Android (Kotlin) SDKs, a Flutter SDK (pub.dev `deeplinknow`), a React Native SDK with an Expo config plugin, an Expo Router helper and a useDeferredDeepLink() hook, and an MCP server so coding agents can set it up. - Free AASA, assetlinks.json and ad-block checkers you can use without signing up. ## Pick URLgenius if… - You link into other companies' apps (Amazon, Instagram, TikTok), which is most of URLgenius's business. - You can't ship an app update or add an SDK. - Your links are shared inside Instagram and Facebook and must get out of the in-app browser. Sources: [4] [2] If you're linking into your own app and want the context to survive an install, that's what I do. ## The pricing math URLgenius bills per click: $0.02 on Growth, with discounts after 10,000. Deferred deep linking is on the Custom plan from $199 a month. I bill on monthly active users. [1] 50,000 clicks a month on Growth with a branded domain is up to $1,049 a month, and still no deferred links. On DeepLinkNow that depends on users, not clicks: free up to 25,000 monthly users, then $49 on Startup with your own domain and deferred deep links. ## Switching from URLgenius 1. Add the DLN SDK next to the one you have and ship it. 2. Point your link domain at DLN. Paths on it work straight away. 3. Read the link with checkDeferredDeepLink() on first launch. 4. Keep URLgenius for links into other companies' apps. DeepLinkNow only opens your own. ## Questions ### How does URLgenius's pricing compare with DeepLinkNow's? URLgenius bills per click: $0.02 on Growth, with discounts after 10,000. Deferred deep linking is on the Custom plan from $199 a month. I bill on monthly active users. 50,000 clicks a month on Growth with a branded domain is up to $1,049 a month, and still no deferred links. On DeepLinkNow that depends on users, not clicks: free up to 25,000 monthly users, then $49 on Startup with your own domain and deferred deep links. ### When is URLgenius the better choice? You link into other companies' apps (Amazon, Instagram, TikTok), which is most of URLgenius's business. You can't ship an app update or add an SDK. Your links are shared inside Instagram and Facebook and must get out of the in-app browser. ### Can I run DeepLinkNow next to URLgenius? Yes. The SDK doesn't depend on any other deep linking SDK, so you can ship both, move your link domain over, and remove the old one later. ## Sources 1. [URLgenius pricing](https://app.urlgeni.us/pricing) 2. [URLgenius FAQ](https://app.urlgeni.us/faq) 3. [URLgenius: deferred deep linking](https://app.urlgeni.us/deferred-deep-linking) 4. [URLgenius: app linking](https://app.urlgeni.us/app-linking) DeepLinkNow is not affiliated with the vendors compared here.