Implement deep linking that seamlessly routes users from web links, emails, and notifications into specific screens in your mobile app. Covers Universal Links, App Links, deferred deep linking, link routing architectures, and the testing strategies that prevent deep links from silently breaking.
Deep linking connects the web and mobile apps by allowing a URL to open a specific screen or state within a mobile app, whether the app is installed or not. It matters when a user clicks a link from an email, a social post, or a web dashboard and expects to land directly in the app at the exact location—like a product detail, a user profile, or a checkout flow—without navigating manually.
When a user taps a URL, the system checks whether the corresponding mobile app is installed. If installed, the app opens and handles the URL. If not, the system may prompt to install the app from the App Store or Play Store, then open it after installation. This process is governed by a combination of URL schemes, universal links (iOS), and app links (Android), all working together to ensure a seamless transition.
A URL scheme is the simplest form of deep linking. It uses a custom protocol, such as myapp://user/123. The mobile app registers this scheme during installation. On iOS, this registration happens in Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.example.myapp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
On Android, the same registration is done in 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" />
<data android:scheme="myapp" />
</intent-filter>
</activity>
When a myapp://user/123 link is tapped, the system launches the app and passes the path and query parameters to it.
Universal links extend URL schemes by allowing the app to handle any https:// URL. The app declares a apple-app-site-association file at the root of its domain:
https://example.com/apple-app-site-association
This file is a JSON document, typically served with Content-Type: application/json, and contains a list of paths and patterns the app can handle:
{
"applinks": {
"apps": [],
"details": [
{
"appID": "ABC123XYZ456.com.example.myapp",
"paths": ["/user/*", "/product/*", "/orders", "*"]
}
]
}
}
The appID is the combination of the Apple Developer Team ID and the app’s bundle ID. The paths array supports wildcards (*) and exact paths. When the user taps a link like https://example.com/user/123, iOS checks for the apple-app-site-association file, fetches it, matches the path, and launches the app with the full URL as a query.
App links use assetlinks.json to establish trust between a website and a mobile app. The file is hosted at:
https://example.com/.well-known/assetlinks.json
It contains a list of assetlink objects, each describing a relationship between a web domain and an app package name:
[
{
"relation": ["delegate_default", "delegate"],
"target": {
"namespace": "android",
"package_name": "com.example.myapp",
"sha256_cert_fingerprints": [
"a1:b2:c3:d4:e5:f6:...:z1"
]
}
}
]
The sha256_cert_fingerprints field must match the app’s signing certificate. The relation field specifies how the app handles the link: delegate_default means the app handles the link and is the default handler for the domain.
When a universal link or app link is available, the system attempts to open the app. If the app is not installed, it can fall back to the https:// URL, which may redirect to a mobile-optimized web version or trigger an install prompt.
On iOS, if the app is not installed and a universal link is available, the system downloads the app from the App Store and launches it with the URL. On Android, if the app is not installed, the system displays a “Install and Open” prompt.
Once the app is launched, it must parse the incoming URL and navigate to the appropriate screen. The app can be in one of three states: not running, in the background, or already running.
When the app starts from a deep link, the system passes the URL to the app’s entry point. On iOS, AppDelegate.swift receives the URL in application(_:didFinishLaunchingWithOptions:):
func application(_ app: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]?) -> Bool {
if let url = launchOptions?[.launchUrl] as? URL {
handleDeepLink(url)
}
return true
}
On Android, MainActivity.onCreate() receives the intent data:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent = getIntent();
Uri data = intent.getData();
if (data != null) {
handleDeepLink(data);
}
}
The app can then extract parameters from the URL:
func handleDeepLink(_ url: URL) {
let pathComponents = url.pathComponents
let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems ?? []
switch pathComponents[1] {
case "user":
let userId = pathComponents[2]
let user = User(id: userId, params: queryItems)
showUserScreen(user)
case "product":
let productId = pathComponents[2]
let product = Product(id: productId, params: queryItems)
showProductScreen(product)
default:
showHomeScreen()
}
}
When the app is already running, a deep link can arrive in the background. On iOS, application(_:continueUserActivity:restorationHandler:) handles incoming user activities:
func application(_ app: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([Any]) -> Void) -> Bool {
if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
handleDeepLink(userActivity.webpageURL!)
}
return true
}
On Android, onNewIntent() is called when the app receives a new intent while already running:
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
Uri data = intent.getData();
if (data != null) {
handleDeepLink(data);
}
}
The app must update its UI to reflect the new state and optionally add a back stack entry.
Deep links often carry query parameters to control state. The query string is parsed and used to configure the app.
Common parameters include:
id, userId, productId, orderIdtab, section, view, modeutm_source, utm_campaign, utm_mediumFor example, https://example.com/product/123?tab=reviews&utm_source=email opens the product screen with the reviews tab selected, and tracks the source as email.
A single deep link can include multiple parameters. The app must combine them into a coherent state:
{
"id": "123",
"tab": "reviews",
"sort": "newest",
"filter": "featured"
}
The app can merge query parameters with existing state, such as a cached user profile or a navigation stack.
If a deep link points to a non-existent resource, the app should handle it gracefully. For example, a user/999 link where user 999 does not exist:
If the URL is malformed, the app should parse it and fall back to defaults:
id parameter → show home screen.tab value → show first tab.Testing deep links requires verifying the full flow: from a URL, through app install or launch, to screen rendering.
Use xcrun to test universal links:
xcrun simctl openurl booted https://example.com/user/123
This opens the app on the simulator and passes the URL. Use xcrun simctl log to inspect logs:
xcrun simctl log booted --filter "MyApp"
Use adb to send intents:
adb shell am start -W -a android.intent.action.VIEW \
-d "https://example.com/product/456" \
-n com.example.myapp/.MainActivity
Verify that the app launches and processes the URL.
To test assetlinks.json, use the Android App Links Assistant tool, but the core mechanics are standard:
Content-Type.sha256_cert_fingerprints match the app’s signing certificate.adb shell am start -a android.intent.action.VIEW -d https://example.com/user/123.apple-app-site-association file: The file must be served with Content-Type: application/json, and must be accessible without redirects. A redirect to apple-app-site-association.json without the Content-Type header can cause the universal link to fail silently.appID format: The appID must be TEAMID.BUNDLEID, but a typo in the team ID or bundle ID can cause the app to be ignored.assetlinks.json: The file must be at the correct location and must be served with Content-Type: application/json. A typo in the path (e.g., asset-links.json) or missing Content-Type header leads to app link failures.launchOptions dictionary is passed to didFinishLaunchingWithOptions, but if the app is already running, onNewIntent must be implemented to handle additional links.NSUserActivity in continueUserActivity: If continueUserActivity is not implemented, the app does not respond to universal links when it is already running.category: BROWSABLE on Android: Without this, the app does not appear in the “Open with” list for web URLs.delegate_default: If the relation is only delegate, the app handles the link but does not become the default handler for the domain.path and query parameters consistently across all screens.https:// URLs for all links, not just myapp://.This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy