Skip to content
Tolinku
Tolinku
Sign In Start Free
Use Cases · · 5 min read

Onboarding and Deferred Deep Linking: The Power Combo

By Tolinku Staff
|
Tolinku fintech deep linking dashboard screenshot for use cases blog posts

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

  1. User sees a link (ad, social post, email, referral)
  2. User taps the link
  3. The deep linking platform records the click data (device info, IP, timestamp, parameters)
  4. User is redirected to the app store
  5. User installs the app
  6. On first launch, the SDK contacts the server
  7. The server matches the device to the original click
  8. The deep link data is returned to the app
  9. 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

// 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,
});
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.

Ready to add deep linking to your app?

Set up Universal Links, App Links, deferred deep linking, and analytics in minutes. Free to start.