deep-linking

Deferred Deep Linking on Android: How to Implement It in Kotlin

Deferred deep linking on Android hands the tapped link to the app on first launch through the Play Install Referrer, with fingerprint matching as the fallback.

WarpLink Team··19 min read

TL;DR: Deferred deep linking on Android lets a user tap a link, install your app from the Play Store, and land on the intended screen on first launch, even though the link never reached the app. Android is the easy platform for it because the Play Store carries a referrer string through the install: the redirect appends referrer=utm_source=warplink&utm_content={link_id} to the store URL, the app reads it back on first launch through Google's Play Install Referrer library, and the server gets an exact handoff with confidence 1.0 and matchGuaranteed true. When the referrer is missing, which happens for sideloaded APKs, devices with no Play Store, and installs older than Google's documented 90 day retention, the SDK falls back to a probabilistic fingerprint of IP, language, and timezone, scored from 0.85 down to 0.20 as the click ages. The check runs once per install, gated by a marker in no-backup storage, and a reinstall runs it again. In Kotlin it is one configure() call, or one checkDeferredDeepLink when you want the no-match answer too.

Deferred deep linking on Android is the technique that carries a link destination through an install, so a user who taps a link without the app still lands on the intended screen the first time the app opens.

Android App Links solve one problem well. The user has your app, taps an https:// link on a domain you have verified with assetlinks.json, and the system routes the intent straight into your Activity. No disambiguation dialog, no browser detour.

The moment the app is not installed, that chain evaporates. The link opens in Chrome, Chrome follows the redirect to the Play Store, the user installs, taps Open, and arrives on your launcher screen with no idea why they came. The product page they wanted, the invite they accepted, the campaign that sent them: none of it survived. The intent that would have carried the URL was never created, because there was no app to send it to.

That is the gap deferred deep linking fills, and unlike iOS, Android gives you a native mechanism to fill it with. The Play Store passes a string through the install, so nobody has to guess. Many Android codebases are missing this piece today only because the hosted link products that used to provide it as a checkbox have thinned out, Firebase Dynamic Links among them when it shut down in August 2025. That is the only mention of it here: the mechanism below is the same whether you build it or buy it.

Why Android Is the Easy Platform

A deferred deep link is not delivered. It is matched. Two events on opposite sides of an install get stitched back together after the fact. The click: the user taps a link without the app, and the redirect records the click with the signals it can see from the browser, the request IP, the primary language, and the timezone, before sending them to the store. The install: on first launch the SDK asks the server which recent click this install belongs to.

On iOS the second half is a guess. There is no referrer, so the server compares install-time signals against click-time signals and returns a probability. On Android it can be an answer: the Play Store hands the app an opaque string the click side put there, and the server reads a link ID straight out of it.

That asymmetry changes what you are allowed to do with the result. A probabilistic match keys on a network, not a device, so every phone behind one address sharing a language and a timezone lands in the same bucket. A referrer match keys on the install itself. That is why matchGuaranteed exists as a separate field from matchConfidence, and why on Android it comes back true far more often than an iOS developer ever sees.

How the Install Referrer Is Set and Read

The click side: a query parameter on the store URL

The Play Store honors a referrer query parameter on a listing URL. Whatever you put in it is stored against that install and handed back to the app afterwards. WarpLink's redirect appends its own pair when, and only when, the resolved destination is a play.google.com URL:

utm_source=warplink marks the referrer as one the SDK should claim: any other source is the ordinary organic case, reported as such rather than as a failure. utm_content={link_id} is the payload, the link's UUID, and the SDK validates its shape before trusting it, so a truncated or rewritten value is rejected rather than sent up as a lookup that will never resolve. A referrer value you set yourself is merged rather than refused: WarpLink's pair goes first and yours is appended, because the parser keeps the first occurrence of each key.

The install side: Google's Install Referrer library

