Troubleshooting
When a link opens the browser or store instead of your app — domain verification, signing fingerprints, caches, consent, and checks that find the cause.
Golden rule: a link that opens the browser or store instead of your installed app is almost always domain verification (AASA / assetlinks.json) — usually a signing-fingerprint mismatch or a stale cache, not SDK code. Apple documents the association-file format under supporting associated domains; Google documents the Android side under verifying App Links.
Check the files before you read further. Our free Universal Links & App Links validator fetches the AASA and assetlinks.json for any domain and flags the failures that don't show up in a browser — a wrong Content-Type, a redirect in front of the file, a missing fingerprint. No signup, and it works on any domain, not just ours.
Enable SDK logs to see matching and routing decisions. iOS: LinkTrailOptions(logEnabled: true, logLevel: .debug) — lines print to the Xcode console prefixed [LinkTrail]. Android: LinkTrailOptions(logEnabled = true), then adb logcat -s LinkTrail. React Native and Flutter: pass logEnabled (and logLevel) in the options you give configure and read the same native lines. Every line reads [LinkTrail] [LEVEL] message on both platforms, with LEVEL DEBUG, INFO, WARNING or ERROR (WARNING, not WARN), and logged URLs carry no query or fragment. The ones to look for, verbatim, each prefixed [LinkTrail] [INFO]: Install held — awaiting consent decision (the gate is doing its job), Consent granted, Sending install event… (consent: true), then Install attributed (Install attributed (deterministic click token) when the token was recovered) or Organic install (no attribution match). A link the SDK did not claim logs [LinkTrail] [DEBUG] Not a LinkTrail link — ignoring: <url>. (With no API key configured the SDK never initializes, so the log filter shows nothing at all.)
Faster deferred re-testing: instead of uninstall/reinstall, call resetForTesting — LinkTrail.resetForTesting() on iOS, React Native and Flutter, LinkTrail.resetForTesting(context) on Android. It clears the install flag, cached attribution, device ID and the consent decision, so the next launch behaves like a genuine first launch and needs a fresh setConsent. Testing only — never ship it.
Testing locally
Re-test a first launch without reinstalling: resetForTesting clears all SDK state, including the consent decision, so record consent again before the install can go out. On iOS, trackInstall(force: true) re-sends an install that was already tracked. It does not open the consent gate, so with requireConsent on and no decision, even a forced install is held.
#if DEBUG
LinkTrail.resetForTesting() // clears all SDK state, consent included
LinkTrail.shared?.setConsent(true) // the reset cleared the decision
LinkTrail.shared?.trackInstall(force: true) // a fresh install event each launch
#endifAndroid: fake the Play Install Referrer. A build installed with adb install has no Play Install Referrer, so the deterministic deferred path can't run on an emulator. A debuggable app can read a stand-in referrer instead, and the SDK then parses it exactly like Play's and doesn't ask Play at all:
adb shell setprop debug.linktrail.referrer "lt_click=<32-hex token>" # before the app's first launch
adb shell setprop debug.linktrail.referrer "" # off again (Play is asked as usual)- It's read only for the install: on the first launch after an install or
adb shell pm clear <package>. - A manifest fallback works too, in your debug manifest only:
<meta-data android:name="LINKTRAIL_REFERRER" android:value="lt_click=…"/>. The system property wins over it. - A value without a valid
lt_click(32 lowercase hex) gives no token, as a real organic referrer would. - It logs a WARNING with the token redacted:
Using debug referrer override "lt_click=<redacted>" (debug build only; from system property debug.linktrail.referrer).
Release builds never read these overrides: the referrer override is honoured only when the host app is debuggable. The backend host isn't configurable either, so release builds always talk to https://backend.linktrail.io. Every log line is in the SDK log reference.
Address-bar navigation never opens the app
By design on both platforms. Test by tapping a link from another app, or: adb shell am start -a android.intent.action.VIEW -d "https://…".
Android has two caches
Google's Digital Asset Links crawl (~1 h) and the per-device verdict set at install time. Fix assetlinks → wait for Google → reinstall or adb shell pm verify-app-links --re-verify <package>.
Deferred link never arrives, or links route but nothing attributes
The consent gate is holding the install (default on every platform). Nothing is sent until you call setConsent(true) or setConsent(false); grant attributes, deny routes only. Re-engagement links route either way, which is why direct opens look fine. If your app has no consent prompt yet, pass requireConsent: false while integrating — see Consent & privacy. Otherwise: SDK not configured, bad API key, or it wasn't a genuine first launch. Re-test: uninstall → click link → reinstall → launch → answer the prompt.
A link opens the app but never navigates
The link's host isn't one the SDK claims. Since 0.1.0 handleDeepLink claims only https links on linktrail.io (or a subdomain) and on the hosts listed in linkDomains (or their subdomains); a custom domain that isn't listed is ignored with no request and no onError, and the debug log shows [LinkTrail] [DEBUG] Not a LinkTrail link — ignoring: <url>. Add every custom domain to linkDomains — clearing it no longer means "every host". A custom-scheme link (myapp://slug) is not claimed either; share the https link.
A restored or new phone acts like a fresh install
By design since 0.1.0. The SDK keeps its state (install flag, consent decision, device ID, queued events, a saved deferred link) on the device only and excludes it from backups, so an app restored from a backup or moved to a new phone counts as a new install and must ask for consent again. Replay the decision your app stored with setConsent after configure.
Deferred installs on iOS arrive unattributed
clickTokenSource defaults to .pasteButton, so the click token is read only when the user taps a paste control. Render LinkTrailPasteButton (or a UIPasteControl natively) and set autoTrackInstall: false, or switch to .automatic and accept the Allow Paste alert. Android is unaffected: it uses the Play Install Referrer.
An invalid API key does NOT fail configure
The backend rejects it asynchronously with 401 {"code":401,"message":"Invalid or missing API key"} and it surfaces via onError as invalidAPIKey (iOS), InvalidApiKey (Android), invalid_api_key (React Native) or LinkTrailInvalidApiKeyException (Flutter). Watch onError during integration.
Installs fail with 404
The backend answered {"code":404,"message":"No app found for this identifier in your workspace"}: the identifier the build sends is not registered in the API key's workspace. Usually a debug build with a suffixed bundle ID or applicationIdSuffix, or a key from a different workspace. Register that identifier as its own app. Every SDK error and HTTP status is on Errors.
Corporate networks and allow-lists
The SDKs call https://backend.linktrail.io only — the host is built in and not configurable. On a managed device, VPN or corporate proxy that filters traffic, allow-list it; otherwise every SDK call fails as a transport error. The link domains themselves (*.linktrail.io or your custom domain) need to be reachable for taps and for the association files.
React Native on iOS: links never arrive at all
Your AppDelegate isn't forwarding to RCTLinkingManager. The continue-userActivity override for Universal Links is not in the stock RN template; see the React Native SDK page.
Wrong screen
Your router doesn't handle that deep link path; log the link in onLink.
New installs opening on the home screen instead of the linked screen? Debugging deferred deep links walks through the five places the payload gets lost.