Android App Links autoVerify Failed: The assetlinks.json Verification Debug Checklist
App Links opening a chooser dialog instead of your app? Work through this assetlinks.json autoVerify debug checklist: the Play App Signing SHA-256 trap, package_name, the intent-filter, multi-host cascade, and the adb verification commands.
TL;DR: When
autoVerifyfails and Android App Links open a chooser dialog (or the browser) instead of your app, the cause in production is almost always a SHA-256 certificate fingerprint mismatch. If you use Play App Signing, Google re-signs your app with a different key than your upload key, soassetlinks.jsonmust list the app signing key SHA-256 from Play Console under App integrity, not your local or upload keystore fingerprint. That is why it works in debug and silently breaks in production. Start with one command:curl -sI https://yourdomain.com/.well-known/assetlinks.json. If that is not a clean200withapplication/jsonand no redirect, fix the server first. Then check the fingerprint. The rest of this checklist covers the other failure modes, in the order worth checking them.
Why autoVerify Fails Silently
Android App Links fail the same quiet way Universal Links do on iOS. There is no crash, no error toast. The link just opens a browser or pops a "Open with" chooser dialog instead of launching your app, and you are left guessing whether the problem is your assetlinks.json file, your manifest, your signing certificate, or the device's verification cache.
A developer writing on Medium in late 2025 captured the exact shape of it: "Okay, here's where I wasted like 2 days of my life... the annoying SHA-256 certificate issue that had me pulling my hair out for days." That sentence is the whole post in miniature. The setup looked correct, the file was live, the intent filter was there, and verification still failed, because the one fingerprint in the file was the wrong one.
This is the part that makes the bug so cruel: it works perfectly when you build and run from your machine, then fails the moment the app ships through the Play Store. Your local build is signed with your debug or upload key. The build Google distributes is signed with a different key. If your assetlinks.json only carries the local fingerprint, autoVerify passes in debug and fails in production, and the symptom (a chooser dialog) gives you no hint why.
This guide is the checklist. Work it top to bottom. Each step is a command you can run or a value you can check, ordered so the highest-probability causes come first. For the iOS equivalent, the Apple App Site Association debug checklist is the sister guide.
Step 1: Confirm the File Is Reachable and Correct
The Digital Asset Links file must live at exactly this path, served over HTTPS:
https://yourdomain.com/.well-known/assetlinks.json
Unlike the iOS AASA file, this one does have a .json extension, and it must sit inside /.well-known/. Check it with curl:
curl -sI https://yourdomain.com/.well-known/assetlinks.json
You are looking for four things in the response:
- Status
200. Not301, not302, not404. Google's verification requires the file to be reachable without any redirects: a 301 or 302 fails verification outright. If your server forceswww.or appends a trailing slash, the fetch fails and verification never completes. This is the most common server-side cause. Runcurl -sILto see the full redirect chain, then remove the redirect so the file resolves with a direct200. content-type: application/json. Servingtext/plainortext/htmlbreaks verification. It must beapplication/json.- No authentication. Basic auth, a bot-protection challenge, or a login wall blocks the verifier. The
.well-knownpath has to be public. - A valid certificate. The HTTPS certificate must be valid and trusted. Self-signed or expired certificates fail, and unlike a browser there is no "proceed anyway."
Then confirm the body is valid JSON:
curl -s https://yourdomain.com/.well-known/assetlinks.json | jq .
If jq errors, the file is not valid JSON. A trailing comma, a stray comment, or a byte-order mark at the top of the file will all break parsing. The format is a JSON array of statement objects:
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.myapp",
"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"
]
}
}
]
Step 2: The SHA-256 Fingerprint Trap (Read This One Twice)
This is the cause that eats days, and the one to suspect first whenever the link works in debug and fails in production. The sha256_cert_fingerprints array has to contain the SHA-256 of the certificate that actually signs the app on the user's device. Get the wrong certificate in there and everything else can be perfect while verification still fails.
Upload key vs App Signing key
If you enrolled in Play App Signing (the default for new apps), there are two different keys in play:
- Your upload key. You sign the artifact you upload to the Play Console with this. It proves the upload is from you.
- Google's app signing key. Google re-signs the app with this before distributing it. This is the certificate on the device, and this is the one Android checks during verification.
These are different keys with different fingerprints. The trap is putting the upload key fingerprint in assetlinks.json when the device sees the app signing key. To get the right value, open the Play Console, go to App integrity > Play app signing, and copy the SHA-256 from the App signing key certificate section. Do not copy the Upload key certificate value. Paste carefully: a single wrong character fails verification with no useful error.
Debug vs release
Your local debug builds are signed with the debug keystore, which has yet another fingerprint. Pull it with:
keytool -list -v -keystore ~/.android/debug.keystore \
-alias androiddebugkey -storepass android -keypass android
For a release signed locally (no Play App Signing), read the fingerprint from your release keystore:
keytool -list -v -keystore release.jks -alias your-alias
Or read it straight from a built APK with apksigner (the most reliable way to inspect what actually signed an APK):
apksigner verify --print-certs app-release.apk
List every fingerprint you need
sha256_cert_fingerprints is an array on purpose. List every certificate that signs a build you want to verify: the Play app signing key, your upload key (some flows still check it), and your debug key if you want App Links to verify on debug builds too.
"sha256_cert_fingerprints": [
"AA:BB:...:Play app signing key",
"CC:DD:...:upload key",
"EE:FF:...:debug key"
]
Multiple apps that share a domain (a free and a paid flavor, or several package_name variants) each need their own statement object in the array. Add a second object rather than cramming two packages into one.
A faster way to generate a correct file: Google's Statement List Generator takes your domain, package name, and fingerprint and outputs the exact JSON. Use it to sanity-check what you deployed.
Step 3: Verify the package_name
A small one that is easy to overlook. The package_name in assetlinks.json must exactly match your app's applicationId, not the manifest package attribute if you remap it, and not a flavor suffix you forgot about.
If you ship build flavors, the applied ID can differ from what you read in the manifest:
// build.gradle.kts
android {
defaultConfig { applicationId = "com.example.myapp" }
productFlavors {
create("pro") { applicationIdSuffix = ".pro" } // -> com.example.myapp.pro
}
}
If the device runs the pro flavor, the verifier looks for com.example.myapp.pro and your file lists com.example.myapp, so it fails. Confirm the installed ID with adb shell pm list packages | grep example, and make sure the file lists exactly that.
Step 4: The intent-filter Needs autoVerify and the Exact Host
Verification only runs for intent filters that opt in. Your AndroidManifest.xml must set android:autoVerify="true" on a filter that declares the https scheme and the exact host:
<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="yourdomain.com" />
<data android:pathPrefix="/product" />
</intent-filter>
Three things break here:
- Missing
autoVerify. Withoutandroid:autoVerify="true", Android never fetches yourassetlinks.jsonat all. The link still works, but it shows the chooser dialog instead of opening your app directly. NoautoVerify, no verification. - Host mismatch.
android:host="yourdomain.com"does not coverwww.yourdomain.comorlinks.yourdomain.com. The host in the filter, the host in your links, and the domain servingassetlinks.jsonall have to be the same string. A wildcard host like*.yourdomain.comrequires a reachable, correctassetlinks.jsonat each subdomain you actually link to. - Wrong filter shape. Verification only runs for a filter that uses the
httporhttpsdata scheme (not a custom scheme) and includes theVIEWaction plus theBROWSABLEandDEFAULTcategories shown above. If any of those are missing, the system never treats the filter as an App Link and skips verification, so the host falls back to the chooser.
Step 5: The Multi-Host Failure Cascade
This one surprises people. Before Android 12, App Link verification was all-or-nothing across your manifest. Android collected every host from every autoVerify filter, then verified them as a set. If any declared host failed (one typo, one domain without a matching assetlinks.json), verification could fail for all of them, including the hosts that were configured perfectly.
So if you declare three hosts:
<data android:scheme="https" android:host="yourdomain.com" />
<data android:scheme="https" android:host="go.yourdomain.com" />
<data android:scheme="https" android:host="share.yourdomain.com" />
then all three need a valid assetlinks.json reachable at their own /.well-known/assetlinks.json, each listing the right fingerprint. Curl every one:
for h in yourdomain.com go.yourdomain.com share.yourdomain.com; do
echo "== $h =="
curl -sI "https://$h/.well-known/assetlinks.json" | head -n 1
done
Android 12 made verification per-host, which softens this, but you still cannot afford a broken host. A failed host means that host falls back to the chooser dialog, and on devices that have not yet shipped the per-host behavior, it can still drag down the rest. List only hosts you can actually serve the file from.
Step 6: Read the Device Verification State with adb
Once the server side checks out, ask the device what it actually recorded. This is the single most useful command in the whole checklist:
adb shell pm get-app-links com.example.myapp
The output lists each declared domain and its verification state, something like:
com.example.myapp:
ID: 01234567-89ab-cdef-...
Signatures: [...]
Domain verification state:
yourdomain.com: verified
go.yourdomain.com: 1024
Read the state next to each domain:
verifiedis what you want. The domain passed and the app handles its links directly.nonemeans nothing was recorded yet. The verifier has not run, or the filter is missingautoVerify.1024(or any number at or above 1024) is a device-verifier error code. The fetch or the fingerprint check failed. Go back to Step 1 and Step 2.legacy_failuremeans a legacy verifier rejected it for an unknown reason. Common on Android 12 setups carried over from older verification. Reset and re-verify (Step 7).approved/deniedmean it was force-set through the shell, not earned through verification. Useful for testing, but not how production should look.migrated/restoredmean the state was preserved from a legacy verification result or a user data restore, not freshly verified.system_configuredmeans the device configuration approved the domain automatically. Not something you set, and not an error.
To force the device to re-run verification without reinstalling:
adb shell pm verify-app-links --re-verify com.example.myapp
Give it a few seconds, then run pm get-app-links again to see the updated state. Re-verification needs network access and can take a moment.
Step 7: Reset Verification on Android 12 and Later
Android 12 reworked domain verification, and stale state is a frequent reason a correct setup still shows the wrong result. The device may be holding a legacy_failure or an old 1024 from before you fixed the file. Clear it and re-verify:
# Reset the recorded state for all of this package's domains
adb shell pm set-app-links --package com.example.myapp 0 all
# Trigger a fresh verification pass
adb shell pm verify-app-links --re-verify com.example.myapp
# Confirm
adb shell pm get-app-links com.example.myapp
You can also confirm the per-domain "Open by default" setting through the UI: Settings > Apps > your app > Open by default > Add link. On Android 12 and later, a verified link shows as supported there. If the toggle is empty, verification did not land, which points you back to the fingerprint or the file.
One more Android 12 note: the verifier is stricter about the server. A redirect, a missing application/json content type, or an unreachable host that older Android tolerated will now produce legacy_failure. Step 1 is not optional on modern Android.
Step 8: Check the Digital Asset Links API Directly
Google runs the verification through its own Digital Asset Links API, and you can query the exact same endpoint the verifier uses. This tells you what Google's infrastructure sees, independent of any one device:
curl -s "https://digitalassetlinks.googleapis.com/v1/statements:list?\
source.web.site=https://yourdomain.com&\
relation=delegate_permission/common.handle_all_urls" | jq .
A healthy response lists your statement with the package_name and the sha256_cert_fingerprints you deployed. If the response is empty or missing your package, the API could not fetch or parse your file, which sends you back to Step 1. If the fingerprint in the response is not the one your device is signed with, you have the Step 2 mismatch in black and white.
The Short Version
App Link opens a chooser / browser, not the app
│
▼
curl -sI the assetlinks.json URL ──► not 200 / redirect / wrong content-type? ──► fix the server (Step 1)
│ ok
▼
SHA-256 = Play APP SIGNING key, not upload key? ──► no ──► copy from Play Console > App integrity (Step 2)
│ yes
▼
package_name matches the installed applicationId (incl. flavors)? ──► no ──► fix it (Step 3)
│ yes
▼
intent-filter has autoVerify="true" + exact host? ──► no ──► fix the manifest (Step 4)
│ yes
▼
every declared host serves a valid file? ──► no ──► fix or drop the broken host (Step 5)
│ yes
▼
adb pm get-app-links shows "verified"? ──► no (1024 / legacy_failure / none)
│ │
│ yes = done ▼
│ reset + re-verify on Android 12+ (Step 6, 7)
▼
still failing? ──► query the Digital Asset Links API (Step 8)
How WarpLink Removes Half of This Checklist
Steps 1 and 8 are entirely about hosting a byte-perfect assetlinks.json: right path, right content type, no redirects, valid JSON, reachable by Google's verifier. That is the half of the checklist that has nothing to do with your app and everything to do with your server.
When you register an app with WarpLink, it generates the assetlinks.json from the package name and SHA-256 fingerprints you provide and serves it for you, at the edge, with Content-Type: application/json, no redirects, and no auth wall, on both your WarpLink link domain and any custom domain you add. The hosting failure modes in Step 1 do not happen on a WarpLink domain.
What WarpLink cannot do for you is know which certificate signs your production build. You still own the app side: the autoVerify intent filter, the exact host, and registering the correct SHA-256, which means the Play App Signing key from Play Console for a Play-distributed app, not your upload or debug key. That is Step 2, and it stays yours no matter who hosts the file. This guide is vendor-neutral on purpose: whether you host assetlinks.json yourself or let a service do it, the checklist is the same. WarpLink just deletes the rows that come from getting the hosting wrong.
The SDK side is one call. You pass the incoming intent URI to WarpLink.handleDeepLink(uri) from onCreate() and onNewIntent(), and it returns the destination, custom parameters, and attribution data on the main thread, for both cold and warm start. The full setup is in the Android deep links docs.
Frequently Asked Questions
Why do my Android App Links work in debug but not in production?
Almost always the SHA-256 fingerprint. Your debug build is signed with your debug or upload key, but Play App Signing re-signs the distributed app with Google's signing key, which has a different fingerprint. Your assetlinks.json lists the wrong one, so autoVerify passes locally and fails in the Play Store build. Copy the App signing key SHA-256 from Play Console under App integrity > Play app signing and add it to the file.
Where do I get the correct SHA-256 for assetlinks.json?
For a Play-distributed app, copy it from Play Console: App integrity > Play app signing, under "App signing key certificate" (not "Upload key certificate"). For a locally signed release, read it from your release keystore with keytool -list -v -keystore release.jks -alias your-alias, or from a built APK with apksigner verify --print-certs app-release.apk. List every signing key you use in the sha256_cert_fingerprints array.
What does state 1024 or legacy_failure mean in pm get-app-links?
1024 (or higher) is a device-verifier error: the fetch or the fingerprint check failed, so re-check the file (Step 1) and the SHA-256 (Step 2). legacy_failure means a legacy verifier rejected it, common on Android 12 carrying stale state. Reset with adb shell pm set-app-links --package <pkg> 0 all and re-verify.
Why does autoVerify show a chooser dialog instead of opening my app?
Either the intent filter is missing android:autoVerify="true" (so Android never verifies and defaults to the chooser), or verification ran and failed. Run adb shell pm get-app-links <package>: none points to a missing autoVerify, while 1024 or legacy_failure points to the file or the fingerprint.
How do I force Android to re-verify App Links without reinstalling?
Run adb shell pm verify-app-links --re-verify <package>, then check the result with adb shell pm get-app-links <package>. On Android 12 and later, reset stale state first with adb shell pm set-app-links --package <package> 0 all. Re-verification needs network access. Reinstalling still works as a fallback, since Android re-verifies at install time.
Related Guides
- Start here: Deep Linking: The Complete Guide for Mobile Developers covers how App Links, Universal Links, and deferred deep links fit together, including the App Links section.
- The iOS sister checklist: Apple App Site Association Not Working: The iOS Universal Links Debug Checklist is this same checklist for the AASA file and Team ID prefix.
- Broader iOS symptom: Universal Links Not Opening: Every Cause and How to Fix Each One triages the file, the entitlement, and the cold-start handler on iOS.
- Reference: the Android deep links docs and the deep linking concepts page.
Once your App Links verify and open the right screen reliably, the next question is usually attribution: which link, campaign, or channel actually drove the open and the install, and how those opens show up in your analytics. That is a different problem from verification, and it is the other two pillars, attribution and analytics, that WarpLink handles after the link resolves.
Create a free WarpLink account and let the assetlinks.json host itself, so you can get back to the half of this checklist that is actually about your app.
WarpLink Team
Building the open deep linking platform for developers and small teams.
Related Posts
Apple App Site Association Not Working: The iOS Universal Links Debug Checklist
Universal Links opening Safari instead of your app? Work through this apple-app-site-association debug checklist: AASA fetch, Apple's CDN cache, appIDs, components, and the device-side gotchas.
Deep Linking: The Complete Guide for Mobile Developers
What is deep linking? Learn how universal links, app links, and deferred deep links work. Covers iOS, Android, and cross-platform implementation.
Deferred Deep Linking on iOS: How to Implement It in Swift
A vendor-neutral Swift walkthrough of deferred deep linking on iOS: why iOS has no native support, the match cascade (IDFV, fingerprint, raw signals), the ATT and Private Relay reality, and a one-call first-launch implementation.