Skip to content
Tolinku
Tolinku
Sign In Start Free
Deep Linking · · Updated · 21 min read

The Complete Guide to Deep Linking in 2026

By Tolinku Staff
|
Tolinku deep linking fundamentals dashboard screenshot for deep linking blog posts

Deep links are URLs that send users directly to specific content inside your mobile app. Instead of landing on your app's home screen, users arrive exactly where you want them: a product page, user profile, checkout flow, or any other screen in your app.

This guide covers everything you need to know about implementing deep links in 2026. We'll explore the different types of deep links, walk through implementation on iOS and Android, examine common pitfalls, and share best practices from production apps handling millions of deep link clicks.

What is Deep Linking?

A deep link is a URL that points to a specific location within a mobile app rather than simply launching it. Think of deep links as the mobile equivalent of web URLs. Just as https://example.com/products/shoes takes you to a specific product page on a website, a deep link like myapp://products/shoes or https://example.com/products/shoes can take you directly to that same product inside the mobile app.

Deep linking solves a fundamental problem in mobile apps: without it, every external link to your app dumps users on the same generic home screen. Users then have to navigate through your app to find what they came for, assuming they remember what that was. Many won't bother.

The technical implementation varies by platform. iOS uses Universal Links, Android uses App Links, and both platforms support custom URL schemes. Modern deep linking platforms like Tolinku handle these platform differences automatically, letting you focus on defining your link routes rather than wrestling with configuration files.

There are three main types of deep links, each solving different problems. For a focused breakdown, see types of deep links: standard, deferred, and contextual.

Standard deep links work when the app is already installed. They use either custom URL schemes (myapp://path) or HTTPS links that the operating system recognizes should open in your app.

Custom URL Schemes:

myapp://product/123
myapp://user/johndoe
myapp://settings/notifications

HTTPS Deep Links (Universal Links/App Links):

https://myapp.com/product/123
https://myapp.com/user/johndoe
https://myapp.com/settings/notifications

Standard deep links fail when the app isn't installed. Custom URL schemes show an error. HTTPS links fall back to the web, which might be what you want, or might not.

Deferred deep links remember where the user wanted to go, survive the app installation process, and route them to the correct screen after they open the app for the first time. This solves the "install gap" problem.

Here's how deferred deep linking works:

  1. User clicks a deep link on a device without your app
  2. System redirects to the App Store or Google Play
  3. User installs and opens the app
  4. App retrieves the original deep link destination
  5. User arrives at the intended screen, not the home screen

The magic happens through device fingerprinting or deterministic matching. The deep linking service matches the click before install with the app open after install using device characteristics like IP address, user agent, screen resolution, and timezone.

Contextual deep links carry additional data beyond just the destination. This data persists through the installation process and provides context when the user arrives in the app.

https://myapp.com/invite?referrer=alice&campaign=summer2024&discount=20

Contextual deep links enable powerful use cases:

  • Referral programs with attribution
  • Personalized onboarding flows
  • Campaign-specific promotions
  • A/B testing different user experiences
  • Pre-filling forms with user data

How Deep Linking Works on iOS

iOS supports two deep linking mechanisms: custom URL schemes and Universal Links. Apple strongly recommends Universal Links for production apps.

Custom URL Schemes on iOS

URL schemes are the older approach. You register a custom protocol in your app's Info.plist:

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>myapp</string>
        </array>
    </dict>
</array>

Then handle incoming URLs in your AppDelegate:

func application(_ app: UIApplication, 
                 open url: URL, 
                 options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    // Parse the URL and navigate to the appropriate screen
    if url.scheme == "myapp" {
        let path = url.path
        let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems
        
        // Handle navigation based on path and query parameters
        // Example: myapp://product/123?source=email
        if path.starts(with: "/product/") {
            let productId = String(path.dropFirst("/product/".count))
            navigateToProduct(id: productId)
        }
    }
    return true
}

The problem with URL schemes: any app can claim any scheme. If two apps register myapp://, iOS picks one arbitrarily. There's no verification that the app claiming the scheme is legitimate. For a detailed comparison, see URI schemes vs Universal Links.

Universal Links solve the security and uniqueness problems of URL schemes. They're HTTPS URLs that open in your app when it's installed, or fall back to your website when it's not.

For the full iOS deep dive, see Universal Links: everything you need to know. Setting up Universal Links requires:

  1. An apple-app-site-association (AASA) file hosted on your domain
  2. Associated Domains capability in your app
  3. Code to handle incoming Universal Links

Apple App Site Association file (hosted at https://yourdomain.com/.well-known/apple-app-site-association):

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "TEAMID.com.company.app",
        "paths": [
          "/product/*",
          "/user/*",
          "/invite/*"
        ]
      }
    ]
  }
}

