Deferred Deep Links
Preserve a link's destination through App Store and Play Store installs, then route the user on first launch, in React Native.
Deferred deep links let you route users to specific content even when they don't have your app installed. The user clicks a link, installs from the App Store or Play Store, and on first launch the SDK matches them back to the original link.
How It Works
- User taps a WarpLink URL in a browser
- WarpLink records the click. The server derives the IP and stores it with the normalized language and timezone. Clicks that share a fingerprint are kept as a list, newest first, up to 10, so a second click never overwrites the first
- User is redirected to the App Store (iOS) or Play Store (Android)
- User installs and opens the app
- SDK detects first launch. A completion marker is written only after the check completes. It is scoped to one install on both platforms, and no backup restores it, so a reinstall attributes fresh
- SDK collects device signals (preferred language, timezone name and offset, and platform IDs) and sends them to the attribution API. The server derives the IP and computes the fingerprint
- Server matches against stored click data
- Deep link returned with
isDeferred: true
Automatic Check
The deferred check fires automatically from configure(). The result arrives in your onLink callback, where isDeferred is true:
import { WarpLink } from '@warplink/react-native';
WarpLink.configure({
apiKey: 'wl_live_yoursdkkeyhere000000000000000000',
onLink: ({ deepLink, error }) => {
if (error || !deepLink) return;
if (deepLink.isDeferred) {
const confidence = deepLink.matchConfidence ?? 0;
if (confidence > 0.5) navigateTo(deepLink.deepLinkUrl ?? deepLink.destination);
} else {
navigateTo(deepLink.destination); // a tap, not a deferred install
}
},
});
Manual Check (Advanced)
To run the check yourself, set automaticDeferredDeepLinks: false and call checkDeferredDeepLink():
import { useEffect, useState } from 'react';
import { WarpLink, type WarpLinkDeepLink } from '@warplink/react-native';
function App() {
const [isReady, setIsReady] = useState(false);
useEffect(() => {
WarpLink.checkDeferredDeepLink()
.then((link) => {
if (link?.isDeferred) {
navigateTo(link.deepLinkUrl ?? link.destination);
}
})
.catch((error) => {
console.warn('Deferred deep link check failed:', error.message);
})
.finally(() => {
setIsReady(true);
});
}, []);
if (!isReady) return <SplashScreen />;
return <>{/* Your app */}</>;
}
Confidence-Based Routing
const link = await WarpLink.checkDeferredDeepLink();
if (!link?.isDeferred) return;
const confidence = link.matchConfidence ?? 0;
if (confidence > 0.5) {
// High confidence — navigate directly
const productId = link.customParams['product_id'] as string | undefined;
if (productId) {
navigation.navigate('Product', { id: productId });
} else {
navigation.navigate('WebView', { url: link.destination });
}
} else if (confidence > 0.3) {
// Medium confidence — show suggestion
navigation.navigate('Suggestion', {
message: 'Were you looking for this?',
url: link.destination,
});
}
// Below 0.3 — ignore, show default onboarding
Confidence Scores
Probabilistic scores depend on the fingerprint variant. enriched_tz hashes the IANA timezone name and is what an SDK that collects a zone name sends. enriched hashes the minute offset instead and stays as the fallback for older SDKs. basic drops the timezone entirely.
| Scenario | Confidence | Match Type |
|---|---|---|
| IDFV re-engagement (iOS) | 1.0 | deterministic |
| Play Install Referrer (Android) | 1.0 | deterministic |
enriched_tz fingerprint, < 1 hour | 0.85 | probabilistic |
enriched_tz fingerprint, < 3 hours | 0.65 | probabilistic |
enriched_tz fingerprint, < 6 hours | 0.50 | probabilistic |
enriched_tz fingerprint, < 24 hours | 0.30 | probabilistic |
The offset variant scores 0.80 / 0.60 / 0.45 / 0.25 across the same bands, and basic scores 0.70 / 0.50 / 0.35 / 0.20. Two further signals can only reduce the score: more than one distinct link in the fingerprint bucket applies x0.6, and a shared click IP applies x0.6 for carrier-grade NAT or a private address, x0.9 for a household IPv4 address, and x1.0 for IPv6. Past 24 hours there is no match at all.
Both deterministic matches set matchGuaranteed to true on the returned link. Gate anything sensitive, such as auto sign-in or showing personal data, on that flag rather than on a matchConfidence threshold. A probabilistic match is a best guess made from a network-shaped fingerprint and can name the wrong user.
Match Window
The match window is set per link on the server, not in the SDK. Configure it in the dashboard when you create or edit a link. The default is 6 hours and the ceiling is 24 hours, so the 24 hour band above only applies to links configured past the default.
The window is deliberately short. The fingerprint key is a network (IP, language, timezone), not a device, so every extra hour lets another stranger behind the same shared address join the bucket while adding almost no real matches. Links created before the current limits may still carry a longer stored value, but the server caps every window at 24 hours when it reads them.
This governs probabilistic matching only. The IDFV and Play Install Referrer branches are deterministic and are not affected by the window.
Platform Differences
| iOS | Android | |
|---|---|---|
| Deterministic match | IDFV (re-engagement) | Play Install Referrer |
| Completion marker | Backup-excluded file in the app container | File in noBackupFilesDir |
| Device-seen marker | Keychain | SharedPreferences (restored by Auto Backup) |
| ATT required? | No (IDFV is exempt) | N/A |
The completion marker is per install and no backup restores it, so a reinstall re-attributes on both platforms. The device-seen marker outlives an uninstall on both platforms and only sets is_reinstall on the request.
Caching Behavior
- Attribution check happens once per install (first launch only)
- Result cached by the native SDK
- Subsequent calls to
checkDeferredDeepLink()resolve with the cached result, the matched deep link if there was one andnullif there was not, without another network request - A reinstall is a new install, so it gets a new check. See App Reinstall
Edge Cases
Offline First Launch
checkDeferredDeepLink() rejects with E_NETWORK_ERROR. The attempt is not consumed, so the SDK retries the check on the next launch.
Check for connectivity before calling checkDeferredDeepLink() if your first-launch experience depends on it.
App Reinstall
A reinstall is a new install and is attributed again on both platforms. The JavaScript layer does nothing special here: it bridges to the native SDK, which keeps two markers with two different jobs.
- Completion marker. Says the check already ran for this install, and it is gone once the app is. iOS keeps it in a backup-excluded file in the app container, Android in
noBackupFilesDir. Neither comes back from a restore, so a reinstall starts clean - Device-seen marker. Says the device was attributed at some point, and it outlives an uninstall. iOS keeps it in the Keychain, Android in
SharedPreferences, which Auto Backup restores. It gates nothing. Its only job is to setis_reinstall: trueon the attribution request
Both installs count. The dashboard shows installs and reinstalls together as one installs number.
Multiple Links Before Install
Clicks that share a fingerprint are kept as a list, newest first, up to 10. The most recent click your app can claim is the one matched, and the others stay available for the other devices that share the address. When more than one click is claimable, the confidence score is multiplied by 0.6 to report that ambiguity.