On first launch the SDK connects to the Play Store app and asks for the referrer, through Google's Play Install Referrer library, com.android.installreferrer:installreferrer, currently version 2.2. The WarpLink Android SDK depends on it already, so there is nothing to add to your build.gradle.kts beyond the SDK itself.

Three properties of that read shape the whole design:

  • It is asynchronous and it can hang. The library binds to a service in the Play Store app, which may be updating, disabled, or missing. The SDK gives it a 2 second budget, then moves to the fingerprint path rather than blocking first launch on a service that will never answer.
  • It is only worth calling once. Google's guidance is to invoke the API once, on the first execution after install. The SDK does exactly that, gated by the completion marker described below.
  • It carries a timestamp. Alongside the referrer string, the library returns installBeginTimestampSeconds, the moment the Play install began. That number separates a genuine first open from an app that has sat on the device for months.

When the referrer names a WarpLink link, the SDK sends the link ID up with the attribution request and the server records a deterministic match: matchType is DETERMINISTIC, matchConfidence is 1.0, and matchGuaranteed is true.

The 90 day window, and the 7 day routing window

Google documents the install referrer as available for 90 days, and it does not change unless the app is reinstalled. That is generous, and it creates a case worth handling deliberately. Suppose a user installed your app four months ago from a campaign link and you are only now shipping the release that includes an attribution SDK. On that first launch the referrer is read for the first time. Attributing the install to the original link is correct. Dropping that user into a product page from four months ago is not: they are a long-time user opening what they think is a routine update.

The SDK splits those decisions. It attributes the install regardless of age, and routes the user only when the Play install began within the last 7 days, the click-to-install window deferred deep linking is built for. Past that the check settles with nothing to route and your app opens normally. An unknown timestamp routes, on the principle that silence is not evidence of age.

When the Referrer Is Missing

The deterministic path is the primary one, not the only one. It is unavailable in several ordinary situations:

  • The app was sideloaded. An APK installed with adb install, or handed over as a file, never went through the Play Store. There is no referrer to return.
  • The device has no Play Store, so nothing can bind to the referrer service at all.
  • The Play Store service refuses. The library reports this as a response code rather than an exception: service disconnected, service unavailable, feature not supported on this device, developer error, or a denied bind. The SDK names the code in its Logcat output under the WarpLink tag, which is the only way to tell "this user installed organically" from "the referrer API never worked on this device".
  • The referrer data expired, or the referrer is not ours: another utm_source, or a utm_content that is not a valid link identifier.

Every one of those produces the same result: no link ID, so the SDK falls back to probabilistic fingerprint matching with no code change on your side.

What the fingerprint is made of

The SDK collects three signals and sends them raw. Nothing is hashed on the device:

SignalSourceNotes
Preferred languageLocale.getDefault().toLanguageTag()Sent raw, for example en-US. The server normalizes it to the primary subtag.
Timezone offsetTimeZone.getDefault().getOffset(now)In minutes, daylight-saving aware, not the raw offset.
IANA zone nameZoneId.systemDefault().idFor example America/Toronto. Empty when the process default zone has no tzdb entry.

The IP address is the third component, derived server-side from the address the attribution request arrives on. An app cannot reliably see its own public IP, so a device-computed IP hash could never match the one computed at click time. Letting the server do it on both sides means the two hashes are computed the same way, in the same place.

Two signals are conspicuously absent. The SDK collects no User-Agent and no screen dimensions, because neither matches reliably between a browser at click time and a native app at install time. It also collects no advertising ID, no Android ID, no location, and no cross-app identifier. Collection is total and never throws: a device whose zone name cannot be resolved still sends its language and offset, because a degraded request that misses is recorded server-side where an abandoned one is not.

Confidence, and the One Field to Gate On

The fingerprint has three variants, and which one you get depends on what the SDK could send. enriched_tz hashes the IANA zone name and is what current SDKs send. enriched hashes the minute offset instead and remains the path for older SDKs and for devices that cannot resolve a zone. basic drops the timezone entirely and keys on IP and language alone.

