Deeplinkly

Glossary/Failure modes

Android App Links not working

Definition

Android App Links fail when the system cannot verify that a domain and an app belong to the same owner, at which point tapped links open in the browser instead of the app with no error shown to the user.

Verification is a silent process: it runs at install time, it either succeeds or it does not, and nothing surfaces to the user either way. That makes the failure hard to reason about from the outside and trivial to diagnose from the inside — a single adb command tells you the exact verification state of every domain your app claims. Start there, then work down this list.

Get the verification state first

Do not guess. Android will tell you precisely which domains verified and which did not, and the answer usually points straight at the cause.

Read the verification state
adb shell pm get-app-links com.example.shop

# com.example.shop:
#   ID: 8f3c1e40-...
#   Signatures: [AB:CD:EF:...]
#   Domain verification state:
#     example.com: verified
#     www.example.com: 1024
What each domain verification state means.
StateMeaningMost likely cause
verifiedWorking. Links open the app
noneVerification never ranandroid:autoVerify="true" is missing, or the intent filter is malformed
1024Verification ran and failedFingerprint mismatch, or assetlinks.json unreachable
legacy_failureFailed in a way the system will not retryMalformed statement file
migratedCarried over from a pre-Android 12 installRe-verify to get a real answer
Domain absent entirelyThe system does not know you claim itThe host is not in any autoVerify intent filter
Force verification to run again
# Ask the system to re-run verification for every declared domain
adb shell pm verify-app-links --re-verify com.example.shop

# Then re-read the state (verification is asynchronous — give it a few seconds)
adb shell pm get-app-links com.example.shop

The causes, in order of how often they are the real one

1. The SHA-256 fingerprint is from the wrong key. This is the cause more often than everything else combined. If your app is distributed through Google Play with Play App Signing — the default — Google re-signs your app with a key you do not hold. The fingerprint in assetlinks.json must be the app signing key, not the upload key you sign with locally. Find it in Play Console under Test and release → Setup → App signing. The upload certificate is listed on the same screen, which is exactly why the wrong one gets copied.

Get the fingerprint for each build you need to support
# Debug builds — the shared debug keystore
keytool -list -v \
  -keystore ~/.android/debug.keystore \
  -alias androiddebugkey -storepass android -keypass android

# A release keystore you hold
keytool -list -v -keystore release.jks -alias upload

# What Google actually ships: Play Console →
#   Test and release → Setup → App signing → App signing key certificate

List every fingerprint, not one

assetlinks.json takes an array of fingerprints, and a build verifies if it matches any of them. Include the Play app signing key, your release key, and the debug key together — that is one file that works for internal builds, Play builds, and local development, rather than a file you edit whenever you change build type.

/.well-known/assetlinks.json
[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.shop",
      "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",
        "7B:14:12:6C:1A:99:2E:5B:57:B1:0F:D8:D1:C2:24:9A:8D:4C:03:5F:9E:B7:22:A1:44:8E:D0:6C:19:33:F0:2B"
      ]
    }
  }
]

2. `android:autoVerify="true"` is missing. Without it, the intent filter still lets your app appear in the disambiguation dialog, but the system never attempts verification and never makes your app the default handler. The attribute goes on the intent filter, not on the activity, and only needs to appear on one filter per app.

3. The intent filter is incomplete. It needs the VIEW action, both the BROWSABLE and DEFAULT categories, and an https scheme. Missing BROWSABLE means the link cannot be opened from a browser at all — which is the entire use case. Only http and https schemes are ever verified; a custom URI scheme is not an App Link and is not verified.

AndroidManifest.xml
<activity
    android:name=".MainActivity"
    android:exported="true">

    <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" />
        <data android:scheme="https" android:host="www.example.com" />
    </intent-filter>
</activity>

4. `assetlinks.json` is unreachable, redirected, or the wrong content type. It must be at https://example.com/.well-known/assetlinks.json, served as application/json, over a valid certificate, and reachable with no redirects — Google documents that requirement as explicitly as Apple does. The failure is the same shape as an AASA file not found, and usually has the same cause: a canonicalisation rule or a WAF nobody associated with the app. See how to validate assetlinks.json for the checks.

5. You changed the file after the app was installed. Verification runs at install time. Fixing assetlinks.json on the server changes nothing for an app already on the device until it is reinstalled, updated, or explicitly re-verified with pm verify-app-links --re-verify. Most reports of "the fix did not work" are this.

6. The user turned it off, or another app claimed the domain. Under Settings → Apps → your app → Open by default, a user can disable verified link handling, and it stays disabled. On Android 12 and later the same screen shows which links the app is approved to open, which makes it a fast way to confirm what the system thinks independently of adb.

7. One bad host used to fail every host. Before Android 12, if any host in an autoVerify intent filter failed verification, *all* of them failed — so a stale staging domain in the manifest silently broke the production one. Android 12 changed this to per-host verification. If you support Android 11 or earlier, audit the manifest for hosts you no longer serve a statement file from.

8. Verification ran while the device had no usable network. It is a network call at install time, and it can fail for reasons that have nothing to do with your configuration. Android retries, but the retry schedule is not immediate. --re-verify on a connected device rules this in or out in seconds.

9. The package name does not match. package_name in assetlinks.json is the applicationId from your Gradle configuration, which is not necessarily the Java package your source lives in — and build variants with applicationIdSuffix (.debug, .staging) produce a different one for every variant. Each needs its own statement in the file.

Why `adb shell am start` tells you nothing

The near-universal way to test a deep link is also the one that hides the failure you are looking for. Sending an explicit VIEW intent matches on the intent filter alone — it never consults domain verification, so a completely unverified App Link opens the app perfectly and you conclude it works.

Two tests that measure different things
# Tests intent filter matching and your in-app routing only.
# This SUCCEEDS even when domain verification has failed.
adb shell am start -W -a android.intent.action.VIEW \
  -d "https://example.com/products/42" com.example.shop

# Tests what actually happens to a real tap: no package, so the system
# resolves it the way it would from a browser or a chat app.
adb shell am start -W -a android.intent.action.VIEW \
  -c android.intent.category.BROWSABLE \
  -d "https://example.com/products/42"
What each verification method actually proves.
MethodProvesBlind to
am start with the package nameIntent filter matches, routing worksVerification state — always succeeds
am start without the packageEnd-to-end resolutionWhich of the nine causes failed
pm get-app-linksThe exact per-domain verification stateWhether your in-app routing handles the URL
Google's Statement List APIYour assetlinks.json is fetchable and well-formedWhether the fingerprint matches the installed build
Tapping a link in a notes appThe real user pathNothing — but tells you nothing about why

Do not test by tapping a link inside a chat app

Instagram, TikTok, LinkedIn, and most chat apps open links in an embedded webview that never leaves the app, so a correctly verified App Link will still appear not to fire. Test from the Android launcher's browser or a plain notes app, and treat in-app browsers as their own separate problem.

deep link debugger

Point it at your domain and it fetches assetlinks.json, checks the redirect chain and content type, validates the statement shape and relation, and shows the fingerprints the file declares so you can compare them against your Play Console app signing key. It checks the iOS side in the same pass.

Open the deep link debugger

Frequently asked questions

Why do my Android App Links open the browser instead of the app?
Because domain verification failed, so the system does not treat your app as the approved handler for that domain. Run adb shell pm get-app-links <package> to see the per-domain state. A state of 1024 means verification ran and failed — usually a SHA-256 fingerprint mismatch. A state of none means verification never ran, usually because android:autoVerify="true" is missing from the intent filter.
Which SHA-256 fingerprint goes in assetlinks.json?
The fingerprint of the key that signs the app as users receive it. If you use Play App Signing, that is Google's app signing key, found in Play Console under Test and release, Setup, App signing — not your upload key. Using the upload key fingerprint is the single most common cause of App Links failing. The file accepts an array, so listing the app signing key, the upload key, and the debug key together avoids the problem.
Do I need to reinstall the app after fixing assetlinks.json?
Yes, or force re-verification. Domain verification runs at install time and the result is cached, so correcting the file on your server does not change anything for an app already installed. Either reinstall, or run adb shell pm verify-app-links --re-verify <package> and re-check the state.
Why does adb am start open my app even though App Links are broken?
Sending an explicit VIEW intent matches against the intent filter and never consults domain verification, so it succeeds whether or not the domain is verified. It tests your routing, not your setup. To test real resolution, omit the package name and include the BROWSABLE category so the system resolves the intent the way a browser would.
What changed about App Links verification in Android 12?
Verification became stricter and per-host. Before Android 12, one failing host in an autoVerify intent filter caused every host in it to fail, so a stale staging domain could silently break production links. From Android 12 each host is verified independently, and unverified web links no longer fall back to the disambiguation dialog — they simply open in the browser.
Does assetlinks.json need to be at the .well-known path?
Yes. It must be at https://yourdomain.com/.well-known/assetlinks.json, served over HTTPS with a valid certificate and a Content-Type of application/json, and it must be reachable without redirects. Google documents the no-redirect requirement explicitly, so an http-to-https upgrade or an apex-to-www canonicalisation in front of the file will fail verification.

Related terms

  • AASA file not foundAn AASA file not found error means Apple's content delivery network could not retrieve a usable apple-app-site-association file from a domain, which disables Universal Links for that domain entirely.
  • Apple App Site Association (AASA)The apple-app-site-association file is a JSON document hosted at a domain's /.well-known/ path that tells iOS which app is allowed to handle which URLs on that domain.