Skip to main content

Deferred Deep Linking

Deferred deep linking allows you to route new users to specific content after they install your app, creating a seamless experience from click to conversion.

What is Deferred Deep Linking?

Regular deep linking only works for users who already have your app installed.

Deferred deep linking works for new users who don't have your app yet:

  1. User clicks link to product page
  2. User doesn't have app → Sent to App Store/Google Play
  3. User installs app
  4. User opens app for first time
  5. App automatically navigates to the original product page

The "deep link" is deferred until after install.

Why It Matters

Without Deferred Deep Linking

User clicks: Instagram ad for wireless headphones

Flow:

  1. Click ad → App Store
  2. Install app
  3. Open app → Generic home screen
  4. User has to search for headphones again
  5. 60-70% drop-off - user forgets or gives up

With Deferred Deep Linking

User clicks: Same Instagram ad

Flow:

  1. Click ad → App Store
  2. Install app
  3. Open app → Automatically shows wireless headphones
  4. User adds to cart immediately
  5. 15-25% drop-off - seamless experience

Impact: 3-5x higher conversion rate from click to purchase.

How It Works

Create a link with deep link parameters:

curl -X POST https://api.linkforty.com/api/links \
-H "Authorization: Bearer $LINKFORTY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"templateId": "template_123",
"originalUrl": "https://example.com/products/123?productId=123&category=electronics",
"iosUrl": "https://apps.apple.com/app/id123456789",
"androidUrl": "https://play.google.com/store/apps/details?id=com.yourapp",
"attributionWindowHours": 168
}'

Generated: https://lnk.forty.com/abc123

One link can serve many destinations: append query parameters when you share it and they reach the app after install, so you do not need a separate link per item. See URL Parameter Passthrough.

When user clicks, LinkForty:

  1. Detects device (iOS, Android, or Web)
  2. Creates fingerprint from:
    • IP address
    • User-agent
    • Screen resolution
    • Timezone
    • Language
  3. Stores fingerprint with link data and timestamp
  4. Redirects to App Store (iOS) or Google Play (Android)

3. User Installs App

User downloads and installs from store.

Important: Attribution window starts when user clicks link, not when they install.

4. User Opens App

On first launch, the SDK reports the install and invokes your deferred deep link handler:

import LinkForty from '@linkforty/mobile-sdk-react-native';

LinkForty.onDeferredDeepLink((deepLinkData) => {
// deepLinkData is the attributed link's data, or null for organic installs
});

5. LinkForty Matches Install

SDK:

  1. Generates fingerprint from same device signals
  2. Sends to LinkForty API
  3. Searches for matching click within attribution window
  4. Returns deep link data if match found
// Returns:
{
productId: "123",
category: "electronics",
utm_source: "instagram",
utm_campaign: "spring-sale"
}

6. App Navigates

Your app uses deep link data to navigate:

if (deepLinkData && deepLinkData.productId) {
navigation.navigate('Product', { id: deepLinkData.productId });
}

User sees the original product page they clicked on!

Fingerprint Matching

What is Fingerprinting?

Fingerprinting creates a unique identifier from device characteristics without requiring:

  • Device IDs (IDFA, GAID)
  • User accounts
  • Cookies
  • App Tracking Transparency permission (iOS)

Fingerprint Components

LinkForty uses these signals:

SignalExampleStability
IP Address192.168.1.1High
User-AgentiPhone14,2; iOS 17.1Very High
Screen Resolution1170x2532Very High
TimezoneAmerica/New_YorkHigh
Languageen-USHigh
PlatformiOSVery High

Combined, these create a fingerprint like:

fp_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

Match Accuracy

Expected accuracy: 70-80%

ScenarioMatch Rate
Same WiFi, same device, under 24h90-95%
Same cellular, same device, under 7 days75-85%
Different network (WiFi → cellular)60-70%
VPN or proxy in between30-50%
Over 14 days between click and install50-60%

Why Matches Fail

Common reasons for no match:

  1. Network changed - Clicked on WiFi, installed on cellular
  2. VPN/proxy used - IP address changes
  3. Attribution window expired - Installed too long after click
  4. Different browser - In-app browser vs Safari (iOS)
  5. iOS Private Relay - Hides IP address
  6. Multiple devices - Clicked on iPad, installed on iPhone

Improving Match Rate

1. Longer attribution windows

  • 7 days: 70-75% match rate
  • 14 days: 75-80% match rate
  • 30 days: 65-70% match rate (more false positives)

2. Platform-specific links

  • Create separate links for iOS and Android
  • Reduces ambiguity in matching

3. Direct traffic

  • Social media → Good match rates (in-app browsers)
  • SMS/iMessage → Excellent match rates (same device)
  • Email → Variable (different devices)

Implementation

Add deep link parameters to your link URLs:

# Product link
curl -X POST https://api.linkforty.com/api/links \
-H "Authorization: Bearer $LINKFORTY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"templateId": "template_123",
"originalUrl": "https://example.com/products/headphones?productId=456&category=audio",
"iosUrl": "https://apps.apple.com/app/id123",
"androidUrl": "https://play.google.com/store/apps/details?id=com.app"
}'

Deep link parameters: Any query parameter in originalUrl becomes available in app.

Step 2: Integrate SDK

Install and initialize the SDK:

import LinkForty from '@linkforty/mobile-sdk-react-native';

// Initialize on app launch
LinkForty.init({
baseUrl: 'https://go.yourdomain.com',
appToken: 'at_your_app_token', // recommended for Cloud
});

See SDK Integration Guide for full setup.

On first app launch:

import { useEffect } from 'react';
import LinkForty from '@linkforty/mobile-sdk-react-native';
import { useNavigation } from '@react-navigation/native';

function App() {
const navigation = useNavigation();

useEffect(() => {
// Fires on a new install attributed to a link; null for organic installs
LinkForty.onDeferredDeepLink((data) => {
if (data) {
console.log('Deep link data:', data);
handleDeepLink(data);
}
});
}, []);

const handleDeepLink = (data) => {
const params = data.customParameters ?? {};
if (params.productId) {
navigation.navigate('Product', { id: params.productId });
} else if (params.category) {
navigation.navigate('Category', { slug: params.category });
} else if (data.deepLinkPath) {
// Route using the link's deep link path
navigation.navigate(data.deepLinkPath);
}
};

return (
// Your app
);
}

Step 4: Test

Test flow:

  1. Create test link with query parameters
  2. Copy link to notes app on test device
  3. Uninstall app from device
  4. Click link from notes
  5. Install app from store
  6. Open app
  7. Check if navigated to correct content

Expected logs:

[LinkForty] Checking for deep link...
[LinkForty] Match found!
[LinkForty] Deep link data: {
productId: "456",
category: "audio",
utm_source: "test"
}
[App] Navigating to product 456

URL Parameter Passthrough

You do not need a separate link for every piece of content. Add query parameters to one link when you share it, and they reach the app after the install.

https://go.example.com/content?slug=titanic
https://go.example.com/content?slug=spiderman-homecoming

Both are the same link. The value you appended arrives in customParameters, whether the app was just installed or was already on the device:

function openContent(data) {
const slug = data?.customParameters?.slug;
if (slug) {
navigation.navigate('Content', { slug });
}
}

// New install, matched back to the click
LinkForty.onDeferredDeepLink(openContent);

// App already installed — the OS hands the link straight to the app
LinkForty.onDeepLink((url, data) => openContent(data));

The two entry points are different moments, but the value is read the same way in both. You do not need to parse the URL yourself.

On a direct open the SDK merges the parameters from the tapped URL into the payload it resolves for the link. If a name appears in both, the URL wins — see Precedence.

SDK Versions

Delivering URL parameters on a direct open requires:

SDKMinimum version
React Native1.6.1
Expo1.6.1
iOS1.4.1
Android1.3.1
Flutter0.2.1

Earlier versions still receive parameters on the deferred path. On a direct open they fall back to the link's stored configuration, so customParameters will not carry what was appended to the URL.

Reserved Parameters

Some names are consumed by LinkForty and never reach your app, because they already do a job. Matching is case-insensitive, and the first two are matched by prefix — every name starting with utm_ or fp_ is reserved, not just the ones listed here:

NameUsed for
utm_* — e.g. utm_source, utm_medium, utm_campaign, utm_term, utm_contentcampaign reporting — see Campaigns
fp_* — e.g. fp_tz, fp_lang, fp_sw, fp_sh, fp_platform, fp_pvthe device fingerprint used to match the install
lf_click (exact)the click id LinkForty adds to your destination URL

Anything else is passed through. Avoid the utm_ and fp_ prefixes for your own values.

Limits

  • 16 parameters per link. Beyond that, the extras are dropped.
  • 256 characters per value. Longer values are silently truncated, not rejected — the link still works and the click is still recorded, so check the length yourself if a value might be long.

Precedence

A parameter on the URL beats one configured on the link itself. A link set up with slug=default that is shared as ?slug=titanic delivers titanic — what the sharer put on the URL is more specific than the link's stored configuration.

What Does Not Come Through

  • Bot traffic. Clicks classified as bots record no parameters at all.
  • Values added after the click. Parameters are captured at click time. Changing the URL later does not change what an earlier click stored.
  • The fragment. Anything after # never reaches the server.
Validate before you route

These values come from a public URL and anyone can set them to anything. Treat them as untrusted input: check that a value is one you expect before routing on it, and never pass one straight into a URL loader, a database query, or a privileged navigation.

Use Cases

Link: Instagram ad for wireless headphones

{
"originalUrl": "https://shop.example.com/products/wireless-headphones?productId=789",
"iosUrl": "https://apps.apple.com/app/id123",
"androidUrl": "https://play.google.com/store/apps/details?id=com.shop"
}

App flow:

const data = await LinkForty.getInstallData();
// { productId: "789" }

navigation.navigate('Product', { id: data.productId });

User sees: Wireless headphones product page immediately after install.

Link: Email with article link

{
"originalUrl": "https://blog.example.com/articles/how-to-cook-pasta?articleId=123&section=recipes",
"iosUrl": "https://apps.apple.com/app/id456",
"androidUrl": "https://play.google.com/store/apps/details?id=com.blog"
}

App flow:

const data = await LinkForty.getInstallData();
// { articleId: "123", section: "recipes" }

navigation.navigate('Article', { id: data.articleId });

User sees: Article about cooking pasta.

Referral Programs

Link: Friend's referral link

{
"originalUrl": "https://example.com/referral/john-doe?referrerId=user_456&reward=10",
"iosUrl": "https://apps.apple.com/app/id789",
"androidUrl": "https://play.google.com/store/apps/details?id=com.app"
}

App flow:

const data = await LinkForty.getInstallData();
// { referrerId: "user_456", reward: "10" }

// Award referral credit
await awardReferralCredit(data.referrerId, data.reward);

// Show success message
showToast(`You received $${data.reward} credit from ${getReferrerName(data.referrerId)}!`);

User sees: Welcome message with referral credit.

Promotional Campaigns

Link: Limited-time offer

{
"originalUrl": "https://example.com/promo/spring-sale?couponCode=SPRING20&discount=20",
"iosUrl": "https://apps.apple.com/app/id123",
"androidUrl": "https://play.google.com/store/apps/details?id=com.app"
}

App flow:

const data = await LinkForty.getInstallData();
// { couponCode: "SPRING20", discount: "20" }

// Auto-apply coupon
await applyCoupon(data.couponCode);

// Navigate to sale
navigation.navigate('Sale', { discount: data.discount });

User sees: Sale page with coupon already applied.

Advanced Techniques

Multi-Step Onboarding

Defer deep link until after onboarding:

const [onboardingComplete, setOnboardingComplete] = useState(false);
const [pendingDeepLink, setPendingDeepLink] = useState(null);

useEffect(() => {
checkDeepLink();
}, []);

const checkDeepLink = async () => {
const data = await LinkForty.getInstallData();
if (data) {
setPendingDeepLink(data);
}
};

const handleOnboardingComplete = () => {
setOnboardingComplete(true);

if (pendingDeepLink) {
handleDeepLink(pendingDeepLink);
}
};

Conditional Navigation

Navigate based on user state:

