Glossary/Failure modes
Testing a deep link
Definition
Testing a deep link means verifying three separate things — that the operating system routes the URL to the app, that the app receives it, and that the app navigates to the right screen — each of which can pass while the others fail.
The reason deep links so often ship broken is that the easiest tests only exercise the third of those, and they pass regardless of the first two. xcrun simctl openurl and adb am start both hand the URL directly to the app, bypassing the association and verification layers entirely — so they succeed on a build with no entitlement, no association file, and no verified domain. This page is which command tests which layer.
The three layers, and what tests each
| Layer | Question | How to test it |
|---|---|---|
| Association | Does the OS accept this app as the handler for this domain? | pm get-app-links (Android), swcutil show (iOS), or tapping a real link |
| Delivery | Does the URL reach the app's entry point? | am start (Android), simctl openurl (iOS), with a breakpoint in the handler |
| Routing | Does the app navigate to the right screen? | The same commands, watching where the app lands |
A passing delivery test says nothing about association
This is the single most common false confidence in deep linking. Both simctl openurl and am start with an explicit package succeed on completely unverified setups, because they skip the layer that decides whether a real tap would ever reach your app. If the only evidence that deep links work is one of those commands, the deep links do not work.
Android
# 1. ASSOCIATION — the real state. Anything but "verified" is a failure.
adb shell pm get-app-links com.example.shop
# Force re-verification after fixing assetlinks.json
adb shell pm verify-app-links --re-verify com.example.shop
# 2. HONEST END-TO-END — no package name, so the system resolves it the way
# a browser would. Opens the browser if verification failed.
adb shell am start -W -a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d "https://example.com/products/42"
# 3. DELIVERY AND ROUTING ONLY — explicit package. Always opens the app,
# verified or not. Useful for testing your router, useless for setup.
adb shell am start -W -a android.intent.action.VIEW \
-d "https://example.com/products/42" com.example.shop
# A custom scheme, which is never verified and always resolves this way
adb shell am start -W -a android.intent.action.VIEW \
-d "myapp://products/42"
# See which activity actually resolved, without launching it
adb shell pm resolve-activity --brief \
-a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d "https://example.com/products/42"The -W flag makes am start wait and print the result, which is how you tell a launch from a no-op. The output names the activity that handled the intent — if that is a browser rather than your app, verification is the problem regardless of what the rest of the command did.
For a fuller command reference, including reading the verification state of every app on the device and dumping intent filters, see how to debug deep links with adb.
iOS
# 1. ASSOCIATION — from macOS, the real state and the failure reason
swcutil show --domain example.com
swcutil dl -d example.com
# What Apple's CDN is serving to devices right now
curl -sS https://app-site-association.cdn-apple.com/a/v1/example.com
# 2. DELIVERY AND ROUTING ONLY — bypasses association completely.
# Succeeds on a build with no entitlement and no AASA file.
xcrun simctl openurl booted "https://example.com/products/42"
xcrun simctl openurl booted "myapp://products/42"
# 3. HONEST END-TO-END — there is no command for this on iOS.
# Put the URL in Notes on a physical device and tap it.There is no iOS equivalent of am start without a package — no way to ask the system to resolve a URL as a tap would. That makes the physical-device Notes test not merely the best iOS test but the only real one, and it is why iOS setups reach production broken more often than Android ones.
- Test on hardware, not the simulator. The simulator caches associations differently and does not reliably reflect what a device does.
- Do not test from your own website. iOS deliberately does not open the app for a same-domain tap, so this produces a false failure. See Universal Links not opening the app.
- Do not test from an in-app browser. A link tapped inside Instagram or a chat app never reaches the OS at all.
- Notes, Messages to yourself, or a plain HTML page on a different domain are the contexts that behave like a real user's.
Testing what users actually do
Every command above tests an installed app on a developer's device with the link in a clean context. Most real taps are none of those things, and the gap between the two is where deep linking quietly fails in production.
| Case | Why it differs | How to test |
|---|---|---|
| App not installed | The link falls back to the web, and the destination is lost unless deferred routing carries it | A device that has never had the app |
| First launch after install | Deferred payload arrives asynchronously and may lose a race with your first screen | Play internal testing track, fresh device |
| Cold launch vs. already running | Different entry points in the app; handling one is a common half-fix | Force-quit, then tap. Then background, then tap |
| Tapped inside Instagram or TikTok | The webview never asks the OS to resolve the URL | Post the link to a private story and tap it |
| Tapped from an email | Click tracking rewrites the URL onto the ESP's domain | Send yourself a real campaign, not a plain email |
| Link with query params or a fragment | Path-only matching rules may not cover it | Tap a URL with ?utm_source=…#section |
The email case catches more bugs than any other
Send yourself an actual campaign through your real email platform rather than a hand-written email. Click tracking rewrites the link onto the platform's domain, which is not in your association file, so the tap is a link to their domain and the app never opens. It is invisible in every other test. See deep links in email clients.
deep link debugger
The association layer is the one the local commands cannot test honestly. Enter your domain and it checks what both platforms would fetch — assetlinks.json, the association file, redirect chains, content types, and fingerprints — so the only layer left to test on-device is your own routing.
Frequently asked questions
- How do I test a deep link on Android?
- Run adb shell pm get-app-links followed by your package name to see the real per-domain verification state, then adb shell am start -W -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d followed by the URL, with no package name. Omitting the package is what makes it an honest test: the system resolves the intent the way a browser would, so it opens the browser if verification failed. Including the package always opens the app regardless.
- How do I test a Universal Link on iOS?
- Put the URL in the Notes app on a physical device and tap it. There is no command-line equivalent, because xcrun simctl openurl delivers the URL straight to the app and bypasses domain association entirely — it succeeds on builds with no entitlement and no apple-app-site-association file. To check the association layer itself, run swcutil show --domain yourdomain.com from macOS.
- Why does adb am start open my app when deep links are broken for users?
- Because passing an explicit package name makes it an explicit intent, which matches against the intent filter and never consults domain verification. It tests that your app can receive and route the URL, not that Android will ever send it one. Drop the package name and add the BROWSABLE category for a test that reflects real behaviour.
- Can I test deep links in the iOS Simulator?
- Only partially, and unreliably. The simulator caches domain associations differently from a device, so association results there do not transfer. It is fine for testing that your app routes a URL correctly once it receives one, but any conclusion about whether Universal Links work needs a physical device and a real link tap.
- Why do my deep links work in testing but fail in email campaigns?
- Email platforms rewrite links for click tracking, so the URL the user taps is on the platform's domain and redirects to yours. Neither iOS nor Android resolves redirects before deciding whether an app can handle a URL, so the tap is a link to the tracking domain, which is not in your association file. Testing with a hand-written email misses this entirely — send a real campaign.
- What should I test besides tapping a link?
- Cold launch versus an already-running app, since they arrive through different entry points and handling only one is a common half-fix. Also test with the app not installed, first launch after install for deferred routing, a URL carrying query parameters and a fragment, a tap from inside a social app's in-app browser, and a tap from a real email campaign.
Related terms
- Debugging deep links with adb — adb provides direct access to Android's domain verification state, intent resolution, and package manifest data, which together identify why a deep link opens the browser instead of the app.
- Deep link opens the browser instead of the app — A deep link opens the browser instead of the app when the operating system has not accepted the app as a verified handler for that URL, or when the tap occurred in a context that never offers the app the chance to handle it.
- Deferred deep link not working — A deferred deep link fails when the signal meant to carry the pre-install destination across the app store — an install referrer, a stored token, or a server-side match — is absent, expired, or never read on first launch.