Apps with deep linking see 2x higher user engagement and 3x better conversion from campaigns. This guide covers everything from basic deep links to advanced attribution.
What is Deep Linking?
Types of Links
1. Deep Links (URI Schemes):
myapp://profile/123
- Only works if app installed
- Falls back to error if not installed
2. Universal Links (iOS) / App Links (Android):
https://myapp.com/profile/123
- Opens app if installed
- Opens website if not
- Seamless experience
3. Deferred Deep Links:
- User clicks link without app
- App Store/Play Store opens
- After install, opens to specific content
- Requires third-party SDK
Use Cases
- Marketing campaigns (email, social, ads)
- Referral programs (invite friends)
- Content sharing (share specific items)
- Password reset links
- Email verification
- Push notification actions
- QR codes
iOS Universal Links
Setup Steps
1. Enable Associated Domains:
Xcode:
1. Select target → Signing & Capabilities
2. Add "Associated Domains"
3. Add domains:
- applinks:myapp.com
- applinks:www.myapp.com
2. Create apple-app-site-association file:
{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAM_ID.com.myapp.bundle",
"paths": [
"/profile/*",
"/products/*",
"/invite/*",
"NOT /admin/*"
]
}
]
}
}
Host at: https://myapp.com/.well-known/apple-app-site-association
Requirements:
- HTTPS only
- No .json extension
- Content-Type: application/json
- Max 128 KB
3. Handle Universal Links in App:
// AppDelegate.swift
func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else {
return false
}
// Parse URL
handleDeepLink(url)
return true
}
// SwiftUI App
.onOpenURL { url in
handleDeepLink(url)
}
func handleDeepLink(_ url: URL) {
// Parse URL components
let pathComponents = url.pathComponents
// Example: https://myapp.com/profile/123
if pathComponents.count >= 3 && pathComponents[1] == "profile" {
let userId = pathComponents[2]
navigateToProfile(userId: userId)
}
else if pathComponents.count >= 3 && pathComponents[1] == "products" {
let productId = pathComponents[2]
navigateToProduct(productId: productId)
}
}
Testing Universal Links
Testing methods:
1. Notes app:
- Type link in Notes
- Long press → Open in App
2. Safari:
- Type link in Safari
- Should redirect to app
3. Terminal:
xcrun simctl openurl booted "https://myapp.com/profile/123"
4. Validate file:
https://branch.io/resources/aasa-validator/
https://search.developer.apple.com/appsearch-validation-tool/
Common issues:
- HTTPS not working → Check certificate
- Not redirecting → Check AASA file hosting
- Wrong format → Validate JSON
Android App Links
Setup Steps
1. Add Intent Filters (AndroidManifest.xml):
2. Create assetlinks.json:
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.myapp.android",
"sha256_cert_fingerprints": [
"14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5"
]
}
}]
Host at: https://myapp.com/.well-known/assetlinks.json
Get SHA256 fingerprint:
keytool -list -v -keystore my-release-key.keystore
3. Handle App Links in Activity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleDeepLink(intent)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
handleDeepLink(intent)
}
private fun handleDeepLink(intent: Intent?) {
val appLinkAction = intent?.action
val appLinkData = intent?.data
if (Intent.ACTION_VIEW == appLinkAction) {
appLinkData?.let { url ->
// Parse URL
when {
url.pathSegments.firstOrNull() == "profile" -> {
val userId = url.pathSegments.getOrNull(1)
navigateToProfile(userId)
}
url.pathSegments.firstOrNull() == "products" -> {
val productId = url.pathSegments.getOrNull(1)
navigateToProduct(productId)
}
}
}
}
}
}
Testing App Links
ADB command:
adb shell am start -W -a android.intent.action.VIEW -d "https://myapp.com/profile/123" com.myapp.android
Verify settings:
adb shell pm get-app-links com.myapp.android
Test in browser:
- Open Chrome
- Type link
- Should show "Open in app" dialog
Validate assetlinks.json:
https://digitalassetlinks.googleapis.com/v1/statements:list?source.web.site=https://myapp.com
URI Schemes (Custom Schemes)
iOS Setup
Info.plist:
CFBundleURLTypes
CFBundleURLSchemes
myapp
CFBundleURLName
com.myapp.url
Handle in AppDelegate:
func application(_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
// Parse: myapp://profile/123
handleDeepLink(url)
return true
}
Android Setup
AndroidManifest.xml:
Same handling code as App Links above.
Testing URI Schemes
iOS:
xcrun simctl openurl booted "myapp://profile/123"
Android:
adb shell am start -W -a android.intent.action.VIEW -d "myapp://profile/123"
Web:
Open in App
Deferred Deep Linking
What is Deferred Deep Linking?
Flow:
1. User clicks link (app not installed)
2. Redirects to App Store/Play Store
3. User installs app
4. First app open → navigate to intended content
Technical challenge:
- How to pass data through install?
Solution:
- Device fingerprinting
- Or referrer tracking (Android)
- Third-party SDKs handle this
Branch.io Implementation
iOS Setup:
// Install SDK
pod 'Branch'
// AppDelegate.swift
import Branch
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplication.LaunchOptionsKey: Any]?) -> Bool {
Branch.getInstance().initSession(launchOptions: launchOptions) { params, error in
guard let data = params as? [String: AnyObject] else { return }
// Check if opened from link
if let deepLinkPath = data["$deeplink_path"] as? String {
self.handleDeepLink(path: deepLinkPath, data: data)
}
}
return true
}
// Create deep link
func createBranchLink(productId: String) {
let buo = BranchUniversalObject(canonicalIdentifier: "product/\(productId)")
buo.title = "Product Name"
buo.contentDescription = "Check out this product!"
buo.imageUrl = "https://example.com/image.jpg"
buo.publiclyIndex = true
buo.locallyIndex = true
buo.contentMetadata.customMetadata["product_id"] = productId
let lp = BranchLinkProperties()
lp.feature = "sharing"
lp.channel = "social"
buo.getShortUrl(with: lp) { url, error in
if let url = url {
// Share URL: https://myapp.app.link/abc123
self.shareLink(url)
}
}
}
Android Setup:
// build.gradle
dependencies {
implementation 'io.branch.sdk.android:library:5.+'
}
// AndroidManifest.xml
// Application class
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
Branch.getAutoInstance(this)
}
}
// MainActivity
class MainActivity : AppCompatActivity() {
override fun onStart() {
super.onStart()
Branch.sessionBuilder(this).withCallback { referringParams, error ->
if (referringParams != null) {
val productId = referringParams.optString("product_id")
if (productId.isNotEmpty()) {
navigateToProduct(productId)
}
}
}.withData(intent?.data).init()
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
setIntent(intent)
Branch.sessionBuilder(this).reInit()
}
}
Attribution Tracking
Track Campaign Performance
URL parameters:
https://myapp.com/invite/abc123?
utm_source=instagram
&utm_medium=story
&utm_campaign=launch_week
&utm_content=variant_a
Track in app:
func handleDeepLink(_ url: URL) {
let components = URLComponents(url: url, resolvingAgainstBaseURL: true)
let queryItems = components?.queryItems
let source = queryItems?.first(where: { $0.name == "utm_source" })?.value
let medium = queryItems?.first(where: { $0.name == "utm_medium" })?.value
let campaign = queryItems?.first(where: { $0.name == "utm_campaign" })?.value
Analytics.logEvent("deep_link_open", parameters: [
"source": source ?? "unknown",
"medium": medium ?? "unknown",
"campaign": campaign ?? "unknown"
])
// Navigate to content
handleNavigation(url)
}
Best Practices
URL Structure
Good:
✓ https://myapp.com/products/123
✓ https://myapp.com/invite/abc123
✓ https://myapp.com/reset-password?token=xyz
Bad:
❌ https://myapp.com?screen=products&id=123 (unclear)
❌ https://myapp.com/p/123 (not descriptive)
❌ myapp://open?params=base64encodedmess (ugly)
Fallback Handling
Always have fallbacks:
- Link doesn't match → Home screen
- Content deleted → Show error + similar content
- User not authorized → Login screen
- Network error → Cache or retry
func handleDeepLink(_ url: URL) {
guard let content = parseURL(url) else {
// Fallback to home
navigateToHome()
return
}
fetchContent(content.id) { result in
switch result {
case .success(let data):
navigateToContent(data)
case .failure(let error):
if error == .notFound {
showNotFoundError()
navigateToHome()
} else {
showRetryDialog()
}
}
}
}
Security Considerations
- Validate all URL parameters
- Don't trust user input from links
- Verify authentication before sensitive actions
- Use HTTPS for web-based deep links
- Rate-limit link generation
Analytics
Key Metrics
Track:
- Deep link opens
- Install attribution (which link drove install)
- Conversion from deep link
- Time to conversion
- Deep link click-through rate
- Platform breakdown (iOS vs Android)
Events:
analytics.logEvent("deep_link_clicked", ...)
analytics.logEvent("deep_link_opened", ...)
analytics.logEvent("deep_link_converted", ...)
Conclusion
Deep linking dramatically improves user experience and campaign effectiveness. Implement universal/app links for seamless web-to-app transitions, add deferred deep linking for attribution, and track everything for optimization.