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.
Types of Deep Links
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.
1. Standard Deep Links
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.
2. Deferred Deep Links
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:
- User clicks a deep link on a device without your app
- System redirects to the App Store or Google Play
- User installs and opens the app
- App retrieves the original deep link destination
- 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.
3. Contextual Deep Links
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 on iOS
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:
- An apple-app-site-association (AASA) file hosted on your domain
- Associated Domains capability in your app
- 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)
}
}
}
}
Android App Links
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:
- Digital Asset Links file hosted on your domain
- Intent filters with
autoVerify="true" - 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:
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:
- Spotlight Search: Users can find your app content directly from iOS search
- Siri Suggestions: iOS suggests your app based on user behavior patterns
- 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
Problem 1: Universal Links Not Opening the App
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:
- Verify AASA file:
curl -i https://yourdomain.com/.well-known/apple-app-site-association - Check Apple's CDN:
curl -i https://app-site-association.cdn-apple.com/a/v1/yourdomain.com - Use Apple's validator: https://search.developer.apple.com/appsearch-validation-tool
- Check device logs in Console.app for
swcdprocess errors
Problem 2: App Links Verification Failing
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.packageProblem 3: Deferred Deep Links Not Working
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
Problem 4: Deep Links Working in Development but Not Production
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
Best Practices for Deep Link Routes
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-guideBad:
https://app.com/p?id=12345 https://app.com/content?type=usr&q=98765 https://app.com/x/AbC123XyZHuman-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-90Or use different paths for different versions:
https://app.com/products/123 # Old numeric IDs https://app.com/product/nike-air-max # New slugs3. 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=10These 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; }4. Use HTTPS for Universal Links and App Links
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
Testing Deep Links
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.packageAnalytics 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:
Essential Deep Link Metrics
- Click-through Rate (CTR): Clicks on your deep links divided by impressions
- Install Attribution: Which deep links lead to app installs
- Deferred Deep Link Success Rate: Percentage of installs that successfully route to intended destination
- Time to Action: How long users take to complete desired action after clicking
- 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_imageTrack 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:
- Link Click: User clicks deep link
- App Open: App launches (immediate or deferred)
- Screen View: User arrives at intended destination
- Engagement: User interacts with content
- 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=blueAbandoned Cart Recovery: Email reminders with deep links straight to checkout
https://shop.app/cart?recover=abc123&discount=10Promotional Campaigns: QR codes in stores that open limited-time offers
https://shop.app/promo/SUMMER2024?store=NYC001Social 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/123User Invitations: Referral links that track who invited whom
https://social.app/invite?code=ALICE123&reward=premium_monthMedia 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=1250Playlist Sharing: Share curated content collections
https://media.app/playlist/workout-mix-2024?creator=johndoeFinancial Services
Payment Links: Request or send money with pre-filled amounts
https://pay.app/[email protected]&amount=50¬e=dinnerDocument Review: Deep link to specific documents requiring signature
https://bank.app/documents/loan-application-2024?action=signFood Delivery
Restaurant Menus: Link directly to a restaurant's menu
https://food.app/restaurant/pizza-palace/menu?category=pizzasReorder Previous Orders: One-tap reordering
https://food.app/order/repeat?id=12345&address=homeGroup Orders: Share a group order for others to add items
https://food.app/group-order/abc123/joinMigration Strategies
If you're moving from another deep linking solution or building deep linking for an existing app, plan your migration carefully.
Migrating from Firebase Dynamic Links
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.
- Audit Existing Links: Export all your Dynamic Links and their destinations
- Set Up Redirects: Configure your new deep linking service to handle old URLs
- Update Marketing Materials: Replace Dynamic Links in emails, ads, and print
- 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.