← Back to Blog

Deep Link Not Working? How to Debug Universal Links and App Links

A symptom-first guide to debugging Universal Links and App Links: find the failure class, validate AASA and assetlinks.json, and verify with adb and curl.

Quick answer: A deep link that "doesn't work" is almost always one of four failure classes, and each has a fast check. If the link opens the browser instead of your app, the domain-to-app association is broken: validate the apple-app-site-association (AASA) file on iOS or assetlinks.json on Android, and confirm the app side claims the domain. If the app opens but lands on the wrong screen, the association is fine and your route mapping is dropping or misreading the path. If it works in development but not production, your release build is signed with a different certificate than the one in your verification file. And if it worked yesterday and broke after a release, re-run verification - both platforms cache the association, so a changed file is not picked up until the OS re-fetches it.

$ cat table-of-contents

  1. Which symptom are you seeing?
  2. How do you check the AASA file on iOS?
  3. How do you check assetlinks.json on Android?
  4. Why does the app open on the wrong screen?
  5. Why does it work in dev but not in production?
  6. The command walkthrough
  7. If LinkTrace hosts your verification files
  8. Where LinkTrace fits
  9. Frequently asked questions

Which symptom are you seeing?

Debugging deep links goes wrong in one predictable way: people edit files before they know which layer is broken. A tap on a link passes through four layers - the OS deciding whether your app owns the domain, the verification file on your domain, the entitlement or manifest inside your app, and your app's own routing code once it opens. Each symptom below points at a different layer, and the fastest fix is to test the layer the symptom implicates before touching anything else.

SymptomLikely failure classFirst check
Link opens the browser, app stays closedDomain association broken or never verifiedFetch the AASA / assetlinks.json with curl; check the OS verification state
App opens on the home screen, not the linked screenRoute mapping - the app never reads or never matches the pathLog the URL your handler actually receives
Works on one OS, fails on the otherOnly one platform's association is configured correctlyVerify each platform independently; they share nothing
Works in dev, fails in production (or the reverse)Different signing certificates between buildsCompare the cert fingerprints each build is actually signed with
Worked before, broke after a release or domain changeStale OS cache, changed paths, or a dropped entitlementForce the OS to re-verify; diff what changed in the release

One symptom deserves its own note before the tree: "link opens the browser instead of the app" can also mean the tap never reached the OS at all - you pasted the URL into an address bar, tapped it inside an in-app browser that suppresses app hand-off, or tapped a link on the same domain you were already browsing. Universal Links deliberately do not fire in those contexts. Our Universal Links vs App Links guide has the full cause table and test matrix for that symptom; this post picks up once you have confirmed the OS should have routed the tap and did not.

How do you check the AASA file on iOS?

The AASA file is the whole association on the domain side. Apple's own troubleshooting procedure (Technical Q&A QA1916) starts here, and so should you. The checks, in order:

  1. It is served correctly. The file must come back with HTTP 200, a JSON content type, and no redirect of any kind - Apple's fetcher will not follow one. Check both locations, because iOS looks in /.well-known/apple-app-site-association and at the root /apple-app-site-association:

TERMINAL

curl -sI https://yourdomain.com/.well-known/apple-app-site-association
# want: HTTP 200, content-type: application/json, no 3xx
  1. The appID is right. Each entry is TEAMID.bundleID - your Apple Developer Team ID, a dot, your bundle identifier. QA1916 calls this out specifically: the value must match the application-identifier entitlement in your signed app, and it can differ from what you expect after an app transfer or with non-team code signing.
  2. The paths match the URL you are testing. With the modern components syntax you match on path fragments, query items, and fragments; with the legacy paths array it is prefix matching. If neither matches the exact link you tapped, iOS opens Safari by design.
  3. The device actually re-fetched it. iOS downloads the AASA at install time, not at tap time. QA1916's diagnostic procedure is built on this: delete the app, reinstall, and confirm in your server logs (or a proxy) that the AASA was requested during install. If you edited the file without reinstalling, the phone is still using the old copy.
  4. Check Apple's cached copy. Devices are served the AASA through Apple's CDN, which can lag your deploy. Compare what Apple serves at https://app-site-association.cdn-apple.com/a/v1/yourdomain.com against the file you just deployed - a mismatch means the fix is propagation time, not your file.
  5. The user did not opt out per-domain. Long-pressing a link and choosing to open it in Safari disables Universal Links for that domain on that device. QA1916's fix: open the link, let Safari load, pull down on the page to reveal the Smart App Banner, and tap OPEN to re-enable.