Time since clickenriched_tzenrichedbasic
< 1 hour0.850.800.70
< 3 hours0.650.600.50
< 6 hours0.500.450.35
< 24 hours0.300.250.20

Past 24 hours there is no match at all. A deterministic referrer match sits above this table entirely at 1.0.

The zone name earns the higher ceiling because it carries far more entropy than the offset, roughly 340 zones against 38 offsets, and because it does not shift at a daylight-saving boundary.

Two multipliers then apply, and both only ever reduce the score, because a confident wrong answer is worse than an honest uncertain one. A bucket that held more than one distinct link scores x0.6, since the answer was chosen from a set rather than found outright. A shared click IP scores x0.6 for carrier-grade NAT or a private address, x0.9 for a household IPv4 address, and x1.0 for IPv6: a carrier address fronts hundreds of unrelated subscribers, a home router fronts one household. Clicks that share a fingerprint are kept as a list, newest first, capped at 10, so a second person behind the same address does not overwrite the first.

The match window

The window that governs all of this is set per link on the server, in the dashboard, not in the SDK. There is no matchWindowHours option to pass. The default is 6 hours and the ceiling is 24 hours, so the bottom row of the table only applies to links configured past the default. It is short on purpose: the fingerprint key is a network, not a device, so every extra hour lets another stranger behind the same shared address join the bucket while adding almost no real matches. This governs the probabilistic tier only, and the referrer branch reads an install record instead, so a short window costs it nothing.

Gate on matchGuaranteed, not on a threshold

The result carries three attribution fields, and they are not interchangeable. matchGuaranteed: Boolean is true only for a deterministic match, which on Android means the Play Install Referrer named this link. matchType: MatchType? is DETERMINISTIC or PROBABILISTIC, useful for logging and tuning. matchConfidence: Double? is the score, useful for reporting and not for a trust decision.

Anything sensitive belongs behind matchGuaranteed: automatic sign-in, restoring a session, showing personal data, resuming a purchase. A probabilistic match is a best guess computed from a network-shaped fingerprint. It can name the wrong user, and no confidence threshold can tell you that it did.

Confidence is the second decision, and it applies only once matchGuaranteed is false. Route to specific public content above 0.5, and degrade gracefully below it, which can mean a welcome screen that names the content so the user opts in rather than a hard redirect you are 30 percent sure about.

First Launch, Completion Markers, and Reinstalls

The deferred check runs on first launch and exactly once per install. How that "once" is enforced is the single most common source of confusing test results. The SDK keeps two markers with deliberately opposite lifetimes:

The completion marker is a file in the app's no-backup storage, noBackupFilesDir. Android Auto Backup never captures that directory, so the marker dies with the install that wrote it. It is written only after the server gives a definitive answer, a match or a confirmed no-match, which is what makes an offline first launch safe: the attempt is recorded but the gate stays open, so the SDK retries next launch instead of caching a failure forever. If the directory cannot be written, the marker falls back to a SharedPreferences entry bound to this install's first-install time, so a restored copy from an older install cannot gate the new one.

The device-seen marker is a plain SharedPreferences entry, and Auto Backup does restore it. It gates nothing. Its only job is to set is_reinstall: true on the attribution request, so an install following an earlier attributed one on the same device can be told apart afterwards.

The consequence is clean. A reinstall is a new install: the completion marker is gone, so the check runs again and the install is attributed again. Both count, and the dashboard shows them together as one installs number. No manifest or backup_rules.xml changes are needed. If you have set android:allowBackup="false", the device-seen marker cannot survive an uninstall, so a reinstall on that device is reported as a first install. Attribution is unaffected, and the SDK logs the caveat when debug logging is on.

Implementing It in Kotlin

The SDK requires API 26 and above and Kotlin 1.8 and above. Add the dependency to your app module's build.gradle.kts:

