Standard deep links break when the app isn't installed. The user clicks a link, lands in the app store, installs the app, and then opens to a generic home screen with no memory of what they originally clicked. Deferred deep linking solves this: it preserves the link data through the install process so the app can pick up where the user left off. Combined with smart onboarding, this creates the experience users expect.
For the mechanics of deferred deep linking, see Deferred Deep Linking: How It Works. For fingerprinting approaches, see Fingerprinting vs Deterministic Matching for Deep Links. For real-world applications, see Deferred Deep Linking Use Cases.
How Deferred Deep Linking Works with Onboarding
The Flow
- User sees a link (ad, social post, email, referral)
- User taps the link
- The deep linking platform records the click data (device info, IP, timestamp, parameters)
- User is redirected to the app store
- User installs the app
- On first launch, the SDK contacts the server
- The server matches the device to the original click
- The deep link data is returned to the app
- The app uses the data to personalize onboarding
// Step 8-9: First launch handler
async function handleFirstLaunch() {
// Check for deferred deep link
const deferred = await Tolinku.checkDeferredLink();
if (deferred) {
// We have context from before install
const context = parseDeepLinkContext(deferred);
return startPersonalizedOnboarding(context);
}
// No match - start standard onboarding
return startStandardOnboarding();
}
function parseDeepLinkContext(deferred) {
return {
path: deferred.path,
referrer: deferred.params.ref,
referrerName: deferred.params.referrer_name,
product: deferred.params.product_id,
category: deferred.params.category,
campaign: deferred.params.utm_campaign,
source: deferred.params.utm_source,
promoCode: deferred.params.promo,
};
}
Match Rate Reality
Deferred deep linking relies on probabilistic matching (fingerprinting). Match rates vary:
| Factor | Impact on Match Rate |
|---|---|
| Time between click and install | < 1 hour: 85-90%, < 24 hours: 70-80%, > 24 hours: 50-60% |
| Network stability | Same network: +10-15%, Network change: -10-15% |
| iOS ATT consent | Allowed: no impact, Denied: -5-10% |
| VPN usage | Reduces match rate by 15-25% |
| Shared IP (office, campus) | Reduces accuracy significantly |
Designing for Partial Match Rates
Since 20-40% of users won't be matched, design onboarding to work well both with and without context:
function OnboardingRouter({ deferredContext }) {
if (deferredContext && deferredContext.confidence === 'high') {
// Full personalization
return <PersonalizedOnboarding context={deferredContext} />;
}
if (deferredContext && deferredContext.confidence === 'low') {
// Partial personalization (use what we have, ask for the rest)
return <PartialPersonalizedOnboarding context={deferredContext} />;
}
// No context - standard flow with manual referral code entry
return <StandardOnboarding showReferralCodeField={true} />;
}
Practical Implementation Patterns
Pattern 1: Referral Through Install
The most common deferred deep linking use case for onboarding. For personalization techniques that build on this pattern, see Personalized Onboarding Flows with Deep Link Data.
async function handleReferralDeepLink(context) {
if (context.referrer) {
// Validate the referrer still exists
const referrer = await validateReferrer(context.referrer);
if (referrer) {
// Store the referral connection
await saveReferralConnection(currentUser.id, referrer.id);
// Personalize onboarding
return {
flow: 'referral',
welcomeMessage: `${referrer.name} invited you!`,
reward: context.reward || '$10',
skipSteps: ['how_did_you_hear'],
};
}
}
return { flow: 'standard' };
}
Pattern 2: Product Context Through Install
User clicked a product link, installed the app, and should see the product:
async function handleProductDeepLink(context) {
if (context.product) {
const product = await getProduct(context.product);
if (product && product.isAvailable) {
return {
flow: 'product_focused',
firstScreen: 'ProductDetail',
productId: product.id,
showSignupAfterView: true,
preSelectedCategory: product.category,
};
}
}
return { flow: 'standard' };
}
Pattern 3: Campaign Context Through Install
User clicked an ad campaign link:
async function handleCampaignDeepLink(context) {
if (context.campaign) {
const campaign = await getCampaignConfig(context.campaign);
return {
flow: 'campaign',
landingPage: campaign.appLanding,
promoCode: context.promoCode,
preSelectedCategory: context.category,
skipSteps: context.category ? ['category_selection'] : [],
};
}
return { flow: 'standard' };
}
Pattern 4: Content Share Through Install
User tapped a shared content link:
async function handleContentShareDeepLink(context) {
if (context.contentId) {
return {
flow: 'content_first',
// Show the content BEFORE asking for signup
firstScreen: 'ContentPreview',
contentId: context.contentId,
sharedBy: context.sharerName,
// Defer signup to after content viewing
signupTrigger: 'after_content_view',
};
}
return { flow: 'standard' };
}
Timing the Deferred Check
When to Call checkDeferredLink()
// Option 1: On app launch (recommended)
useEffect(() => {
async function init() {
const result = await Tolinku.checkDeferredLink();
if (result) {
handleDeferredContext(result);
}
}
init();
}, []);
// Option 2: After splash screen but before onboarding
function SplashScreen() {
useEffect(() => {
async function checkAndRoute() {
// Show splash for minimum 1 second (branding)
const [result] = await Promise.all([
Tolinku.checkDeferredLink(),
delay(1000),
]);
if (result) {
navigation.navigate('PersonalizedOnboarding', { context: result });
} else {
navigation.navigate('StandardOnboarding');
}
}
checkAndRoute();
}, []);
return <SplashAnimation />;
}
Handling Slow Responses
The deferred check requires a network call. Handle delays gracefully:
async function checkDeferredWithTimeout() {
const timeoutPromise = new Promise((resolve) => {
setTimeout(() => resolve(null), 3000); // 3 second timeout
});
const result = await Promise.race([
Tolinku.checkDeferredLink(),
timeoutPromise,
]);
return result;
}
If the network is slow, start standard onboarding. If the deferred result arrives later, you can retroactively apply the context (update the referral connection, show a notification about the promo code, etc.).
Fallback Strategies
Manual Entry
For users where deferred matching fails, provide a way to enter context manually:
function OnboardingWithFallback({ deferredContext }) {
if (deferredContext) {
return <PersonalizedOnboarding context={deferredContext} />;
}
return (
<StandardOnboarding>
<ReferralCodeField
placeholder="Have a referral code? Enter it here"
onSubmit={async (code) => {
const referrer = await lookupReferralCode(code);
if (referrer) {
applyReferralContext(referrer);
}
}}
/>
</StandardOnboarding>
);
}
Clipboard Check
Some platforms check the clipboard for a referral code (with user permission):
async function checkClipboardForCode() {
// Only check if the user explicitly taps "Paste referral code"
const clipboardContent = await Clipboard.getString();
if (clipboardContent && isValidReferralCode(clipboardContent)) {
return clipboardContent;
}
return null;
}
Note: Clipboard access is increasingly restricted on iOS (shows a notification) and Android (requires user gesture). Use this as a convenience feature, not a primary mechanism.
Measuring Deferred + Onboarding
Key Metrics
| Metric | Definition | Target |
|---|---|---|
| Deferred match rate | Matched / Total first launches | 60-80% |
| Personalized completion rate | Completed / Matched users | 65-85% |
| Standard completion rate | Completed / Unmatched users | 40-60% |
| Personalization lift | Personalized rate / Standard rate | 1.3-1.5x |
| Referral attribution rate | Attributed referrals / Total referral clicks | 60-75% |
Attribution Tracking
analytics.track('onboarding_completed', {
flow: 'personalized', // or 'standard'
deferredMatched: true,
matchConfidence: 'high',
contextType: 'referral', // or 'product', 'campaign'
source: context.source,
timeFromClickToInstall: context.clickToInstallMs,
timeFromInstallToOnboard: context.installToOnboardMs,
});
Deferred Link Health Monitoring
async function deferredLinkHealthReport(dateRange) {
const clicks = await countClicks(dateRange);
const installs = await countInstalls(dateRange);
const matched = await countMatchedInstalls(dateRange);
const personalized = await countPersonalizedOnboarding(dateRange);
return {
clickToInstallRate: (installs / clicks * 100).toFixed(1) + '%',
matchRate: (matched / installs * 100).toFixed(1) + '%',
personalizationRate: (personalized / installs * 100).toFixed(1) + '%',
matchedCompletionRate: await getCompletionRate({ matched: true }),
unmatchedCompletionRate: await getCompletionRate({ matched: false }),
};
}
If the match rate drops below 60%, investigate: are users taking longer to install? Are network conditions changing? Is the matching algorithm degrading?
For deferred deep linking, see the deferred deep linking docs. For deep linking features, see Tolinku deep linking. For onboarding use cases, see the onboarding documentation.
Get deep linking tips in your inbox
One email per week. No spam.