When a user taps a link to your app and they don't have it installed, something breaks. The link dies. The user gets dumped into the App Store or Google Play, installs your app, opens it, and lands on the home screen with zero context about why they came. That referral code from the campaign? Gone. That product page a friend shared? Lost.
Deferred deep linking exists to fix this. It preserves the link's destination and any attached data across the app installation process, so the user arrives at the right screen on first open. The word "deferred" means the link fires later, after the install, instead of immediately.
This article explains exactly how deferred deep linking works at every layer: the click, the redirect, the fingerprint, the match, and the SDK callback. For use cases, see deferred deep linking use cases. If you're building this yourself or evaluating a platform, you'll understand what's happening under the hood.
The Problem: The Install Gap
Standard deep links work fine when the app is already installed. A Universal Link on iOS or an App Link on Android hands the URL to your app, your app parses it, and navigates to the right screen. Simple.
The problem shows up when the app is not installed. Here's what happens without deferred deep linking:
- A user clicks a link (from an email, ad, social post, or shared message).
- The OS checks whether an app is registered to handle the URL.
- No app is found.
- On iOS, a Universal Link falls back to the web URL. A custom scheme (
yourapp://) shows an error or does nothing. On Android, App Links fall back to the browser. - The user manually goes to the App Store or Google Play (or you redirect them there).
- The user installs the app and opens it.
- The app has no idea why the user came or where they wanted to go. The original link data is completely gone.
This "install gap" is where deferred deep linking picks up. Every piece of context attached to the original link (the destination path, query parameters, campaign tags, referral codes) needs to survive the trip through the app store and emerge on the other side when the app launches for the first time.
The tricky part: there is no direct communication channel between the browser (where the click happened) and the freshly installed app. The app store sits in between, and neither Apple nor Google passes custom link data through the install process. Deferred deep linking has to reconstruct that connection through indirect means.
How Deferred Deep Linking Works
The full flow has five stages:
Stage 1: Click Capture. A user taps a deferred deep link. The link points to a server (not directly to the app store). The server logs the click along with everything it can observe about the device: IP address, user agent string, screen dimensions, OS version, language, timezone, and a timestamp. It also stores the intended destination and any payload data attached to the link.
Stage 2: Redirect. After recording the click, the server redirects the user to the appropriate app store. On iOS, that means the App Store. On Android, the Google Play Store. The redirect is fast; the user barely notices the intermediate step.
Stage 3: Install. The user installs the app. This step is entirely outside the linking service's control. It might take seconds or days. Most services set a time window (typically 1 to 24 hours) during which a match is still considered valid.
Stage 4: First Open and Matching. The user opens the app for the first time. The app's SDK sends device information to the deferred deep linking server: IP address, user agent, device model, OS version, screen size, locale, and timezone. The server compares this data against recent click records, looking for a match.
Stage 5: Link Delivery. If a match is found, the server returns the original link data (destination path, parameters, metadata) to the SDK. The SDK passes this to the app, which navigates the user to the correct screen and applies any context (like a referral bonus, a discount code, or a specific piece of content).
The entire mechanism hinges on stage 4: matching the click to the first open. That's where the technical complexity lives.
Matching Techniques
There are three broad approaches to matching a click to a first app open. Most production systems use a combination.
Probabilistic Matching (Device Fingerprinting)
Probabilistic matching builds a fingerprint from observable device attributes collected during the click and again on first open. These attributes include:
- IP address: The strongest signal. If the user clicks and opens the app from the same Wi-Fi network, the IP will usually match. Mobile carrier IPs are less reliable because they change more often and are shared across users via Carrier-Grade NAT (CGNAT).
- User agent: Contains the OS version, device model, browser engine, and browser version. On its own it's not unique, but combined with other signals it narrows the match.
- Screen resolution: Helps distinguish device models, though many devices share common resolutions.
- Language and locale: Useful as a disambiguator when multiple clicks come from the same IP.
- Timezone: Another weaker signal that adds to the fingerprint's specificity.
The server hashes or indexes these attributes and compares click records against the first-open data. A match is accepted if the combined signals cross a confidence threshold.
Accuracy: Probabilistic matching typically achieves 70-85% match rates in real-world conditions. It degrades on shared networks (office Wi-Fi, university campuses), behind VPNs, and when there's a long delay between click and install. It can also produce false positives when multiple users on the same network install the same app around the same time.
Deterministic Matching
Deterministic matching uses a stable, unique identifier that persists across the click and the app open. The main options:
- Apple's SKAdNetwork and AdAttributionKit: These are Apple's privacy-preserving attribution APIs. They confirm that an install came from a specific ad, but they return limited, aggregated data with time delays. They do not pass through arbitrary link payloads.
- Google Play Install Referrer: Android's Install Referrer API (see also Google Play Install Referrer guide) lets you attach a referrer string to the Play Store URL. When the app opens, it can read this referrer string via the InstallReferrerClient. This is deterministic and reliable on Android.
- Clipboard-based matching (deprecated on iOS): Historically, some services wrote a unique token to the device clipboard when the user clicked the link in Safari, then read the clipboard on first app open (see clipboard-based deferred linking). Apple deprecated this approach starting in iOS 16 by requiring user permission before apps can read the clipboard, and further restricted it in subsequent versions. This method is no longer practical on iOS.
The Google Play Install Referrer is the most reliable deterministic method available. On iOS, there is no equivalent that passes custom link data deterministically through the install.
Hybrid Approaches
Most deferred deep linking implementations combine multiple techniques:
- On Android, use the Install Referrer API as the primary source. Fall back to fingerprinting if the referrer is missing or empty.
- On iOS, use fingerprinting as the primary method. Supplement with Apple's attribution APIs for ad campaign measurement (though these won't carry custom payload data).
- Apply time-windowing: only consider click records from the last N hours. Shorter windows reduce false positives.
- Use IP-plus-user-agent as the strongest fingerprint combination. Add screen resolution and timezone as tiebreakers when multiple candidates exist.
Building It Yourself
Let's look at the core server-side logic for a basic deferred deep linking service. This is simplified, but it illustrates the moving parts.
Click Endpoint (Node.js/Express)
When a user clicks a deferred deep link, the server records the device fingerprint and redirects to the app store:
// POST /click/:linkId or GET /link/:linkId
app.get('/link/:linkId', async (req, res) => {
const link = await db.getLink(req.params.linkId);
if (!link) return res.status(404).send('Link not found');
const fingerprint = {
ip: req.headers['x-forwarded-for'] || req.socket.remoteAddress,
userAgent: req.headers['user-agent'],
language: req.headers['accept-language'],
screenWidth: req.query.sw || null,
screenHeight: req.query.sh || null,
timezone: req.query.tz || null,
};
// Store the click record with a TTL (e.g., 24 hours)
await db.storeClick({
linkId: link.id,
fingerprint,
destination: link.destination, // e.g., "/product/123"
payload: link.payload, // e.g., { referralCode: "abc" }
createdAt: Date.now(),
expiresAt: Date.now() + 24 * 60 * 60 * 1000,
});
// Redirect to the appropriate app store
const ua = req.headers['user-agent'] || '';
if (/iPhone|iPad|iPod/.test(ua)) {
res.redirect(link.appStoreUrl);
} else if (/Android/.test(ua)) {
// Append referrer for Play Install Referrer API
const referrer = encodeURIComponent(`linkId=${link.id}`);
res.redirect(`${link.playStoreUrl}&referrer=${referrer}`);
} else {
res.redirect(link.webFallbackUrl);
}
});
Notice how the Android redirect includes a referrer parameter. The Play Store preserves this value and makes it available to the app through the Install Referrer API.
Match Endpoint (Node.js/Express)
When the app opens for the first time, the SDK calls the match endpoint:
// POST /match
app.post('/match', async (req, res) => {
const { ip, userAgent, screenWidth, screenHeight,
timezone, installReferrer, platform } = req.body;
// Android: try deterministic match first
if (platform === 'android' && installReferrer) {
const params = new URLSearchParams(installReferrer);
const linkId = params.get('linkId');
if (linkId) {
const click = await db.getClickByLinkId(linkId);
if (click && click.expiresAt > Date.now()) {
await db.markClickMatched(click.id);
return res.json({
matched: true,
destination: click.destination,
payload: click.payload,
});
}
}
}
// Probabilistic fallback: fingerprint matching
const candidates = await db.getRecentClicks({
ip,
maxAge: 24 * 60 * 60 * 1000,
});
let bestMatch = null;
let bestScore = 0;
for (const click of candidates) {
let score = 0;
const fp = click.fingerprint;
if (fp.ip === ip) score += 50;
if (fp.userAgent === userAgent) score += 30;
if (fp.screenWidth === screenWidth
&& fp.screenHeight === screenHeight) score += 10;
if (fp.timezone === timezone) score += 5;
if (fp.language?.split(',')[0] === req.body.language?.split(',')[0]) {
score += 5;
}
if (score > bestScore) {
bestScore = score;
bestMatch = click;
}
}
const MATCH_THRESHOLD = 75;
if (bestMatch && bestScore >= MATCH_THRESHOLD) {
await db.markClickMatched(bestMatch.id);
return res.json({
matched: true,
destination: bestMatch.destination,
payload: bestMatch.payload,
confidence: bestScore,
});
}
res.json({ matched: false });
});
This is the core logic. A production system would add rate limiting, request signing (to prevent spoofed match requests), expiry cleanup, and more granular scoring.
Collecting the Fingerprint on Click (JavaScript)
To improve match accuracy, the click page can collect additional device signals via JavaScript before redirecting:
// Runs on the click landing page before redirect
function collectFingerprint() {
return {
sw: window.screen.width,
sh: window.screen.height,
tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
lang: navigator.language,
dpr: window.devicePixelRatio,
platform: navigator.platform,
};
}
// Append fingerprint data to the redirect URL as query params
const fp = collectFingerprint();
const params = new URLSearchParams(fp).toString();
window.location.href = `/link/${linkId}?${params}`;
More signals mean better accuracy, but every additional signal also increases the risk of mismatches when values change between the click and the install (for example, if the user moves to a different network).
Privacy Considerations
Deferred deep linking inherently involves device identification, which puts it in tension with platform privacy policies and regulations.
iOS: ATT and IDFA
Apple's App Tracking Transparency (ATT) framework, introduced in iOS 14.5, requires apps to ask for user permission before accessing the IDFA (Identifier for Advertisers). Most users decline. This effectively killed IDFA-based deterministic matching for deferred deep links.
Fingerprinting itself is against Apple's User Privacy and Data Use policy, which states that apps may not "derive data from a device for the purpose of uniquely identifying it." Apple has increasingly enforced this rule. However, using IP address and user agent for first-party attribution (matching your own link clicks to your own app opens) occupies a gray area that many apps still rely on. The key distinction Apple draws is between cross-app tracking (identifying users across different companies' apps) and first-party attribution (measuring your own marketing).
Android: Privacy Sandbox and Ad ID
Google's Privacy Sandbox for Android is gradually restricting access to the Advertising ID (formerly GAID). The Attribution Reporting API is Google's replacement for cross-app attribution, providing aggregated results rather than individual-level data.
For deferred deep linking specifically, the Play Install Referrer remains the recommended approach. It doesn't require any advertising identifier, and it's not affected by Privacy Sandbox restrictions because it tracks the install referral (a first-party relationship between the developer and the store) rather than cross-app user behavior.
GDPR and Data Retention
Under GDPR, IP addresses are personal data. If you're implementing deferred deep linking, you need to:
- Store click records for the minimum time necessary (24 hours or less is typical).
- Delete or anonymize click records after the match window expires.
- Include deferred deep linking in your app's privacy policy and data processing disclosures.
- Ensure the legal basis for processing is documented (legitimate interest for first-party attribution is a common approach).
Short TTLs on click records help here. There's rarely a reason to keep unmatched click data beyond 48 hours.
Platform-Specific Behavior
iOS Quirks
Safari's Intelligent Tracking Prevention (ITP): Safari aggressively limits cookies and tracking on iOS. This doesn't directly affect the fingerprinting approach (which doesn't use cookies), but it does mean any cookie-based matching strategy will fail.
Clipboard restrictions: Starting in iOS 16, apps that read the clipboard trigger a visible system prompt asking the user to confirm. In iOS 17 and later, Apple further tightened this by preventing silent reads entirely. Clipboard-based deferred deep linking is dead on iOS.
Universal Links behavior on first install: When a user installs your app and then taps a Universal Link, iOS may open it in Safari instead of the app if the apple-app-site-association (AASA) file hasn't been cached yet. The AASA file is typically fetched by the OS shortly after install, but there can be a brief window where Universal Links don't work. This is a separate issue from deferred deep linking, but it affects the user experience around install time.
SFSafariViewController and ASWebAuthenticationSession: These in-app browsers share cookies with Safari, which can matter if your click page sets a cookie as a backup identification method. However, link clicks from apps like Instagram, Twitter, and Facebook typically use their own in-app browsers, which do not share Safari's cookie jar.
Android Quirks
Play Install Referrer reliability: The Install Referrer API works well, but the referrer string has a 1024-byte limit. If you need to pass complex payload data, store it server-side and pass only a short identifier through the referrer.
Chrome Custom Tabs vs. WebView: Links opened in Chrome Custom Tabs share the browser's cookie jar. Links opened in a WebView (common in apps like Facebook) do not. This affects cookie-based approaches but not fingerprinting.
Intent filters and verified links: Android App Links verification uses a assetlinks.json file. After install, the system verifies the association asynchronously. There's usually no meaningful delay, but in rare cases (slow networks, server issues), verification can be postponed.
Multiple browsers: Android users might click a link in Chrome but have a different default browser. The user agent collected at click time might differ from what the SDK reports. Fingerprinting implementations need to account for this by weighting user agent matching lower when the browser component differs but the device model and OS version match.
Deferred Deep Linking with SDKs
Building deferred deep linking from scratch is doable but maintenance-heavy. The matching logic, platform-specific workarounds, privacy compliance updates, and edge case handling add up quickly. SDKs handle these concerns so you can focus on what happens when the link data arrives.
Here's how you would handle deferred deep links using Tolinku's SDKs.
iOS (Swift)
import TolinkuSDK
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
Tolinku.configure(appKey: "tolk_pub_your_key")
// Handle deferred deep links on first open
Tolinku.checkDeferredLink { result in
switch result {
case .success(let linkData):
guard let linkData = linkData else {
// No pending deferred link
return
}
// Navigate to the destination
self.handleDeepLink(
path: linkData.destination,
params: linkData.payload
)
case .failure(let error):
print("Deferred link check failed: \(error)")
}
}
return true
}
func handleDeepLink(path: String, params: [String: Any]) {
// Route to the correct screen based on path
// e.g., "/product/123" -> show product with ID 123
let router = DeepLinkRouter.shared
router.navigate(to: path, with: params)
}
}
Android (Kotlin)
import com.tolinku.sdk.Tolinku
import com.tolinku.sdk.DeferredLinkResult
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Tolinku.configure(this, "tolk_pub_your_key")
// Check for deferred deep links
Tolinku.checkDeferredLink { result ->
when (result) {
is DeferredLinkResult.Found -> {
val destination = result.linkData.destination
val payload = result.linkData.payload
handleDeepLink(destination, payload)
}
is DeferredLinkResult.NotFound -> {
// No pending deferred link
}
is DeferredLinkResult.Error -> {
Log.e("Tolinku", "Deferred link error", result.exception)
}
}
}
}
private fun handleDeepLink(path: String, payload: Map<String, Any>) {
// Route to the correct screen
// e.g., "/product/123" -> ProductActivity
val router = DeepLinkRouter(this)
router.navigate(path, payload)
}
}
Both SDKs handle the fingerprint collection, server communication, Install Referrer (on Android), and match retrieval automatically. The callback fires once with the result, and you only need to handle the routing.
For more details on how Tolinku handles deferred deep linking, including configuration options and attribution behavior, see the documentation.
Measuring Success
Match Rates
Your deferred deep link match rate is the percentage of first opens that successfully return the original link data. Track this metric closely. Healthy match rates look like:
- Android with Install Referrer: 95%+ (deterministic matching is very reliable).
- iOS with fingerprinting only: 60-80%, depending on your user base and the time between click and install.
- Blended (iOS + Android): 75-90% overall.
If your match rates drop below these benchmarks, investigate your attribution window, your fingerprint scoring, and whether network conditions are causing IP mismatches.
Attribution Windows
The time window between click and first open matters. Most installs happen within the first hour after a click. After 24 hours, match accuracy drops sharply because IP addresses change, and the probability of collisions increases.
Recommended windows:
- 1 hour: Highest accuracy, but misses users who install later.
- 24 hours: Good balance. This is the industry standard.
- 7 days: Catches more installs but with significantly lower match confidence. Only use this if you're willing to accept more false positives.
Debugging Failed Matches
When a deferred deep link doesn't match, check these things in order:
- Was the click recorded? Verify the click endpoint received and stored the request. If clicks aren't being recorded, the redirect might be happening too fast (before the server responds), or the URL might be malformed.
- Did the SDK call the match endpoint? Check server logs for the match request. If it's missing, the SDK might not be initialized properly, or the network request might be failing silently.
- Did the fingerprints overlap? Compare the click fingerprint with the match request data. If the IP changed (user switched from Wi-Fi to cellular), the strongest matching signal is gone.
- Did the match window expire? If the user installed the app days after clicking, the click record may have already been cleaned up.
- Was there a competing click? If multiple users on the same network clicked the link around the same time, the wrong one might have been matched (or neither, if your system conservatively rejects ambiguous matches).
Common Pitfalls and Solutions
Pitfall: Matching fires on every app open, not just the first.
Solution: The SDK should call the match endpoint only once, on the very first launch. Store a flag in local storage (e.g., UserDefaults on iOS, SharedPreferences on Android) indicating that the deferred link check has already been performed.
Pitfall: False positives on shared networks. Multiple people at the same office or university click the same link and install the app. Fingerprints are nearly identical. Solution: Require a minimum match score that includes more than just IP and user agent. Add screen resolution and timezone. If multiple candidates score above the threshold, reject the match rather than guessing.
Pitfall: User switches networks between click and install. The user clicks on Wi-Fi, walks to the train, installs on cellular. The IP address changes completely. Solution: This is a fundamental limitation of probabilistic matching. The user agent and device characteristics still provide partial matching, but accuracy drops. On Android, the Install Referrer API sidesteps this problem entirely.
Pitfall: Redirect chains strip referrer data.
Multiple redirects (ad network to tracker to your click server to the app store) can lose or overwrite the HTTP Referer header and query parameters.
Solution: Capture all necessary data at the first redirect and store it server-side. Pass only a short identifier through the redirect chain.
Pitfall: Click records pile up and slow down matching. If your service handles high volume and you don't expire old records, the match query scans too many rows. Solution: Use a TTL-based data store (Redis with key expiry works well) or run a cleanup job that purges unmatched clicks older than your attribution window.
Pitfall: Forgetting to handle the "no match" case. If the SDK returns no deferred link, the app should proceed normally to the home screen or onboarding flow. Don't block the user experience while waiting for the match response. Solution: Set a reasonable timeout (2-3 seconds) on the match request. If it times out or returns no match, continue with the default first-open experience.
Conclusion
Deferred deep linking solves a real gap in the mobile linking experience. Without it, every user who installs your app from a link loses the context that brought them there. With it, users land on the right screen with the right data on their very first app open.
The underlying mechanics are straightforward: capture device signals on click, store them server-side, collect the same signals on first open, and find the match. For platform-specific details, see deferred deep linking on iOS and deferred deep linking on Android. The complexity comes from platform restrictions (Apple's ATT, clipboard deprecation, Android's Privacy Sandbox), network variability (IP changes, shared networks), and the inherent uncertainty of probabilistic matching.
On Android, the Play Install Referrer API gives you a reliable, deterministic path. On iOS, fingerprinting remains the primary approach, with all its accuracy tradeoffs.
If you're evaluating whether to build deferred deep linking yourself or use a platform like Tolinku, consider the maintenance burden. The initial implementation is manageable, but keeping up with OS changes, privacy regulations, and edge cases across two platforms is ongoing work. A dedicated platform handles that for you, so your engineering time goes toward the product features your users actually see.
For further reading, explore deep linking concepts or check out Tolinku's deferred deep linking documentation for implementation details.
Get deep linking tips in your inbox
One email per week. No spam.