You do not add the Play Install Referrer library yourself. The SDK brings it.

Step 1: Configure once, in Application.onCreate

configure() registers cold-start handling and fires the deferred check for you. One onLink callback receives everything: a tapped App Link, a warm-start intent, and a deferred install match, told apart with isDeferred.

Register the class with android:name=".MyApp" in AndroidManifest.xml, declare android.permission.INTERNET, and forward warm-start intents with WarpLink.onNewIntent(intent) from the Activity that receives your App Links.

The credential above is an SDK key, created in the dashboard under API Keys and then SDK key. An API key looks identical and resolves deep links fine, but it cannot record installs, so the integration looks healthy while every attribution call is rejected. Check that before hunting for referrer bugs. A malformed key does not throw: configure() logs a warning, delivers WarpLinkError.InvalidApiKeyFormat to your onLink, and leaves the SDK unconfigured, which is what makes it safe to call from Application.onCreate().

Step 2: Route on the result

WarpLinkDeepLink carries the destination, an Android-specific deep link URL when the link has one, the custom parameters attached to the original link, and the three attribution fields.

That customParams payload carries the original campaign context, which is the bridge from routing the user to attributing the install.

Step 3: Take the check manually if you need the no-match answer

One behaviour of the automatic path matters before you build an onboarding flow on it. onLink is invoked for a deferred match; when there is no match, nothing is dispatched, because a no-match is not a link. So onLink alone cannot tell you "no deferred link arrived, start onboarding".

If you need that answer, for example to hold a splash screen until you know which way to go, call checkDeferredDeepLink yourself and turn off the automatic firing so the check does not run twice:

Then, in the Activity that owns your first-launch experience:

Every callback is dispatched on the main thread, so driving navigation directly from it is safe. The three second cap is your own policy, not the SDK's: the referrer read alone is budgeted at 2 seconds and a network round trip follows it. If the timeout wins, the check is not consumed and it retries on the next launch.

Calling checkDeferredDeepLink while a check is already in flight does not start a second one. The later caller is parked and answered with the running check's result, so a manual call racing the automatic one never loses the match.

Testing It on a Real Device

The fingerprint path is easy to test. The referrer path is not, because nothing outside the Play Store produces a real referrer, and the old broadcast mechanism the internet still describes is not what the library reads.

To exercise the fingerprint path, which is the sideload path:

Keep the gap between the tap and the launch inside an hour, both to stay inside the match window and to land in the top confidence band. With debugLogging = true the SDK logs the referrer outcome, the fallback decision, and the matched link ID under the WarpLink tag.

To exercise the referrer path, you need an install the Play Store performed. An internal testing track is the usual way to get one without a public release: upload the build, opt a tester in, and install from the store listing rather than from Android Studio. Confirm on your own build that the referrer parameter survives your particular opt-in flow rather than assuming it, and read the Logcat line to see which branch fired.

A physical device is worth the trouble throughout, since emulator images without Play Services cannot exercise the referrer branch at all. If the problem turns out to be App Links verification rather than deferred matching, adb shell pm get-app-links com.yourcompany.yourapp (Android 12 and above) reports what the system thinks of your domain.

This is the section the thin posts skip. Most reports of "it does not work" are testing artifacts rather than integration bugs.

  • The install did not come from the Play Store. Android Studio, adb install, and any file-based install bypass the store, so there is no referrer. You are testing the fingerprint path whether you meant to or not, and it will score like one.
  • The check already completed for this install. It runs once, gated by the completion marker. A rerun from Android Studio over an existing install returns the cached answer with no network request. Uninstall first, every time.
  • You are watching onLink for a no-match. The automatic path dispatches matches only. If your onboarding depends on hearing "nothing matched", take the manual path in step 3 above.
  • The match window expired, so the stored click aged out. The default is 6 hours, set per link.
  • The install is older than the routing window. The referrer named a link and the install was attributed, but the SDK did not route because the Play install began more than 7 days ago. Debug logging says so explicitly.
  • The device was offline on first launch. The check fails with WarpLinkError.NetworkError, the attempt is not consumed, and it retries next launch. That reads as a silent failure if you only watch the first run.
  • You are using an API key, not an SDK key. Resolution works and attribution is rejected, which produces exactly the symptom of a referrer that never matches.
  • The click and the install were on different networks. On the fingerprint path, tapping on office wifi and installing on cellular is enough to drop the match.
  • The Play Store service refused the bind. Check Logcat for the response code the SDK names. Feature not supported and permission denied are device conditions, not code bugs.