const handleDeepLink = async (data) => {
const user = await getCurrentUser();

if (!user) {
// Not logged in - show login, then navigate
navigation.navigate('Login', {
redirectTo: 'Product',
redirectParams: { id: data.productId }
});
} else {
// Logged in - navigate directly
navigation.navigate('Product', { id: data.productId });
}
};

Handle missing or invalid data:

const handleDeepLink = async (data) => {
if (data.productId) {
// Validate product exists
const product = await api.getProduct(data.productId);

if (product) {
navigation.navigate('Product', { id: data.productId });
} else {
// Invalid product - fallback to category
if (data.category) {
navigation.navigate('Category', { slug: data.category });
} else {
// No valid destination - go to home
navigation.navigate('Home');
}
}
}
};

Track deep link usage:

const handleDeepLink = async (data) => {
// Track deep link event
await LinkForty.trackEvent({
eventName: 'deep_link_opened',
properties: {
productId: data.productId,
utm_source: data.utm_source,
utm_campaign: data.utm_campaign,
hasAccount: !!user
}
});

// Navigate
navigation.navigate('Product', { id: data.productId });
};

Troubleshooting

Symptom: the deferred deep link handler fires with null (or getInstallData() returns null) despite the user clicking the link.

Debug steps:

  1. Check attribution window:

    # View link details
    curl https://api.linkforty.com/api/links/abc123 \
    -H "Authorization: Bearer $LINKFORTY_API_KEY"

    # Check attribution_window_hours value
  2. Enable debug logging:

    LinkForty.init({
    apiKey: API_KEY,
    debug: true
    });
  3. Check click was recorded:

    • Go to Analytics → Links → Select link
    • Verify click appears in list
    • Note timestamp
  4. Check install timing:

    • Compare click timestamp to install time
    • If >attribution window → No match expected
  5. Verify network conditions:

    • Same WiFi/cellular for click and install?
    • VPN enabled?
    • iOS Private Relay active?

Low Match Rates

Expected: 70-80% for normal conditions

If lower (e.g., 40-50%):

  1. Increase attribution window:

    • Try 14 days instead of 7
    • May capture more delayed installs
  2. Check traffic source:

    • In-app browsers → Good (70-80%)
    • Web browsers → Lower (60-70%)
    • Email links → Variable (different devices)
  3. Platform-specific issues:

    • iOS 14+ privacy features reduce accuracy
    • Android: Check Google Play Install Referrer integration
  4. Geographic factors:

    • Users in regions with unstable IPs
    • Frequent VPN usage

Incorrect Content Loaded

Symptom: Deep link data has wrong product ID.

Possible causes:

  1. False positive match:

    • User clicked different link before
    • Fingerprints happened to match
    • Solution: Shorter attribution window
  2. Query parameter typo:

    • Check link URL has correct parameters
    • Verify parameter names match app code
  3. Cached data:

    • Old deep link data cached
    • Solution: Clear app data and retest

Best Practices

Include useful data in every link:

Good:

originalUrl: "https://example.com/product?productId=123&category=electronics"

Bad:

originalUrl: "https://example.com/product"

2. Use Meaningful Parameter Names

Be explicit:

Good:

?productId=123&categorySlug=electronics

Bad:

?id=123&cat=elec

3. Handle Missing Parameters

Always provide fallbacks:

const data = await LinkForty.getInstallData();

if (data?.productId) {
navigation.navigate('Product', { id: data.productId });
} else if (data?.category) {
navigation.navigate('Category', { slug: data.category });
} else {
navigation.navigate('Home');
}

4. Test on Real Devices

Fingerprint matching doesn't work in simulators. Always test on physical devices.

5. Set Appropriate Attribution Windows

Match window to expected user behavior:

  • Impulse products: 1-3 days
  • Considered purchases: 7-14 days
  • Referrals: 30-90 days

Monitor conversion rates:

// Track when deep link opens
await LinkForty.trackEvent({
eventName: 'deep_link_opened',
properties: { productId: data.productId }
});

// Track if user converts
await LinkForty.trackEvent({
eventName: 'purchase',
value: 29.99,
properties: { productId: data.productId }
});

View in dashboard: See conversion rate from deep link open → purchase.

Next Steps

Further Reading