Android's association file lives at https://<domain>/.well-known/assetlinks.json, and Android's troubleshooting guide lists the same serving discipline as iOS: valid JSON, over HTTPS, no redirects - including http-to-https and apex-to-www hops, both of which silently fail verification. After that, three Android-specific checks catch nearly everything:

  1. The SHA-256 certificate fingerprint matches the build that is installed. The sha256_cert_fingerprints value must match the key that actually signed the APK or AAB on the phone. If you use Play App Signing, the fingerprint that matters is the one Play Console shows under App Integrity - not your local upload key. The fingerprint is uppercase hex with colons.
  2. The intent filter asks for verification. Without android:autoVerify="true" on the intent filter, Android never runs domain verification at all, and every tap falls through to the browser or the disambiguation dialog.
  3. Verification actually ran. Android verifies at install and then caches the result. You can force a fresh run and read the state directly - commands in the walkthrough below.

Why does the app open on the wrong screen?

This is the failure class that file-level debugging never finds, because every file is correct: the OS hands the link to your app, and your app drops it on the home screen. Three distinct causes sit in this class:

  1. The pattern matches less than you think. Your AASA components or your manifest pathPattern covers /promo but the link is /promo/SAVE20. The OS made no promise here - tighten or widen the patterns deliberately and re-test with the real URLs your product shares.
  2. The handler never runs. On iOS, if application(_:continueUserActivity:restorationHandler:) returns false - because your parsing decided it could not use the URL - the app opens but nothing happens, and QA1916 names exactly this as a failure step. On Android the equivalent is an activity that exports the filter but never reads intent.data.
  3. The cold-start path forgets the URL. Warm start (app already running) routes fine, but a cold start consumes the intent to build the back stack and the deep link is lost. The tell: works when the app is in the app switcher, fails when it is force-closed.

The diagnostic for all three is the same: log the raw URL at the entry point, before any parsing. If nothing is logged, the association layer is still suspect; if the URL arrives intact, the bug is in your router and the rest of this post is moot.

Why does it work in dev but not in production?

Nearly every instance of this is a certificate mismatch. Debug builds on Android are signed with the debug keystore; release builds with your release key or Play's app-signing key. Those are different SHA-256 fingerprints, and assetlinks.json entries that only carry one of them will only verify for one build type. Stack Overflow is full of this exact report, and the fix is to list every fingerprint you ship with - debug, release, and the Play Console app-signing cert - in the one file.

The iOS version of the same class is subtler: the AASA is fetched at install, so a device that installed before your fix keeps the old association until you reinstall. A build that "works on my phone" may simply be the phone that installed after the fix shipped. For LinkTrace users there is one more wrinkle, covered below: stage and production identifiers are configured separately, and pointing a stage build at production identifiers (or the reverse) reproduces this symptom exactly.

The command walkthrough

Everything above reduces to a handful of commands. Run them in this order and stop when one fails:

1. DOMAIN FILES - BOTH PLATFORMS

curl -sI https://yourdomain.com/.well-known/apple-app-site-association
curl -sI https://yourdomain.com/.well-known/assetlinks.json
# both: HTTP 200, JSON content-type, no 3xx

2. ANDROID - FORCE RE-VERIFICATION AND READ STATE

adb shell pm verify-app-links --re-verify your.package.name
# wait a few minutes for the verification agent, then:
adb shell pm get-app-links your.package.name
# want: your domain listed as "verified"
# any other state (legacy_failure, a numeric code) = association failed

3. ANDROID 17+ - SEE HOW A URL RESOLVES

adb shell am start --debug-link -a android.intent.action.VIEW -d "https://yourdomain.com/the/exact/link"
# prints App Link Resolution Debug: candidate apps, matched intent filters,
# and which assetlinks.json rules were evaluated

4. IOS - APPLE'S QA1916 PROCEDURE, SHORTENED

# 1. delete the app from a real device (forces a fresh AASA fetch)
# 2. reinstall from Xcode or TestFlight; watch server logs for the AASA request
# 3. email the exact link to the device; long-press it in Mail
# 4. "Open in <your app>" should appear - if it does not, the association failed
# if Safari opens, pull down for the Smart App Banner and tap OPEN

5. IOS - COMPARE APPLE'S CACHED COPY

curl -s "https://app-site-association.cdn-apple.com/a/v1/yourdomain.com"
# this is what devices actually receive; diff it against your deployed file

If LinkTrace hosts your verification files