If the link fires and the app opens but lands in Chrome rather than in your Activity, that is a different problem. It is App Links verification, not deferred matching, and it has its own guide below.

Frequently Asked Questions

Does deferred deep linking on Android need Google Play Services? The deterministic path does, because the Play Install Referrer is read from the Play Store app over a service binding. On a device with no Play Store, such as a device running an alternative services stack, that binding fails and the SDK falls back to probabilistic fingerprint matching automatically. You write no extra code for that fallback, but the match stops being an exact handoff and becomes a scored guess.

Does deferred deep linking work with a sideloaded APK? Only through the fingerprint fallback. An APK installed with adb install or dropped onto the device never passed through the Play Store, so there is no referrer for the Install Referrer library to return and the deterministic branch cannot fire. The SDK detects the empty read and sends the device signals instead, which still produces a match when the click was recent and on the same network.

How long does the Play Install Referrer last? Google documents the install referrer as available for 90 days, and it does not change unless the app is reinstalled. That is far longer than the probabilistic match window, which defaults to 6 hours per link. The WarpLink SDK still attributes an install whose referrer is months old, but it stops routing the user into that content once the Play install began more than 7 days ago, because dropping a long-time user into week-old content is worse than showing them the app.

Why is the match deterministic on Android but not on iOS? Because the Play Store carries the click referrer through the install and the App Store carries nothing. On Android the server is told exactly which link produced the install, so confidence is 1.0 and matchGuaranteed is true. On iOS the only deterministic signal is the vendor identifier, which answers the same install asking again, so a genuine first install falls back to a probabilistic fingerprint that decays over hours.

Why isn't my deferred deep link firing on Android? Usually because the install did not come from the Play Store, or because the check already completed for this install. The deferred check runs once per install and records a completion marker in the app's no-backup storage, so a rerun from Android Studio returns the cached answer instead of matching again. Uninstall, reinstall, and keep the gap between the click and the install short. If the device was offline on first launch, the attempt is not consumed and the check retries next launch.

Do I have to call checkDeferredDeepLink myself? No. configure() fires the deferred check for you and delivers a match to your onLink callback with isDeferred set to true. Call checkDeferredDeepLink manually only when you want the no-match answer as well, for example to hold a splash screen until the answer arrives. In that case set automaticDeferredDeepLinks = false so the check does not run twice.

Everything above is the mechanism, and the mechanism is the same no matter who runs it. What you cannot host inside your own app is the other half: the redirect at the edge that appends the referrer to the store URL and records the click's signals before the Play Store takes over, and the attribution endpoint that computes the fingerprint from the request IP and walks the cascade when the referrer is not there. WarpLink is those two halves, and in Kotlin it collapses to one configure() call.

The part worth saying plainly is that deferred deep linking is install attribution. The same match that routes the user tells you which link, campaign, and channel drove the install, because matchType, matchConfidence, matchGuaranteed, and customParams come back in one payload. That is the bridge from the linking pillar to the attribution pillar, and from there into the real-time analytics that show which taps become users.

Create a free WarpLink account and get deep linking, install attribution, and the analytics behind them in one SDK, with 10,000 clicks a month on the free tier and no time limit.

WarpLink Team

Building affordable, reliable link infrastructure for mobile teams. Deep linking, install attribution, and real-time analytics in one SDK.

Related Posts