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.

Prerequisites

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.

  1. 1Register the app — Apps → Register app, with your package name and every SHA-256 signing fingerprint you ship under, including Play App Signing.
  2. 2Register the identifier your build really has — the SDK sends your app's applicationId (context.packageName, including any applicationIdSuffix), 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 (onError reports it as a server error).
  3. 3Add a link domain — Domains. Or use your free *.linktrail.io subdomain.
  4. 4Create an API key — Settings → API Keys. That is the lt_live_… value below.
  1. Install

    The SDK is published to Maven Central, so no custom repository is needed — just add the dependency:

    app/build.gradle.ktsKotlin
    dependencies {
        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). Keep google() 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.

  2. Integrate

    App.ktKotlin
    import 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 */ }

    customData is a map of strings to strings. A nested object or array set on the link arrives as a compact JSON string — decode it yourself.

    Note

    LinkTrail.configure is 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: register onLink, onAttribution and onError again on it, and use LinkTrail.shared rather than a stored reference, because every call on a replaced instance is ignored. Saved state (install flags, consent, device ID) is kept.

  3. 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 call setConsent(true) or setConsent(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. Call setConsent once your consent UI resolves — the cross-platform model, CMP integration and the Play data-safety answers are in Consent & privacy:

    Important

    An app that never calls setConsent gets no deferred link and no attribution. Wire both answers of your consent prompt to setConsent, or pass requireConsent: false if 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))
    Warning

    The deferred link is delivered at least once. It is saved as soon as it resolves and kept until an onLink handler 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. setConsent re-sends the install only when autoTrackInstall is on — with it off, call trackInstall() yourself after the decision; a call made before it is held and not re-sent.

  4. Forward incoming links from your Activity — both entry points:

    Kotlin
    override 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.

  5. 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>
    Warning

    The assetlinks.json file 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.

    Warning

    handleDeepLink claims only https links on linktrail.io (or a subdomain) and on the hosts listed in linkDomains (or their subdomains). Custom domains must be listed: links on go.yourbrand.com need LinkTrailOptions(linkDomains = listOf("go.yourbrand.com")), or the app opens and never navigates. Any other URL (mailto:, an OAuth callback, myapp://settings) returns false (the suspend form throws NotALinkTrailUrl) with no request and no onError, so route it yourself. Custom-scheme LinkTrail links (myapp://slug) are no longer handled; use the https link. 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 start resolves 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.

    Warning

    Deferred 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 install or 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.

  6. Verify it works

    Every failure on this page is silent unless you look in the right place. Check these in order:

    1. 1SDK logs. Pass LinkTrailOptions(logEnabled = true) to configure, then adb logcat -s LinkTrail. On the first launch expect Install held — awaiting consent decision; after setConsent(true) expect Consent granted, Sending install event… (consent: true), then Install attributed — Install attributed (deterministic click token) when the click token was recovered — or Organic 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 means configure never ran.
    2. 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.
    3. 3Overview and Links. Dashboard → Overview: the installs count goes up. Links → your link shows a click and an install.
    4. 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 through onError as LinkTrailError.InvalidApiKey — but only once the install is sent: after setConsent, or at configure with requireConsent: false. Nothing is sent before that.
    5. 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.

More

Kotlin
// 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 fieldDefaultNotes
requireConsenttrueDeny-by-default: the deferred install is held until setConsent(true/false), then routes (grant attributes, deny routes-only); re-engagement routes regardless
autoTrackInstalltruefalse = defer the install call entirely; call trackInstall() yourself
logEnabledfalseMaster switch for SDK logging (tag: LinkTrail)
logLevelINFODEBUG · INFO · WARNING · ERROR · NONE
requestTimeoutMillis15 000Per-request network timeout
retryPolicyDEFAULT3 attempts, 500 ms base / 8 s max backoff; DISABLED turns retries off
linkDomainsempty (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

APIPurpose
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 / lastDeepLinkCached last results
trackInstallAsync / handleDeepLinkAsync / trackEventAsyncCoroutine suspend twins
LinkTrail.resetForTesting(context)Static. Clears the install flag, attribution, queued events, device ID and consent — testing only
Note

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.