With LinkTrace, the domain side of this whole post is hosted for you: the AASA and assetlinks.json files for your branded subdomain (<your-brand>.linktrace.in) are built from the identifiers you enter in Settings > Deep Linking and served correctly - right location, right content type, no redirects. When a LinkTrace link does not open the app, check in this order:

  1. The identifiers. Team ID and Bundle ID on iOS; package name and SHA-256 fingerprint(s) on Android, in Settings > Deep Linking. A typo here produces exactly the "opens the store even though the app is installed" symptom.
  2. The environment toggle. Production and stage builds are configured separately. If your stage build fails but production works, you are testing against identifiers entered for the other environment.
  3. The hosted file itself. Fetch https://<your-brand>.linktrace.in/.well-known/apple-app-site-association and /.well-known/assetlinks.json with the curl commands above and confirm they contain your app's identifiers.
  4. The app side, which is still yours. LinkTrace serves the files, but your app must still claim the domain: the Associated Domains capability with your branded subdomain on iOS, and the autoVerify intent filter on Android. A perfectly served verification file cannot fix a missing entitlement.
  5. The OS cache. After correcting anything, reinstall the app (iOS) or force re-verification (Android) before concluding the fix did not work.

Where LinkTrace fits

Most of this post is work you do once per domain and then re-do in a panic after a release. LinkTrace's deep linking exists to shrink the domain side of that: verification files are hosted and served correctly on your branded subdomain, identifiers are entered once in Settings with stage and production kept separate, and the same links carry deferred deep linking and attribution, so a link that opens the app today also lands a new installer on the right screen tomorrow. The app-side claim - one capability on iOS, a few manifest lines on Android - stays with you either way; no provider can do that part for you.

Frequently asked questions

Why does my universal link open Safari instead of my app?

Two families of causes. First, context: Universal Links do not fire from the address bar, from a page on the same domain, or inside most in-app browsers - retest from Mail or Notes. Second, association: the AASA file is unreachable, mis-formatted, has the wrong appID, or its paths do not match the URL. Work the AASA checklist in this post, and see the cause table in Universal Links vs App Links for the context cases.

Why does my Android link open the Play Store instead of the app?

For users without the app, the store is correct. For users with the app installed, verification failed: the SHA-256 fingerprint in assetlinks.json does not match the signing certificate of the installed build (check the Play Console app-signing cert if you use Play App Signing), autoVerify is missing from the intent filter, or the file is not served cleanly. Force re-verification with adb shell pm verify-app-links --re-verify and read the state with adb shell pm get-app-links.

Should I test deep links on a real device?

Yes for anything you intend to ship. Domain verification, AASA fetching, and per-user link choices all live at the OS level, and Apple's QA1916 diagnostic procedure is written for a physical device precisely because that is where the real fetch and cache behavior happens. Simulators are fine for exercising your route handler; they are not evidence the association works.

Why did my deep links break after a release?

The usual suspects: the release build is signed with a different certificate than the one in your verification file (Android), the Associated Domains entitlement or intent filter was dropped or edited in the release, your AASA path rules no longer match a changed URL scheme, or the domain's files moved behind a redirect. Diff the release against the last working one for any of these, then force the OS to re-verify before judging the fix.

How long do the platforms cache my verification files?

Neither publishes a firm TTL, so treat both as "cached until the OS decides to re-fetch" and do not rely on propagation time in either direction. iOS fetches the AASA at install, which is why reinstalling is the reliable refresh. Android verifies at install and re-checks periodically; pm verify-app-links --re-verify forces it now. Apple's CDN copy of your AASA can lag a fresh deploy - check it before assuming your edit went live.

Do I need to redo any of this if LinkTrace hosts my files?

The domain-side work - file location, format, content type, no redirects - is handled. What remains yours is entering the correct identifiers per environment in Settings, claiming the domain inside your app, and remembering that OS caches mean "reinstall or re-verify" after any change.

If you remember three things

  • Name the failure class before you edit a file. Browser instead of app is an association problem; wrong screen is a routing problem; dev-works-prod-fails is a certificate problem; broke-after-release is a cache-or-diff problem. Each has one first check.
  • The verification files have serving rules, not just content rules. Both platforms refuse files served with redirects, and Android refuses http-to-https and apex-to-www hops. curl the files exactly as the platforms fetch them.
  • The OS caches, so "I fixed it" is not "it works". Reinstall on iOS, force re-verification on Android, and compare Apple's CDN copy of your AASA before you conclude anything.

Sources

Platform behavior and commands in this guide were checked against the following pages on 30 October 2026.


LinkTrace is a deep linking and install attribution service for startups and app developers. Two HTTPS calls, flat pricing, no SDK. Create a free account or see the plans.

Related guides: What Is Deep Linking?, Universal Links vs App Links, Deep Links in In-App Browsers, How to Open Your App from a WhatsApp Link, and Deferred Deep Links: Implementation Guide