Android SDK
Install and integrate the LinkTrail Android SDK — io.linktrail, minSdk 26, on Maven Central — with configure, onLink, consent and the Install Referrer.
Binary AAR · package io.linktrail · entry point LinkTrail · minSdk 26 · artifact io.linktrail:sdk:0.1.0 on Maven Central.
Before this works, set up three things in the dashboard. Skip one and links fail silently — they open the browser instead of your app. Full flow: Quickstart.
- 1Register the app — Apps → Register app, with your package name and every SHA-256 signing fingerprint you ship under, including Play App Signing.
- 2Register the identifier your build really has — the SDK sends your app's
applicationId(context.packageName, including anyapplicationIdSuffix), and it must equal the identifier you registered, in the same workspace as the API key. A debug build with a suffixed ID (com.acme.shop.debug) needs its own registered app, or every install answers 404 (onErrorreports it as a server error). - 3Add a link domain — Domains. Or use your free
*.linktrail.iosubdomain. - 4Create an API key — Settings → API Keys. That is the
lt_live_…value below.
Install
The SDK is published to Maven Central, so no custom repository is needed — just add the dependency:
app/build.gradle.ktsKotlindependencies { implementation("io.linktrail:sdk:0.1.0") }mavenCentral()is already in the default repositories of every Android project, which also resolves the SDK's transitive dependencies (coroutines, Play Install Referrer, App Set ID). Keepgoogle()alongside it.Nothing else to
configure: the INTERNET permission merges in from the SDK's manifest, and consumer ProGuard/R8 rules are bundled in the AAR — no app-side keep rules needed.Integrate
App.ktKotlinimport io.linktrail.LinkTrail // In Application.onCreate(). The API key is required — a blank key throws. LinkTrail.configure(context = this, apiKey = "lt_live_...") // One hook — fires for deferred (first-launch) AND re-engagement links: LinkTrail.shared?.onLink { link, source -> router.route(link.path, link.customData) } LinkTrail.shared?.onError { error -> /* e.g. LinkTrailError.InvalidApiKey */ }customDatais a map of strings to strings. A nested object or array set on the link arrives as a compact JSON string — decode it yourself.NoteLinkTrail.configureis a singleton. Calling it again with the same API key and equal options is a no-op that returns the existing instance. A call with a different key or options shuts the previous instance down (its pending work is cancelled and its callbacks are dropped) and returns a new one: registeronLink,onAttributionandonErroragain on it, and useLinkTrail.sharedrather than a stored reference, because every call on a replaced instance is ignored. Saved state (install flags, consent, device ID) is kept.Consent
Consent gating is on by default (
requireConsent = true) and is deny-by-default. A fresh install is held until you make a consent decision — nothing is sent until you callsetConsent(true)orsetConsent(false), and only then does the deferred link route: grant attributes it, deny routes it without recording. Re-engagement links (an already-installed app opening a link) route regardless of consent — that only decides whether the open is recorded. The decision is persisted; there is no getter. CallsetConsentonce your consent UI resolves — the cross-platform model, CMP integration and the Play data-safety answers are in Consent & privacy:ImportantAn app that never calls
setConsentgets no deferred link and no attribution. Wire both answers of your consent prompt tosetConsent, or passrequireConsent: falseif consent is collected elsewhere.Kotlin// Grant → releases the held install as an attributed install + flushes queued events: LinkTrail.shared?.setConsent(true) // Deny → still releases the install to route the deep link, but records nothing; clears the queue: LinkTrail.shared?.setConsent(false) // To track at init with no gate, opt out (an explicit setConsent(false) still wins): LinkTrail.configure(context = this, apiKey = "lt_live_...", options = LinkTrailOptions(requireConsent = false))WarningThe deferred link is delivered at least once. It is saved as soon as it resolves and kept until an
onLinkhandler is registered — in that launch or a later one — and it is marked routed only after the handler ran, so a crash inside it can mean one redelivery. A saved link older than 7 days is dropped.setConsentre-sends the install only whenautoTrackInstallis on — with it off, calltrackInstall()yourself after the decision; a call made before it is held and not re-sent.Forward incoming links
Forward incoming links from your Activity — both entry points:
Kotlinoverride fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) LinkTrail.shared?.handleDeepLink(intent?.data) } override fun onNewIntent(intent: Intent) { // app already running super.onNewIntent(intent) setIntent(intent) // keep getIntent() current LinkTrail.shared?.handleDeepLink(intent.data) }Every callback API also has a coroutine suspend twin (
trackInstallAsync,handleDeepLinkAsync, trackEventAsync). Callbacks are delivered on the main thread.Deep-link setup
Declare your App Links host in the manifest:
AndroidManifest.xmlXML<intent-filter android:autoVerify="true"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="https" android:host="yourapp.linktrail.io" /> </intent-filter>WarningThe
assetlinks.jsonfile is generated and hosted by LinkTrail — it's built from the package name + SHA-256 fingerprints you registered in the dashboard (to change fingerprints, edit the app in the dashboard, not a file). Links opening the browser or Play Store instead of your app? That's almost always App Links verification — see Troubleshooting.WarninghandleDeepLinkclaims onlyhttpslinks onlinktrail.io(or a subdomain) and on the hosts listed inlinkDomains(or their subdomains). Custom domains must be listed: links ongo.yourbrand.comneedLinkTrailOptions(linkDomains = listOf("go.yourbrand.com")), or the app opens and never navigates. Any other URL (mailto:, an OAuth callback,myapp://settings) returnsfalse(the suspend form throwsNotALinkTrailUrl) with no request and noonError, so route it yourself. Custom-scheme LinkTrail links (myapp://slug) are no longer handled; use thehttpslink. The full rules: Which links the SDK claims. Upgrading from an earlier version? See Upgrading to 0.1.0.Verify App Links
Install the build on a device or emulator (Android 12+), then ask the system what it decided:
TerminalShell# 1. The verification state per domain — you want "verified": adb shell pm get-app-links com.example.app # yourapp.linktrail.io: verified # 2. Anything else? Re-run verification, wait a few seconds, check again: adb shell pm verify-app-links --re-verify com.example.app adb shell pm get-app-links com.example.app # 3. Simulate tapping a link (typing it into the browser never opens the app): adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE \ -d "https://yourapp.linktrail.io/promo"Without a package name,
am startresolves the link the way a tap does, so it opens your app only when the domain is verified. Still not verified: a fingerprint you install from is not registered (debug, upload and Play App Signing keys all differ) — see App Credentials and the SDK repo's TROUBLESHOOTING.md.WarningDeferred links need an install from Google Play. The click token travels in the Play Install Referrer, which only exists for installs from the Play Store. To test the deferred path, upload the build to a Play testing track (internal testing works), tap your link, and install from the store listing it opens. A build installed with
adb installor from Android Studio has no referrer, so its first launch can only be matched on IP address and platform — and only if that matching is switched on. To test the deterministic path on an emulator, a debug build can read a stand-in referrer: see Testing locally.Verify it works
Every failure on this page is silent unless you look in the right place. Check these in order:
- 1SDK logs. Pass
LinkTrailOptions(logEnabled = true)toconfigure, thenadb logcat -s LinkTrail. On the first launch expectInstall held — awaiting consent decision; aftersetConsent(true)expectConsent granted,Sending install event… (consent: true), thenInstall attributed—Install attributed (deterministic click token)when the click token was recovered — orOrganic install (no attribution match). Both platforms print[LinkTrail] [LEVEL] message, so these lines read[LinkTrail] [INFO] …on iOS and Android (Android logs them under the LinkTrail tag). No lines at all meansconfigurenever ran. - 2API key Last used. Dashboard → Settings → API Keys: the key's Last used changes from Never after the first call made with a valid key.
- 3Overview and Links. Dashboard → Overview: the installs count goes up. Links → your link shows a click and an install.
- 4onError. A wrong or revoked key does not fail
configure. The backend answers 401 with{"code":401,"message":"Invalid or missing API key"}and the SDK reports it throughonErrorasLinkTrailError.InvalidApiKey— but only once the install is sent: aftersetConsent, or atconfigurewithrequireConsent: false. Nothing is sent before that. - 5Re-test deferred without reinstalling. Call
LinkTrail.resetForTesting(context)— it clears the install flag, cached attribution, device ID and the consent decision, so the next launch behaves like a genuine first launch and asks for consent again. Testing only; never ship it.
[LinkTrail] [INFO] Install held — awaiting consent decision [LinkTrail] [INFO] Consent granted [LinkTrail] [INFO] Sending install event… (consent: true) [LinkTrail] [INFO] Install attributed (deterministic click token) [LinkTrail] [INFO] Install attributed [LinkTrail] [INFO] Organic install (no attribution match)Still nothing? Troubleshooting lists the usual causes by symptom.
- 1SDK logs. Pass
More
// Custom + revenue events:
LinkTrail.shared?.trackEvent("purchase", value = 59.99, currency = "USD")
// Cached results:
val attribution = LinkTrail.shared?.lastAttribution
val lastLink = LinkTrail.shared?.lastDeepLink
// Attribution hook (fires when an install is attributed):
LinkTrail.shared?.onAttribution { attribution -> /* … */ }
// Manual install (rare): autoTrackInstall = false, then call trackInstall() yourself —
// after your setConsent decision, or it is held (and setConsent does not re-send it):
LinkTrail.configure(context = this, apiKey = "lt_live_...",
options = LinkTrailOptions(autoTrackInstall = false))
LinkTrail.shared?.trackInstall()Options & defaults
| LinkTrailOptions field | Default | Notes |
|---|---|---|
| requireConsent | true | Deny-by-default: the deferred install is held until setConsent(true/false), then routes (grant attributes, deny routes-only); re-engagement routes regardless |
| autoTrackInstall | true | false = defer the install call entirely; call trackInstall() yourself |
| logEnabled | false | Master switch for SDK logging (tag: LinkTrail) |
| logLevel | INFO | DEBUG · INFO · WARNING · ERROR · NONE |
| requestTimeoutMillis | 15 000 | Per-request network timeout |
| retryPolicy | DEFAULT | 3 attempts, 500 ms base / 8 s max backoff; DISABLED turns retries off |
| linkDomains | empty (linktrail.io only) | Extra link hosts to claim (exact host or subdomain) — list every custom domain; see the warning under Deep-link setup |
Errors are a sealed LinkTrailError: MissingApiKey (thrown synchronously by configure), InvalidApiKey (the backend rejected the key — arrives via onError, not from configure), NotALinkTrailUrl, InvalidUrl, Server, Transport, Decoding, EmptyResponse. Under the hood the SDK also queues events that failed with a retryable error (transport, 408, 429, 5xx) and sends them on the next flush — at configure, on a consent grant and after each successful send — and handles the Play Install Referrer and App Set ID automatically — there's no API to call. (App Tracking Transparency / SKAdNetwork are iOS-only and have no Android equivalent.)
API surface
| API | Purpose |
|---|---|
| configure(context, apiKey, options) | Initialize; deferred install held until a setConsent decision, then routes (re-engagement routes regardless) |
| setConsent(granted) | Grant or revoke the consent gate (see Consent above) |
| onLink { link, source } | Deep link delivered (deferred + re-engagement) |
| onAttribution { attribution } | Install attribution result |
| onError { error } | Failures (invalid key, network…) |
| handleDeepLink(uri) | Forward incoming URLs; true only for a LinkTrail link (https, claimed host, slug) |
| trackEvent(name, value, currency) | Custom + revenue events |
| trackInstall(force) | Manual install track (with autoTrackInstall = false) |
| lastAttribution / lastDeepLink | Cached last results |
| trackInstallAsync / handleDeepLinkAsync / trackEventAsync | Coroutine suspend twins |
| LinkTrail.resetForTesting(context) | Static. Clears the install flag, attribution, queued events, device ID and consent — testing only |
Example app: KickFlip — a Jetpack Compose storefront in the android-sdk repo's example folder, wired end to end. cd example && ./gradlew :app:installDebug, with your API key in example/local.properties as linktrail.apiKey=lt_live_… (without a key, the built-in deep-link simulator still works offline). Source · example app · issue tracker · changelog.
Step-by-step walkthrough, including consent and testing through a Play track: How to set up deferred deep linking on Android.