Deferred Deep Linking Without an SDK
Direct deep linking is solved by the operating system. Getting a brand-new installer to the screen they originally tapped is not — that gap is what deferred deep linking fills, and it is the one part no platform hands you.
On This Page
The gap the OS leaves
When your app is installed, a tap on a Universal Link or App Link is handled entirely by iOS or Android. You get the URL, you route, done. No vendor is involved.
When it is not installed, the chain breaks. The person goes to the App Store or Play, comes back minutes later in a completely fresh process, and nothing about that first launch says which link brought them. There is no shared identifier across that boundary — deliberately, because a shared identifier across that boundary is exactly what the platforms spent the last several years removing.
Deferred deep linking is the practice of bridging it anyway, with an educated guess made on a server.
Approaches that cannot work
These come up constantly, including in otherwise-credible write-ups. Each fails for a structural reason, not an implementation one.
IDFV / Android App Set ID
Both are readable only from inside an installed app. A browser cannot produce either, so there is nothing to record at click time. You can compare the value to itself after install, which tells you nothing about the click that preceded it. This is impossible in principle, not merely unimplemented.
Pasteboard tokens
Writing a token to the clipboard on the web and reading it on launch worked for years. Since iOS 16 a read raises a visible paste prompt, which makes it both conspicuous to the user and unreliable. Closed.
SKAdNetwork
Aggregated and delayed by design. It answers "did this campaign produce installs", never "which screen should this particular launch open".
Play Install Referrer — real, but not what we use
This one genuinely works, on Android only: a referrer string set on the Play Store URL survives installation and can be read on first launch. It is a legitimate mechanism and a sensible thing to layer on for campaign parameters. DeepTap does not currently use it, and we would rather say so than describe a feature we have not built.
What DeepTap does instead
The flow
- Someone taps your link without the app. DeepTap records the destination against a fingerprint built from their client IP and device platform.
- They are sent to the App Store or Play.
- They install and launch. Your app makes one HTTPS GET to the deferred-link endpoint.
- DeepTap computes the same fingerprint from that request and looks for an unclaimed link inside the match window.
- On a match, the row is claimed atomically and the saved path and query parameters are returned.
Three deliberate constraints keep that guess honest:
A short window
60 minutes by default. A real click-install-launch sequence takes minutes; anything older is more likely a stranger than your visitor.
Ambiguity means no match
If two unclaimed links share a fingerprint in the window, DeepTap returns nothing. Delivering a stranger's invite token is worse than delivering nothing.
Crawlers never stored
Link-preview bots from Facebook, WhatsApp, Telegram and friends fetch every shared URL. Their clicks are discarded so they cannot collide with a real one.
Where it degrades
Carrier-grade NAT, VPNs, iCloud Private Relay, and any network change between the click and the launch all reduce the match rate — the last one especially, since installing over cellular and opening on home Wi-Fi is an extremely normal thing to do. No fingerprint method avoids this without an SDK in your app. We do not publish a headline match-rate percentage, because the honest number depends on your traffic.
The API contract
GET https://deeptap.io/api/deferred-link
?subdomain=YOUR-SUBDOMAIN.deeptap.io
&platform=ios # or android
200 — a link was found and claimed
{
"success": true,
"data": {
"path": "/product/123",
"queryParams": { "ref": "campaign" },
"referrer": "https://twitter.com",
"createdAt": "2026-08-26T10:30:00Z"
}
}
404 — nothing to deliver (no match, or already claimed)
{ "success": false, "error": "No deferred link found" }
400 — the request could not be interpreted
{ "success": false, "error": "Client IP address could not be determined" }
{ "success": false, "error": "Platform could not be determined. Pass platform=ios or platform=android." }
429 — too many requests from this client IP
{ "success": false, "error": "Too many requests" }Two things worth knowing
- • Pass
platformexplicitly. It can be inferred from the User-Agent for URLSession and OkHttp, but Dart'sdart:iosends an identical string on both platforms, so Flutter apps must send it. - • The claim is single-use and atomic. The first successful call takes the link; a second call gets a 404. Persist the result yourself if you need it after onboarding finishes.
- • Handle 429 like a miss. The endpoint is unauthenticated — a freshly installed app has no credential to present — so it is rate limited per client IP instead. One call per install is far inside the limit; if you ever see a 429, fall through to your default screen rather than retrying in a loop.
Implementing it
There is no SDK. This is your platform's standard networking, called once, on first launch only — guard it with a flag so a returning user never triggers it.
Swift
func checkDeferredLink() async -> DeferredLinkData? {
let key = "deferredLinkChecked"
guard !UserDefaults.standard.bool(forKey: key) else { return nil }
UserDefaults.standard.set(true, forKey: key)
var components = URLComponents(string: "https://deeptap.io/api/deferred-link")!
components.queryItems = [
.init(name: "subdomain", value: subdomain),
.init(name: "platform", value: "ios"),
]
guard let url = components.url,
let (data, _) = try? await URLSession.shared.data(from: url),
let response = try? JSONDecoder().decode(DeferredLinkResponse.self, from: data),
response.success else { return nil }
return response.data
}Kotlin
suspend fun checkDeferredLink(): DeferredLinkData? = withContext(Dispatchers.IO) {
val prefs = context.getSharedPreferences("deeptap", Context.MODE_PRIVATE)
if (prefs.getBoolean("deferredLinkChecked", false)) return@withContext null
prefs.edit().putBoolean("deferredLinkChecked", true).apply()
val url = "https://deeptap.io/api/deferred-link" +
"?subdomain=$subdomain&platform=android"
runCatching {
val body = OkHttpClient().newCall(Request.Builder().url(url).build())
.execute().use { if (!it.isSuccessful) return@runCatching null else it.body?.string() }
body?.let { parseDeferredLink(it) }
}.getOrNull()
}Framework-specific versions, including the Flutter and React Native forms, are in the iOS, Android, React Native and Flutter guides.
Designing the fallback
This is the part most implementations skip, and it matters more than the match rate. A miss is a normal outcome, not an error. Your app must be good when there is no deferred link — which is also every organic install you will ever get.
Practical consequences worth designing for:
- • Never block the launch on the call. Show your normal first screen immediately and route when the answer arrives. A user staring at a spinner because your deferred lookup is slow is a worse outcome than a missed match.
- • Route after onboarding, not during it. If you have a sign-up flow, hold the destination and navigate once the person is through it. Dropping someone into a product page mid-signup loses both.
- • Treat the payload as untrusted input.
pathandqueryParamscame from a URL someone else constructed. Validate before you navigate. - • Do not treat a match as authentication. The fingerprint is a probabilistic guess, not proof of identity. If a link carries an invite or referral token, verify that token server-side before granting anything.
Testing
The awkward part is that a real test needs a real install, and your first-launch flag means you only get one attempt per install unless you clear it.
A workable loop
- Add a debug-only button that clears the first-launch flag, so you can retry without reinstalling.
- Tap a real link on the device, in Messages or Notes, so the click is recorded from the same network your app will call from.
- Trigger the check and confirm you get the path you clicked.
- Then test the miss: clear the flag, call again without clicking anything, and confirm your app behaves well on the 404.
Test the miss path at least as carefully as the hit path. It is the more common one.
FAQ
Why can’t I just match on IDFV or the Android App Set ID?
Because neither exists at click time. IDFV is created by iOS for your app on that device — a web browser cannot read it, so there is nothing to record when someone taps the link. The Android App Set ID has the same problem: it is readable from inside an app, not from a browser. Any scheme built on them can only compare a value against itself after install, which tells you nothing about the click that came before.
What about the clipboard trick?
Writing a token to the pasteboard on the web and reading it on first launch used to work. Since iOS 16 reading the pasteboard shows the user a visible paste prompt, so using it for attribution is both conspicuous and unreliable. Treat it as closed.
Does SKAdNetwork help?
Not for this. SKAdNetwork returns delayed, aggregated, campaign-level postbacks by design. It can tell you a campaign produced installs; it cannot tell one specific install which product page to open.
What does DeepTap actually match on?
A server-side fingerprint of the client IP and the device platform, restricted to links created within the match window — 60 minutes by default. If more than one unclaimed link shares that fingerprint inside the window, DeepTap returns no match rather than guessing, because handing one person another person’s link is worse than missing.
How reliable is it?
Most reliable when the click and the first launch happen on the same network. It degrades behind carrier-grade NAT, VPNs, iCloud Private Relay, and any network change between click and install. We do not publish a single match-rate number because the real figure depends entirely on your traffic mix — a corporate app on office Wi-Fi and a consumer app on mobile data behave very differently.
Can the same link be claimed twice?
No. The claim is atomic: the first successful call marks the row retrieved and any later call receives a 404. Make the call once, on first launch, and persist the result yourself if you need it after your onboarding flow.