Deep Linking for Mobile Apps: Technical Guide and Best Practices
Master deep linking with universal links, app links, and deferred deep linking for seamless web-to-app experiences.


Deep linking connects the web and app worlds. Done right, it creates seamless user experiences. Done wrong, it creates broken experiences and lost users.
This guide covers the technical implementation of deep linking across iOS and Android, integration with web-to-app campaigns, and how to debug when things break.
What Is Deep Linking and Why It Matters
Deep linking is a link that navigates directly to specific content within an app, rather than the app's home screen.
Example: Instead of clicking a link that opens your app to the home screen, the user clicks and lands directly on a specific workout, product page, or subscription screen.
Why it matters for user acquisition:
- Contextual relevance: user sees the content they clicked on
- Reduced friction: no need to navigate after opening app
- Better attribution: your server can track which deep link led to which user
- Improved retention: users land on meaningful content, not generic home screen
Business impact: Apps with deep linking see 20-40% higher conversion rates from web-to-app traffic compared to apps that redirect to home screen.
Types of Deep Links
Standard Deep Links (Custom Schemes)
The original deep linking method. Less reliable but still used.
Format: myapp://feature/product?id=123&referrer=meta_campaign
How it works:
- User clicks link in browser
- Operating system routes to app via custom scheme
- App parses scheme and navigates to specified screen
Problems:
- If app isn't installed, link fails completely (browser doesn't fall back to web)
- Collisions: multiple apps can register same scheme
- Mobile browser handling inconsistent
Implementation (iOS):
// AppDelegate.swift - handle custom schemes
func application(_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
guard let scheme = url.scheme, scheme == "myapp" else {
return false
}
// Parse URL components
if let host = url.host {
switch host {
case "feature":
handleFeatureDeepLink(url: url)
case "product":
handleProductDeepLink(url: url)
default:
return false
}
}
return true
}
func handleProductDeepLink(url: URL) {
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
let queryItems = components.queryItems {
if let productId = queryItems.first(where: { $0.name == "id" })?.value {
// Navigate to product screen with productId
navigateToProduct(id: productId)
}
}
}Implementation (Android):
<!-- AndroidManifest.xml -->
<activity android:name=".DeepLinkActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp"
android:host="feature" />
</intent-filter>
</activity>Verdict: Use custom schemes only as fallback. Standard deep links should use universal links (iOS) or app links (Android).
Universal Links (iOS)
Apple's modern deep linking solution. Seamless, secure, and reliable.
Format: https://myapp.com/product?id=123 (regular HTTPS URL)
How it works:
- User clicks HTTPS link in browser or receives in text/email
- iOS checks if app is installed and domain is associated
- If app installed: opens app and passes URL
- If app not installed: opens link in browser (fallback)
Why universal links are superior:
- Fallback to web if app not installed (no broken links)
- Uses regular HTTPS URLs (works in more contexts)
- More secure (domain verification prevents hijacking)
- Better analytics (standard web links)
Implementation:
Step 1: Create apple-app-site-association file
Place on your web server at https://yourdomain.com/.well-known/apple-app-site-association
{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAM_ID.BUNDLE_ID",
"paths": ["/product/*", "/feature/*"]
}
]
},
"webcredentials": {
"apps": ["TEAM_ID.BUNDLE_ID"]
}
}Where:
- TEAM_ID: Your Apple Developer Team ID (10-character alphanumeric)
- BUNDLE_ID: Your app's bundle ID (e.g., com.example.myapp)
Step 2: Configure in Xcode
Go to: Target > Signing & Capabilities > Associated Domains
Add domain: applinks:yourdomain.com
Step 3: Handle in AppDelegate
import UIKit
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
return true
}
// Handle universal links
func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else {
return false
}
return handleDeepLink(url)
}
private func handleDeepLink(_ url: URL) -> Bool {
// Extract path components
let pathComponents = url.pathComponents
if pathComponents.count >= 2 {
switch pathComponents[1] {
case "product":
if let productId = url.queryItemValue(for: "id") {
navigateToProduct(id: productId)
return true
}
case "feature":
if let featureName = url.queryItemValue(for: "name") {
navigateToFeature(name: featureName)
return true
}
default:
break
}
}
return false
}
}
extension URL {
func queryItemValue(for name: String) -> String? {
URLComponents(url: self, resolvingAgainstBaseURL: false)?
.queryItems?
.first(where: { $0.name == name })?
.value
}
}Testing universal links:
# Verify apple-app-site-association is accessible
curl -I https://yourdomain.com/.well-known/apple-app-site-association
# Should return 200 status
# Content-Type should be application/json
# On device, go to Settings > General > Rebooting
# Force-close app, then click universal link in Safari
# App should open directly to correct screenApp Links (Android)
Android's equivalent to universal links. Also uses HTTPS URLs with automatic fallback.
Implementation:
Step 1: Create assetlinks.json
Place on your web server at https://yourdomain.com/.well-known/assetlinks.json
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.myapp",
"sha256_cert_fingerprints": [
"AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99"
]
}
}
]To find your SHA256 certificate fingerprint:
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android | grep SHA256Step 2: Configure in AndroidManifest.xml
<activity android:name=".DeepLinkActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Handle app links -->
<data
android:scheme="https"
android:host="yourdomain.com"
android:pathPrefix="/product" />
<data
android:scheme="https"
android:host="yourdomain.com"
android:pathPrefix="/feature" />
</intent-filter>
</activity>Step 3: Handle in Activity
import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
class DeepLinkActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val uri = intent.data
uri?.let { handleDeepLink(it) }
}
private fun handleDeepLink(uri: Uri) {
val path = uri.path ?: return
when {
path.contains("/product") -> {
val productId = uri.getQueryParameter("id")
navigateToProduct(productId)
}
path.contains("/feature") -> {
val featureName = uri.getQueryParameter("name")
navigateToFeature(featureName)
}
}
}
}Testing app links:
# Verify assetlinks.json is accessible
curl https://yourdomain.com/.well-known/assetlinks.json
# Test on device/emulator
adb shell am start -W -a android.intent.action.VIEW -d "https://yourdomain.com/product?id=123" com.example.myappDeferred Deep Linking
Standard deep linking works only if the app is installed. Deferred deep linking handles new installs: users click link, land on app store, install app, then get routed to the original deep link.
How it works:
- User clicks
https://myapp.com/product?id=123 - Server detects app not installed
- Redirects to app store (Play Store or App Store)
- User installs app
- App's first launch receives deep link context
- App navigates user to /product?id=123 automatically
Implementation approach:
Step 1: Web server detection
# Flask example
from flask import Flask, request, redirect
from user_agents import parse
import os
app = Flask(__name__)
def get_app_store_link(original_url):
"""Get app store link based on OS"""
user_agent = parse(request.headers.get('User-Agent'))
if user_agent.is_mobile and user_agent.os.family == 'iOS':
return f"https://apps.apple.com/app/myapp/id{os.getenv('IOS_APP_ID')}"
elif user_agent.is_mobile and user_agent.os.family == 'Android':
return f"https://play.google.com/store/apps/details?id={os.getenv('ANDROID_PACKAGE_NAME')}"
# Desktop - redirect to web version
return "https://myapp.com"
@app.route('/product', methods=['GET'])
def deferred_deep_link():
"""Handle deferred deep linking for /product?id=X"""
product_id = request.args.get('id')
app_store_link = get_app_store_link(request.url)
# Store deferred link mapping server-side
# When app opens for first time, it queries this mapping
deferred_links.store({
'product_id': product_id,
'app_store_link': app_store_link
})
return redirect(app_store_link)
@app.route('/deferred-link', methods=['GET'])
def get_deferred_link():
"""App calls this on first launch to get deferred link"""
# Method 1: IP + user agent matching
# Method 2: Device fingerprinting
# Method 3: Store link in URL parameter
deferred_link = deferred_links.get_by_device_fingerprint(request)
return jsonify(deferred_link)Step 2: App-side implementation
// iOS - on app launch
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Check if this is first launch with referrer
let isFirstLaunch = !UserDefaults.standard.bool(forKey: "hasLaunchedBefore")
if isFirstLaunch {
UserDefaults.standard.set(true, forKey: "hasLaunchedBefore")
// Fetch deferred link
fetchDeferredLink { url in
if let url = url {
self.handleDeepLink(url)
}
}
}
return true
}
private func fetchDeferredLink(completion: @escaping (URL?) -> Void) {
let url = URL(string: "https://myapp.com/deferred-link")!
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data,
let json = try? JSONDecoder().decode(DeferredLink.self, from: data),
let deepLinkURL = URL(string: json.url) {
completion(deepLinkURL)
} else {
completion(nil)
}
}.resume()
}
}Step 3: Using attribution partners
Instead of building deferred linking yourself, use partners like:
- AppsFlyer (integrated deferred deep linking)
- Adjust (deferred deep linking with attribution)
- Branch (deferred deep linking platform)
These handle the complexity (device matching, fingerprinting) automatically.
AppsFlyer example:
import AppsFlyerLib
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
AppsFlyerLib.shared().appsFlyerDevKey = "YOUR_APPSFLYER_KEY"
AppsFlyerLib.shared().appleAppID = "YOUR_APP_ID"
// Handle deferred deep linking
AppsFlyerLib.shared().onDeepLinking { deepLinkResult in
if let deepLinkURL = deepLinkResult.deepLink {
self.handleDeepLink(deepLinkURL)
}
}
AppsFlyerLib.shared().start()
return true
}Deep Linking in Web-to-App Campaigns
Deep linking enables powerful web-to-app flows where users can be contextually routed to app content.
Campaign Structure with Deep Linking
Example: Fitness app promoting specific workout classes
Campaign: "HIIT Workouts"
↓
Web landing page: https://myapp.com/landing?class_id=hiit_beginners
↓
User clicks "Open in App"
↓
Deep link: myapp://class?id=hiit_beginners&source=web_campaign
↓
App opens directly to HIIT workoutImplementation:
<!-- Web landing page -->
<button id="open-app-btn">Open in App</button>
<script>
document.getElementById('open-app-btn').addEventListener('click', function() {
const classId = new URLSearchParams(window.location.search).get('class_id');
// Construct deep link
const deepLink = `myapp://class?id=${classId}&source=web_campaign`;
// iOS universal link fallback
const universalLink = `https://myapp.com/class?id=${classId}&source=web_campaign`;
// Try deep link first
window.location = deepLink;
// Fallback to universal link after 1 second if app didn't open
setTimeout(function() {
window.location = universalLink;
}, 1000);
});
</script>Passing Campaign Data Through Deep Links
Track which campaigns drive installs by encoding attribution data in deep links.
Deep link format:
myapp://product?id=123&utm_source=google&utm_campaign=summer_sale&utm_medium=cpcApp-side attribution tracking:
func handleDeepLink(_ url: URL) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
let queryItems = components.queryItems else { return }
var attributionData: [String: String] = [:]
for item in queryItems {
if item.name.starts(with: "utm_") {
attributionData[item.name] = item.value ?? ""
}
}
// Log to analytics/attribution service
Analytics.logEvent("deep_link_opened", parameters: attributionData)
// Navigate to content
if let productId = queryItems.first(where: { $0.name == "id" })?.value {
navigateToProduct(id: productId)
}
}Debugging Deep Linking Issues
iOS Deep Link Not Opening
Symptoms: Click link, nothing happens.
Debugging steps:
- Verify apple-app-site-association file:
curl -v https://yourdomain.com/.well-known/apple-app-site-association
# Should return 200 with application/json or application/octet-stream- Check associated domains:
# Xcode: Target > Signing & Capabilities > Associated Domains
# Should show "applinks:yourdomain.com"- Force refresh universal links (on device):
Settings > Debug > Reset Universal Links- Check console logs:
// Enable detailed logging in iOS
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
print("⬇️ Universal Link: \(userActivity.webpageURL?.absoluteString ?? "unknown")")
return handleDeepLink(userActivity.webpageURL)
}
}Android Deep Link Not Opening
Symptoms: Click link, browser opens instead of app.
Debugging steps:
- Verify assetlinks.json:
curl https://yourdomain.com/.well-known/assetlinks.json- Check manifest configuration:
adb shell am start -W -a android.intent.action.VIEW -d "https://yourdomain.com/product?id=123" com.example.myapp- Verify SHA256 fingerprint matches:
# Get signing certificate SHA256
keytool -list -v -keystore path/to/keystore -alias key_alias
# Compare with assetlinks.json- Check logs:
adb logcat | grep AppLinksDeep Link Not Passing Data Correctly
Symptoms: Link opens app but parameters missing.
Debug with browser console:
// Before navigating
const originalUrl = window.location.href;
const deepLink = `myapp://product?${new URLSearchParams(window.location.search)}`;
console.log('Original URL:', originalUrl);
console.log('Deep link:', deepLink);App-side debug:
func handleDeepLink(_ url: URL) {
print("Full URL: \(url.absoluteString)")
print("Path: \(url.path)")
print("Query items:")
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true) {
components.queryItems?.forEach { item in
print(" \(item.name) = \(item.value ?? "nil")")
}
}
}Best Practices Summary
-
Use universal links (iOS) and app links (Android) — They're the modern standard and handle fallback automatically.
-
Implement deferred deep linking for new installs — Users should land on relevant content even after install.
-
Pass attribution data in deep links — Track which campaigns drive which installs.
-
Test extensively across devices and OS versions — Deep linking behavior varies.
-
Monitor deep link performance — Track open rates, conversion rates, user retention by deep link destination.
-
Use consistent domain structures — Keep web URLs and deep links parallel (
https://myapp.com/product→myapp://product).
FAQ
Q: Should I use custom schemes or universal links? Use universal links whenever possible. Custom schemes should be fallback only.
Q: Can deep linking improve attribution accuracy? Yes significantly. Web-to-app deep links maintain browser-level attribution that wouldn't be available otherwise.
Q: What if my app isn't installed yet? Use deferred deep linking (or an attribution partner like AppsFlyer/Adjust) to track the intent and navigate correctly after install.
Q: Does deep linking affect app store ranking? No, but traffic quality does. Deep-linked users typically have higher engagement and retention, which indirectly helps ranking.
Q: Can I deep link to purchased content? Yes, but don't require login. Let users see content if it's already purchased, or show paywall if not. Check purchase status at app launch.
Deep linking is foundational for modern mobile marketing. Whether you're building web-to-app campaigns or improving retention, proper deep linking implementation delivers measurable improvements in user experience and campaign efficiency.
Start with universal links and app links. Add deferred deep linking once you have volume. Monitor performance obsessively.
Ready to build web-to-app campaigns that drive high-value users? Join Audiencelab today and integrate deep linking with attribution signals across all your campaigns.