A deep link is just a URL. What makes it interesting is everything that happens between the moment a user taps it and the moment a specific screen inside an app appears. That resolution chain involves the operating system, a server, a verification file, a registered app handler, and sometimes a fallback to a web page.
This article walks through exactly how deep linking works at each layer: URI schemes, OS-level verification, the Apple and Android resolution flows, and what happens when the app isn't installed.
The Basics: What Is a Deep Link
A standard web link points to a page. A deep link points to a location inside a native app. Instead of https://example.com/product/123 opening a browser tab, a deep link can open the product detail view inside the installed app, with the correct item already loaded.
The concept seems simple, but the implementation varies significantly between platforms and link types. There are three main categories:
Custom URI schemes are the oldest mechanism. An app registers a scheme like myapp:// with the OS, and any URL using that scheme is handed to the app. They work reliably when the app is installed, but they have no verification. Any app can register myapp://, and the OS has no way to confirm ownership. There is also no graceful fallback: if the app isn't installed, the link fails silently or shows an error. For a comparison, see URI schemes vs Universal Links.
Universal Links (iOS) and App Links (Android) solve the verification problem. These use standard https:// URLs. The OS verifies that the domain owner has explicitly authorized the app to handle those URLs by checking a hosted configuration file. If verification passes, the OS hands the URL to the app instead of the browser.
Deferred deep links handle the case where the app isn't installed yet. They preserve link context across an install so the user lands in the right place on first open. That's a separate topic covered in Deferred Deep Linking: How It Works Under the Hood.
For the rest of this article, the focus is on how Universal Links and App Links work, since they're the current standard.
How iOS Resolves Universal Links
When a user taps an https:// link on iOS, the OS has to decide: should this go to the browser, or to a registered app?
The answer depends on whether the app has passed domain verification. Here is the resolution chain in order.
Step 1: The App Declares Associated Domains
In Xcode, a developer adds an entitlement called Associated Domains. Each entry is a domain the app claims to handle, prefixed with applinks::
applinks:example.com
applinks:*.example.com
This declaration is included in the app's signed binary. When the app is installed, iOS reads this entitlement and knows which domains the app wants to handle.
Step 2: iOS Fetches the AASA File
After installation (and periodically after that), iOS fetches a file called apple-app-site-association from every domain listed in the app's entitlements. The file must be hosted at one of two paths:
https://example.com/.well-known/apple-app-site-association
https://example.com/apple-app-site-association
Apple also operates a CDN called the Apple CDN (part of the App Site Association system). In iOS 14 and later, iOS fetches the AASA file through Apple's servers, not directly from your domain. Apple caches and distributes it on your behalf. This means the file must be publicly accessible without authentication, and propagation after changes can take time (typically up to 24 hours via the Apple CDN).
Step 3: The AASA File Content
The AASA file is a JSON document that maps URL patterns to app identifiers. A minimal example:
{
"applinks": {
"details": [
{
"appIDs": ["TEAMID.com.example.myapp"],
"components": [
{
"/": "/products/*",
"comment": "Match all product detail pages"
},
{
"/": "/invite/*"
}
]
}
]
}
}
The appIDs field combines the Apple Team ID and the app's bundle identifier. The components array defines which URL paths the app handles. Paths not listed in the AASA file will open in the browser, not the app.
Step 4: Verification and Handoff
If the AASA file is valid and the app's entitlement matches an app ID in the file, iOS marks that domain as verified for that app. When the user taps a matching link, iOS intercepts it before the browser and hands it to the app via the application(_:continue:restorationHandler:) delegate method (UIKit) or the .onOpenURL modifier (SwiftUI).
The app receives the full URL. It then parses the path and query parameters and navigates to the appropriate screen.
What Can Go Wrong
AASA verification can fail for several reasons: the file is unreachable or returns the wrong content type, the Team ID or bundle ID is wrong, the path patterns don't match the actual URL, or the Apple CDN hasn't propagated a recent change yet. For troubleshooting, see debugging AASA files. Apple provides a validation tool to check the file, and the AASA Validator (third-party, not affiliated) is also widely used for debugging.
When verification fails, iOS falls back to opening the URL in Safari.
How Android Resolves App Links
Android's equivalent system is called App Links. The approach is similar to iOS but uses a different verification file and a different dispatch mechanism.
Step 1: Intent Filters in the Manifest
An Android app declares which URLs it handles by adding an <intent-filter> to its AndroidManifest.xml. To qualify as an App Link (verified), the filter must include android:autoVerify="true" and use the https scheme:
<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="example.com"
android:pathPrefix="/products" />
</intent-filter>
</activity>
The autoVerify attribute tells Android to verify ownership before granting exclusive handling rights.
Step 2: Android Fetches assetlinks.json
At install time, Android fetches a file from every domain listed in verified intent filters:
https://example.com/.well-known/assetlinks.json
Unlike iOS, Android fetches this file directly from your server (not through a Google CDN). It must return with a Content-Type of application/json and be accessible without authentication.
The file content maps a domain to one or more app fingerprints:
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.myapp",
"sha256_cert_fingerprints": [
"AA:BB:CC:DD:..."
]
}
}]
The sha256_cert_fingerprints field is the SHA-256 fingerprint of the app's signing certificate. This is the verification mechanism: only an app signed with that certificate can claim ownership of the domain.
You can retrieve your app's fingerprint with:
keytool -list -v -keystore release.keystore -alias my-alias
Step 3: Verification and Intent Dispatch
If Android can fetch and parse assetlinks.json, and the app's package name and certificate fingerprint match an entry in the file, verification succeeds. The app is granted "default" handling rights for URLs matching its intent filters.
When the user taps a matching https:// link, Android dispatches an Intent directly to the app without showing the disambiguation dialog (the "Open with…" picker). The app receives the Intent in its activity, reads the URL from intent.data, and navigates to the correct screen.
Android provides a tool for testing the verification status:
adb shell pm get-app-links --user cur com.example.myapp
The output should show verified for each domain that has passed. Full documentation is available in the Android Verified Links guide.
What Can Go Wrong
Common issues include: the assetlinks.json file being inaccessible (403, 404, wrong content type), the certificate fingerprint being wrong (especially when using debug vs. release signing), or the package name not matching exactly. See the Digital Asset Links setup guide for configuration details. On Android, verification failures mean the link opens in the browser or shows the disambiguation dialog instead of going directly to the app.
The Role of the OS in Link Resolution
A point worth understanding clearly: the OS is the decision-maker. Neither the app nor the server controls where a link goes. The OS makes the call based on what is installed and what has been verified. This has practical consequences:
- Verification files (
apple-app-site-association,assetlinks.json) must be live and correct at install time. Updating them after the fact requires the user to reinstall or wait for iOS/Android to re-fetch them. - An app can only claim a domain it controls. The verification files are the cryptographic proof of that control.
- If two apps claim the same domain and path, the OS behavior depends on the platform. On iOS, the most recently installed app typically wins (or the user is prompted). On Android, the disambiguating dialog appears.
What Happens When the App Is Not Installed
When a user taps a Universal Link or App Link and the app is not installed, the OS has no verified handler for it. What happens next depends on how the link was set up.
For Universal Links, iOS falls back to the web URL in Safari. If the web URL is a redirect that sends the user to the App Store, that redirect happens in the browser. The original path and query parameters from the link are visible in the browser, but they are not automatically passed to the app after installation.
For App Links on Android, the link opens in the default browser. Again, if the URL redirects to the Play Store, that redirect happens in the browser.
In either case, once the user installs and opens the app, the app has no access to that original URL. The link context is lost. This is the problem that deferred deep linking solves, using fingerprint matching or device identifiers to reconnect the user to their original intent.
Putting It Together
A useful way to think about the full resolution chain:
- User taps a link.
- OS checks if a verified app handles this URL.
- If yes and the app is installed: OS hands the URL to the app. App parses it and navigates.
- If no or the app is not installed: OS opens the URL in the browser. Browser follows any redirects (to the App Store, Play Store, or a web fallback page).
- After install (if deferred deep linking is implemented): the app SDK retrieves the original link context and navigates on first open.
Steps 3 and 5 are where the actual navigation happens. Steps 1, 2, and 4 are OS-level routing that the app developer influences but does not fully control.
Practical Implications for Developers
Getting deep linking right requires attention to several things at once:
Hosting the verification files correctly. Both AASA and assetlinks.json need to be served with the correct content type, over HTTPS, without authentication, at the exact paths the OS expects. A CDN or edge cache that modifies JSON or returns HTML error pages on 404 can silently break verification.
Matching path patterns accurately. A mismatch between the paths declared in the AASA or assetlinks.json and the actual URLs you send to users means links open in the browser instead of the app.
Handling the URL inside the app. Receiving the URL is just the start. The app needs to parse it, match it to a route, and navigate to the correct screen. Apps with complex navigation stacks often find this harder than the verification setup.
Testing across environments. Debug builds use a different signing certificate than release builds. Verification that works in production may not work in development unless you add the debug fingerprint to assetlinks.json or use a separate staging domain.
Platforms like Tolinku handle the hosting and verification file management automatically, so teams can focus on the in-app routing logic rather than the infrastructure around it. The Tolinku deep linking docs cover how the platform generates and serves these files for each Appspace.
Further Reading
The canonical references for each platform cover the details that matter most when implementation goes wrong:
- Apple: Supporting Associated Domains
- Apple: Universal Links
- Android: Verify Android App Links
- Android: App Links Overview
Both platforms update their verification behavior across OS versions. If you're debugging a link that stopped working after an OS update, the official documentation is the right starting point. For practical testing techniques, see deep link testing tools.
Get deep linking tips in your inbox
One email per week. No spam.