Xcode configuration (Associated Domains):

applinks:yourdomain.com

Swift implementation:

func application(_ application: UIApplication,
                 continue userActivity: NSUserActivity,
                 restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    
    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
          let url = userActivity.webpageURL else {
        return false
    }
    
    // Handle the Universal Link
    handleDeepLink(url: url)
    return true
}

// For iOS 13+ with SceneDelegate
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
          let url = userActivity.webpageURL else {
        return
    }
    
    handleDeepLink(url: url)
}

Apple's Universal Links documentation provides additional implementation details.

The AASA file must be served over HTTPS with a valid certificate, return a 200 status code, and have the correct Content-Type header (application/json). iOS downloads this file when the app is installed and periodically afterwards to check for updates. For setup details, see the AASA file setup guide.

How Deep Linking Works on Android

Android also supports two deep linking mechanisms: custom URL schemes (via Intent Filters) and App Links (Android's equivalent to Universal Links).

Intent Filters on Android

Intent Filters are configured in your AndroidManifest.xml:

<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        
        <!-- Custom URL scheme -->
        <data android:scheme="myapp" />
    </intent-filter>
    
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        
        <!-- HTTPS URLs -->
        <data android:scheme="https" 
              android:host="myapp.com" 
              android:pathPrefix="/product" />
    </intent-filter>
</activity>

Handle incoming intents in your Activity:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    
    handleIntent(intent)
}

override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    handleIntent(intent)
}

private fun handleIntent(intent: Intent) {
    val action = intent.action
    val data = intent.data
    
    if (Intent.ACTION_VIEW == action && data != null) {
        val path = data.path
        val productId = data.getQueryParameter("id")
        
        when {
            path?.startsWith("/product/") == true -> {
                val id = path.removePrefix("/product/")
                navigateToProduct(id)
            }
            path?.startsWith("/user/") == true -> {
                val username = path.removePrefix("/user/")
                navigateToUser(username)
            }
        }
    }
}

App Links are verified HTTPS URLs that open directly in your app without showing the disambiguation dialog. For the full Android guide, see Android App Links: the complete guide. They require:

  1. Digital Asset Links file hosted on your domain
  2. Intent filters with autoVerify="true"
  3. Android 6.0 (API level 23) or higher

Digital Asset Links file (hosted at https://yourdomain.com/.well-known/assetlinks.json):

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.company.app",
    "sha256_cert_fingerprints":
    ["FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:66:75:91:03:3B:9C"]
  }
}]

AndroidManifest.xml configuration:

<activity android:name=".MainActivity">
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        
        <data android:scheme="https" 
              android:host="myapp.com" />
    </intent-filter>
</activity>

Google's Android App Links documentation covers the verification process in detail.

Implementing Deferred Deep Linking

Deferred deep linking requires tracking the user's intended destination before they install your app, then retrieving it after installation. You can build this yourself or use a deep linking platform. For a dedicated walkthrough, see deferred deep linking: how it works.

Building Deferred Deep Linking Yourself

The basic approach uses device fingerprinting:

  1. Before install: When a user clicks a deep link without your app installed, redirect them to a web page that:

    • Collects device fingerprint (IP, user agent, screen size, timezone)
    • Stores the fingerprint with the intended deep link destination
    • Redirects to the App Store or Google Play

    After install: When the app opens for the first time:

    • Collect the same device fingerprint
    • Send it to your server
    • Match against recent clicks
    • Return the original deep link destination
    • Navigate to the appropriate screen

    Here's a simplified server implementation:

    // Store click data
    app.post('/api/deeplink/click', (req, res) => {
      const { fingerprint, destination, timestamp } = req.body;
      
      // Store in database with TTL (e.g., 24 hours)
      await db.clicks.create({
        fingerprint: hashFingerprint(fingerprint),
        destination,
        timestamp,
        expires: Date.now() + 24 * 60 * 60 * 1000
      });
      
      res.json({ success: true });
    });
    
    // Retrieve destination after install
    app.post('/api/deeplink/match', async (req, res) => {
      const { fingerprint } = req.body;
      const hashedFingerprint = hashFingerprint(fingerprint);
      
      // Find recent clicks matching this fingerprint
      const match = await db.clicks.findOne({
        fingerprint: hashedFingerprint,
        expires: { $gt: Date.now() }
      });
      
      if (match) {
        // Delete the match to prevent reuse
        await db.clicks.delete({ _id: match._id });
        res.json({ destination: match.destination });
      } else {
        res.json({ destination: null });
      }
    });
    

    Using a Deep Linking Platform

    Building deferred deep linking yourself means handling:

    • Device fingerprinting algorithms
    • Probabilistic matching with confidence scores
    • Fallback mechanisms when matching fails
    • Privacy compliance (GDPR, CCPA)
    • Scale and performance optimization

    Platforms like Tolinku handle this complexity for you. After SDK integration, deferred deep linking works automatically:

    // iOS - AppDelegate or SceneDelegate
    Tolinku.shared.handleDeepLink { result in
        switch result {
        case .success(let deepLink):
            // Navigate to deepLink.url
            // Access context data via deepLink.parameters
        case .failure(let error):
            // Handle error or default navigation
        }
    }
    
    // Android - MainActivity
    Tolinku.handleDeepLink(this) { result ->
        when (result) {
            is DeepLinkResult.Success -> {
                // Navigate to result.url
                // Access context via result.parameters
            }
            is DeepLinkResult.Error -> {
                // Handle error or default navigation
            }
        }
    }
    

    Deep Linking and App Store Optimization

    Deep links directly impact your App Store Optimization (ASO) and organic search rankings. Both Apple and Google index deep link content, making your app discoverable through search. For a focused guide, see app indexing and SEO for mobile apps.

    iOS App Indexing

    Apple indexes Universal Links content in several ways:

    1. Spotlight Search: Users can find your app content directly from iOS search
    2. Siri Suggestions: iOS suggests your app based on user behavior patterns
    3. Safari Search: Web search results can include your app content

    To optimize for iOS indexing:

    import CoreSpotlight
    import MobileCoreServices
    
    func indexContent(product: Product) {
        let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeText as String)
        attributeSet.title = product.name
        attributeSet.contentDescription = product.description
        attributeSet.thumbnailData = product.image.pngData()
        
        let item = CSSearchableItem(
            uniqueIdentifier: "product-\(product.id)",
            domainIdentifier: "com.myapp.products",
            attributeSet: attributeSet
        )
        
        item.expirationDate = Date.distantFuture
        
        CSSearchableIndex.default().indexSearchableItems([item]) { error in
            if let error = error {
                print("Indexing error: \(error.localizedDescription)")
            }
        }
    }
    

    Android App Indexing

    Google indexes App Links content for Google Search results. When users search for content that exists in your app, Google can show app installation buttons or direct app results.

    Enable app indexing in Android:

    import com.google.firebase.appindexing.Action
    import com.google.firebase.appindexing.FirebaseAppIndex
    import com.google.firebase.appindexing.builders.Actions
    
    fun indexContent(product: Product) {
        val url = "https://myapp.com/product/${product.id}"
        
        val action = Actions.newView(
            product.name,
            url
        )
        
        FirebaseAppIndex.getInstance().update(
            Indexable.Builder()
                .setName(product.name)
                .setDescription(product.description)
                .setUrl(url)
                .setImage(product.imageUrl)
                .build()
        )
        
        FirebaseAppIndex.getInstance().start(action)
    }
    
    override fun onStop() {
        super.onStop()
        
        // Log the end of the view action
        FirebaseAppIndex.getInstance().end(action)
    }
    

    Common Deep Linking Problems and Solutions

    This is the most common iOS deep linking issue. Universal Links fail silently when misconfigured. See debugging AASA files and testing Universal Links for detailed troubleshooting.

    Common causes:

    • AASA file not accessible at the correct URL
    • Invalid JSON syntax in AASA file
    • Wrong Team ID or Bundle ID
    • Associated Domains not configured correctly
    • User has disabled Universal Links for your app

    Debugging steps:

    1. Verify AASA file: curl -i https://yourdomain.com/.well-known/apple-app-site-association
    2. Check Apple's CDN: curl -i https://app-site-association.cdn-apple.com/a/v1/yourdomain.com
    3. Use Apple's validator: https://search.developer.apple.com/appsearch-validation-tool
    4. Check device logs in Console.app for swcd process errors

    Android App Links verification happens asynchronously after app installation. If verification fails, your links open in the disambiguation dialog.

    Common causes:

    • assetlinks.json not served over HTTPS
    • Certificate fingerprint mismatch
    • Wrong package name
    • HTTP redirects on the domain

    Debugging steps:

    # Test with Google's validator
    https://digitalassetlinks.googleapis.com/v1/statements:list?source.web.site=https://yourdomain.com&relation=delegate_permission/common.handle_all_urls
    
    # Check verification status on device
    adb shell pm get-app-links com.your.package
    
    # Force reverification
    adb shell pm verify-app-links --re-verify com.your.package
    

    When deferred deep links fail, users land on the home screen after installing your app, defeating the purpose.

    Common causes:

    • Fingerprint mismatch between click and app open
    • Too much time between click and install
    • Privacy settings blocking fingerprint collection
    • VPN or proxy changing IP address

    Solutions:

    • Implement multiple matching strategies (deterministic + probabilistic)
    • Use shorter TTLs for click data (2-24 hours)
    • Provide manual fallback (let users enter a code)
    • Track match rates and investigate drops

    Common causes:

    • Using localhost URLs in production
    • Forgetting to update AASA/assetlinks.json for production domain
    • SSL certificate issues on production domain
    • URL encoding problems with special characters

    Best practices:

    • Use the same domain for development and production (with subdomains)
    • Automate AASA/assetlinks.json deployment
    • Test with production builds before release
    • URL encode all dynamic path components

    Well-designed deep link routes make your app more discoverable and easier to integrate with other apps and marketing campaigns.

    1. Design Human-Readable URLs

    Good:

    https://app.com/product/nike-air-max-90
    https://app.com/user/johndoe
    https://app.com/article/deep-linking-guide
    

    Bad:

    https://app.com/p?id=12345
    https://app.com/content?type=usr&q=98765
    https://app.com/x/AbC123XyZ
    

    Human-readable URLs are easier to share, remember, and debug. They also perform better in search results.

    2. Version Your Routes

    Include versioning in your routes to maintain backwards compatibility:

    https://app.com/v1/product/123
    https://app.com/v2/product/nike-air-max-90
    

    Or use different paths for different versions:

    https://app.com/products/123        # Old numeric IDs
    https://app.com/product/nike-air-max  # New slugs
    

    3. Handle Missing Content Gracefully

    When a deep link points to content that doesn't exist (deleted product, private profile, expired offer), don't crash or show an error screen.

    func handleProductDeepLink(productId: String) {
        ProductService.fetch(id: productId) { result in
            switch result {
            case .success(let product):
                navigateToProduct(product)
            case .failure(let error):
                if error == .notFound {
                    // Show search with the product ID pre-filled
                    navigateToSearch(query: productId)
                } else {
                    // Show home with a subtle error message
                    navigateToHome(message: "Product not available")
                }
            }
        }
    }
    

    4. Support Multiple URL Formats

    Users might type URLs manually or modify them. Support common variations:

    // All of these should work
    "https://app.com/product/123"
    "https://app.com/products/123"
    "https://app.com/p/123"
    "https://app.com/item/123"
    
    // In your router
    when {
        path.matches("/products?/\\d+".toRegex()) -> handleProduct(id)
        path.matches("/p/\\d+".toRegex()) -> handleProduct(id)  
        path.matches("/item/\\d+".toRegex()) -> handleProduct(id)
    }
    

    5. Add Context Parameters

    Include optional parameters that provide context without breaking the core functionality:

    https://app.com/product/123?source=email&campaign=black-friday
    https://app.com/invite/abc123?referrer=johndoe&reward=10
    

    These parameters help with:

    • Attribution tracking
    • Personalization
    • A/B testing
    • Analytics

    Security Considerations

    Deep links can be attack vectors if not properly secured. Users might craft malicious links to exploit your app. For comprehensive coverage, see deep linking security: preventing hijacking and abuse.

    1. Validate All Input

    Never trust deep link parameters. Validate and sanitize everything:

    func handleDeepLink(url: URL) {
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
              let path = components.path,
              let productId = extractProductId(from: path),
              isValidProductId(productId) else {
            // Reject malformed URLs
            return
        }
        
        // Validate query parameters
        let source = components.queryItems?.first(where: { $0.name == "source" })?.value
        if let source = source, !isValidSource(source) {
            // Log potential attack
            logSecurityEvent("Invalid source parameter: \(source)")
            return
        }
        
        // Safe to proceed
        navigateToProduct(id: productId, source: source)
    }
    
    func isValidProductId(_ id: String) -> Bool {
        // Only alphanumeric and hyphens, reasonable length
        let regex = "^[a-zA-Z0-9-]{1,50}$"
        return id.range(of: regex, options: .regularExpression) != nil
    }
    

    2. Prevent Open Redirects

    Don't allow arbitrary URLs in redirect parameters:

    // BAD - allows open redirect attacks
    val redirectUrl = deepLink.getQueryParameter("redirect")
    openWebView(redirectUrl)  // Could redirect to malicious site
    
    // GOOD - validate against allowlist
    val redirectPath = deepLink.getQueryParameter("redirect")
    val allowedDomains = listOf("myapp.com", "trusted-partner.com")
    
    val fullUrl = URL(redirectPath)
    if (allowedDomains.contains(fullUrl.host)) {
        openWebView(fullUrl)
    } else {
        // Ignore or log suspicious activity
    }
    

    3. Implement Rate Limiting

    Prevent abuse by limiting how often the same device can trigger certain actions through deep links:

    const rateLimiter = new Map();
    const RATE_LIMIT_WINDOW = 60000; // 1 minute
    const MAX_REQUESTS = 10;
    
    function checkRateLimit(deviceId, action) {
        const key = `${deviceId}:${action}`;
        const now = Date.now();
        const requests = rateLimiter.get(key) || [];
        
        // Remove old requests
        const recentRequests = requests.filter(time => now - time < RATE_LIMIT_WINDOW);
        
        if (recentRequests.length >= MAX_REQUESTS) {
            return false; // Rate limit exceeded
        }
        
        recentRequests.push(now);
        rateLimiter.set(key, recentRequests);
        return true;
    }
    

    Always use HTTPS for your deep links. HTTP links can be intercepted and modified:

    • Universal Links require HTTPS
    • App Links require HTTPS for verification
    • Custom schemes can't be encrypted, another reason to avoid them

    Comprehensive deep link testing prevents broken user experiences. Test across devices, OS versions, and edge cases. See deep link testing tools for available testing utilities.

    Manual Testing Checklist

    iOS Testing:

    • Links open app when installed
    • Links fall back to web when app not installed
    • Deferred deep links work after fresh install
    • Links work from Safari, Mail, Messages, and third-party apps
    • Long-press link shows app preview (iOS 13+)
    • Links work after app updates
    • Links work when app is killed vs backgrounded

    Android Testing:

    • Links skip disambiguation dialog (App Links verified)
    • Links work from Chrome, Gmail, SMS
    • Deferred deep links work after install from Play Store
    • Links work with different default browsers
    • Links work on Android 6+ (App Links) and older versions
    • Links handle both http and https schemes

    Automated Testing

    Write unit tests for your deep link routing logic:

    // Swift XCTest example
    func testProductDeepLink() {
        let router = DeepLinkRouter()
        let url = URL(string: "https://myapp.com/product/123?source=test")!
        
        let result = router.parse(url: url)
        
        XCTAssertEqual(result.destination, .product(id: "123"))
        XCTAssertEqual(result.parameters["source"], "test")
    }
    
    func testInvalidDeepLink() {
        let router = DeepLinkRouter()
        let url = URL(string: "https://myapp.com/invalid/path")!
        
        let result = router.parse(url: url)
        
        XCTAssertEqual(result.destination, .home)
    }
    
    // Kotlin JUnit example
    @Test
    fun testProductDeepLink() {
        val router = DeepLinkRouter()
        val uri = Uri.parse("https://myapp.com/product/123?source=test")
        
        val result = router.parse(uri)
        
        assertEquals(Destination.Product(id = "123"), result.destination)
        assertEquals("test", result.parameters["source"])
    }
    
    @Test
    fun testDeferredDeepLink() = runBlockingTest {
        val service = DeepLinkService()
        
        // Simulate click before install
        service.saveClickData("device123", "https://myapp.com/product/456")
        
        // Simulate app open after install
        val destination = service.retrieveDestination("device123")
        
        assertEquals("https://myapp.com/product/456", destination)
    }
    

    End-to-End Testing

    Use tools to simulate the full deep link flow:

    iOS Simulator:

    # Open deep link in simulator
    xcrun simctl openurl booted "https://myapp.com/product/123"
    
    # Test custom scheme
    xcrun simctl openurl booted "myapp://product/123"
    

    Android Emulator:

    # Test deep link
    adb shell am start -W -a android.intent.action.VIEW -d "https://myapp.com/product/123"
    
    # Test with specific package
    adb shell am start -W -a android.intent.action.VIEW -d "https://myapp.com/product/123" com.myapp.package
    

    Analytics and Attribution

    Measuring deep link performance helps you understand which channels drive the most valuable users. For a focused guide, see deep link analytics: measuring what matters. Track these key metrics:

    1. Click-through Rate (CTR): Clicks on your deep links divided by impressions
    2. Install Attribution: Which deep links lead to app installs
    3. Deferred Deep Link Success Rate: Percentage of installs that successfully route to intended destination
    4. Time to Action: How long users take to complete desired action after clicking
    5. Retention by Source: User retention segmented by deep link source

    Implementing Attribution

    Add attribution parameters to all your deep links:

    https://myapp.com/product/123?
      utm_source=email&
      utm_medium=newsletter&
      utm_campaign=black_friday&
      utm_content=hero_image
    

    Track these events in your analytics:

    // Track deep link opened
    Analytics.track("Deep Link Opened", properties: [
        "url": url.absoluteString,
        "source": parameters["utm_source"] ?? "direct",
        "medium": parameters["utm_medium"] ?? "none",
        "campaign": parameters["utm_campaign"] ?? "none",
        "content": parameters["utm_content"] ?? "none",
        "destination": destination.analyticsName
    ])
    
    // Track conversion
    Analytics.track("Deep Link Conversion", properties: [
        "action": "purchase",
        "value": purchase.total,
        "source": session.attributionSource,
        "time_to_convert": Date().timeIntervalSince(session.startTime)
    ])
    

    Building Attribution Funnels

    Track users through the entire journey:

    1. Link Click: User clicks deep link
    2. App Open: App launches (immediate or deferred)
    3. Screen View: User arrives at intended destination
    4. Engagement: User interacts with content
    5. Conversion: User completes desired action
    // Server-side funnel tracking
    const funnel = {
      linkClick: async (clickId, url, metadata) => {
        await db.funnelEvents.create({
          clickId,
          event: 'link_click',
          url,
          timestamp: new Date(),
          ...metadata
        });
      },
      
      appOpen: async (clickId, deferred = false) => {
        await db.funnelEvents.create({
          clickId,
          event: 'app_open',
          deferred,
          timestamp: new Date()
        });
      },
      
      conversion: async (clickId, action, value) => {
        await db.funnelEvents.create({
          clickId,
          event: 'conversion',
          action,
          value,
          timestamp: new Date()
        });
        
        // Calculate attribution
        const events = await db.funnelEvents.find({ clickId });
        const timeToConvert = events[events.length-1].timestamp - events[0].timestamp;
        
        await db.attributions.create({
          clickId,
          source: events[0].utm_source,
          timeToConvert,
          value
        });
      }
    };
    

    Use Cases and Examples

    Deep linking enables countless user experiences. Here are real-world examples across different industries:

    E-commerce

    Product Sharing: Users share products with friends who can tap to view in the app

    https://shop.app/product/summer-dress-floral?size=M&color=blue
    

    Abandoned Cart Recovery: Email reminders with deep links straight to checkout

    https://shop.app/cart?recover=abc123&discount=10
    

    Promotional Campaigns: QR codes in stores that open limited-time offers

    https://shop.app/promo/SUMMER2024?store=NYC001
    

    Social Media

    Content Sharing: Links that open specific posts, profiles, or conversations

    https://social.app/post/xyz789
    https://social.app/user/alice/followers
    https://social.app/messages/thread/123
    

    User Invitations: Referral links that track who invited whom

    https://social.app/invite?code=ALICE123&reward=premium_month
    

    Media and Entertainment

    Content Deep Links: Direct links to videos, articles, or podcasts

    https://media.app/watch/show/season/1/episode/5?t=180
    https://media.app/article/breaking-news-story
    https://media.app/podcast/episode/123?start=1250
    

    Playlist Sharing: Share curated content collections

    https://media.app/playlist/workout-mix-2024?creator=johndoe
    

    Financial Services

    Payment Links: Request or send money with pre-filled amounts

    https://pay.app/[email protected]&amount=50&note=dinner
    

    Document Review: Deep link to specific documents requiring signature

    https://bank.app/documents/loan-application-2024?action=sign
    

    Food Delivery

    Restaurant Menus: Link directly to a restaurant's menu

    https://food.app/restaurant/pizza-palace/menu?category=pizzas
    

    Reorder Previous Orders: One-tap reordering

    https://food.app/order/repeat?id=12345&address=home
    

    Group Orders: Share a group order for others to add items

    https://food.app/group-order/abc123/join
    

    Migration Strategies

    If you're moving from another deep linking solution or building deep linking for an existing app, plan your migration carefully.

    With Firebase Dynamic Links shutting down, many apps need migration strategies. For a detailed walkthrough, see Firebase Dynamic Links shutdown: what to do next and the migration guide.

    1. Audit Existing Links: Export all your Dynamic Links and their destinations
    2. Set Up Redirects: Configure your new deep linking service to handle old URLs
    3. Update Marketing Materials: Replace Dynamic Links in emails, ads, and print
    4. Monitor Analytics: Track both old and new links during transition

    Migrating from Branch

    When migrating from Branch or similar platforms:

    // Redirect old Branch links to new deep links
    app.get('/:branch_key/*', async (req, res) => {
      const branchKey = req.params.branch_key;
      const path = req.params[0];
      
      // Map old Branch links to new structure
      const mapping = await db.linkMappings.findOne({ branchKey });
      
      if (mapping) {
        res.redirect(301, `https://newdomain.com/${mapping.newPath}`);
      } else {
        res.redirect(301, 'https://newdomain.com');
      }
    });
    

    Supporting Multiple URL Schemes

    During migration, support both old and new URL formats:

    func handleUniversalLink(url: URL) -> Bool {
        // Handle new domain
        if url.host == "newdomain.com" {
            return handleNewDeepLink(url)
        }
        
        // Handle legacy domains
        if url.host == "olddomain.com" || url.host == "branch.io" {
            if let mappedURL = mapLegacyURL(url) {
                return handleNewDeepLink(mappedURL)
            }
        }
        
        return false
    }
    

    Performance Optimization

    Deep link routing should be fast. Users expect instant navigation after tapping a link.

    Optimize Route Matching

    Use efficient data structures for route matching:

    // Instead of sequential regex matching
    fun matchRoute(path: String): Route? {
        for (route in routes) {
            if (route.pattern.matches(path)) {
                return route
            }
        }
        return null
    }
    
    // Use a trie or route tree
    class RouteTree {
        private val root = RouteNode()
        
        fun addRoute(pattern: String, handler: RouteHandler) {
            var node = root
            val segments = pattern.split("/").filter { it.isNotEmpty() }
            
            for (segment in segments) {
                node = node.children.getOrPut(segment) { RouteNode() }
            }
            
            node.handler = handler
        }
        
        fun match(path: String): RouteMatch? {
            var node = root
            val segments = path.split("/").filter { it.isNotEmpty() }
            val params = mutableMapOf<String, String>()
            
            for ((index, segment) in segments.withIndex()) {
                node = node.children[segment] 
                    ?: node.children[":param"]  // Parameter match
                    ?: return null
                    
                if (node.children.containsKey(":param")) {
                    // Extract parameter value
                    params[node.paramName] = segment
                }
            }
            
            return node.handler?.let { RouteMatch(it, params) }
        }
    }
    

    Preload Critical Data

    Start loading data as soon as you receive the deep link:

    class DeepLinkHandler {
        func handleDeepLink(_ url: URL) {
            // Parse URL immediately
            guard let route = parseRoute(url) else { return }
            
            // Start loading data before animation completes
            switch route {
            case .product(let id):
                ProductService.shared.preload(productId: id)
                navigateToProduct(id: id)
                
            case .user(let username):
                UserService.shared.preload(username: username)
                navigateToUser(username: username)
            }
        }
    }
    
    // In the destination view controller
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Data might already be loaded
        if let product = ProductService.shared.getCached(productId: productId) {
            display(product)
        } else {
            // Load if not cached
            loadProduct()
        }
    }
    

    Handle Network Failures Gracefully

    Deep links often require network requests. Plan for failures:

    sealed class DeepLinkResult {
        data class Success(val destination: Destination) : DeepLinkResult()
        data class NetworkError(val error: Error, val fallback: Destination) : DeepLinkResult()
        data class NotFound(val suggestion: Destination?) : DeepLinkResult()
    }
    
    fun resolveDeepLink(url: Uri): DeepLinkResult {
        return try {
            val destination = parseAndValidate(url)
            DeepLinkResult.Success(destination)
        } catch (e: NetworkException) {
            // Provide offline fallback
            val fallback = getFallbackDestination(url)
            DeepLinkResult.NetworkError(e, fallback)
        } catch (e: NotFoundException) {
            // Suggest alternative
            val suggestion = findSimilarContent(url)
            DeepLinkResult.NotFound(suggestion)
        }
    }
    

    Future of Deep Linking

    The deep linking landscape continues to evolve with new platform features and user expectations.

    App Clips and Instant Apps

    Apple's App Clips and Google's Instant Apps blur the line between web and native:

    // App Clip deep link handling
    import AppClip
    
    guard let scene = window?.windowScene else { return }
    
    if let userActivity = scene.userActivity,
       let incomingURL = userActivity.webpageURL {
        handleAppClipDeepLink(incomingURL)
    }
    
    // Store data for full app
    let sharedDefaults = UserDefaults(suiteName: "group.com.app.shared")
    sharedDefaults?.set(cartItems, forKey: "clipCart")
    

    Privacy-Preserving Attribution

    With increasing privacy regulations and platform restrictions:

    • iOS 14.5+ requires ATT consent for IDFA
    • Android 12+ restricts device identifiers
    • Browsers block third-party cookies

    Modern deep linking platforms use privacy-preserving techniques:

    • On-device matching when possible
    • Aggregated attribution reporting
    • Consent-based tracking
    • First-party data strategies

    Web-to-App Journeys

    Progressive Web Apps (PWAs) and improved web capabilities create new deep linking scenarios:

    // Check if native app is installed
    if ('getInstalledRelatedApps' in navigator) {
      const relatedApps = await navigator.getInstalledRelatedApps();
      const nativeApp = relatedApps.find(app => app.platform === 'play');
      
      if (nativeApp) {
        // Deep link to native app
        window.location = 'myapp://path/to/content';
      } else {
        // Show install prompt or continue in PWA
        showAppInstallBanner();
      }
    }
    

    Conclusion

    Deep linking is foundational infrastructure for mobile apps. Implementing it correctly means better engagement, higher conversion rates, and app content that shows up in search results.

    Start with the basics: Universal Links on iOS and App Links on Android. Add deferred deep linking to capture users across the install boundary. Track attribution to understand what actually drives growth. Then expand into referral programs, personalized onboarding, and cross-platform journeys.

    The technical details matter, but the user experience matters more. Every deep link should deliver on its promise: take users directly to what they came for, without friction.

    Whether you build deep linking yourself or use a platform like Tolinku, the investment pays off. In a mobile-first world, deep links are not optional. They are essential infrastructure for any serious app.

Get deep linking tips in your inbox

One email per week. No spam.

In this article

Ready to add deep linking to your app?

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