deep-linking

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.

WarpLink Team··16 min read

TL;DR: When autoVerify fails 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, so assetlinks.json must 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 clean 200 with application/json and 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:

Unlike the iOS AASA file, this one does have a .json extension, and it must sit inside /.well-known/. Check it with curl:

You are looking for four things in the response:

  1. Status 200. Not 301, not 302, not 404. Google's verification requires the file to be reachable without any redirects: a 301 or 302 fails verification outright. If your server forces www. or appends a trailing slash, the fetch fails and verification never completes. This is the most common server-side cause. Run curl -sIL to see the full redirect chain, then remove the redirect so the file resolves with a direct 200.
  2. content-type: application/json. Serving text/plain or text/html breaks verification. It must be application/json.
  3. No authentication. Basic auth, a bot-protection challenge, or a login wall blocks the verifier. The .well-known path has to be public.
  4. 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:

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:

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:

For a release signed locally (no Play App Signing), read the fingerprint from your release keystore:

Or read it straight from a built APK with apksigner (the most reliable way to inspect what actually signed an 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.

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:

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:

Three things break here:

  • Missing autoVerify. Without android:autoVerify="true", Android never fetches your assetlinks.json at all. The link still works, but it shows the chooser dialog instead of opening your app directly. No autoVerify, no verification.
  • Host mismatch. android:host="yourdomain.com" does not cover www.yourdomain.com or links.yourdomain.com. The host in the filter, the host in your links, and the domain serving assetlinks.json all have to be the same string. A wildcard host like *.yourdomain.com requires a reachable, correct assetlinks.json at each subdomain you actually link to.
  • Wrong filter shape. Verification only runs for a filter that uses the http or https data scheme (not a custom scheme) and includes the VIEW action plus the BROWSABLE and DEFAULT categories 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:

then all three need a valid assetlinks.json reachable at their own /.well-known/assetlinks.json, each listing the right fingerprint. Curl every one:

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:

The output lists each declared domain and its verification state, something like:

Read the state next to each domain:

  • verified is what you want. The domain passed and the app handles its links directly.
  • none means nothing was recorded yet. The verifier has not run, or the filter is missing autoVerify.
  • 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_failure means 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 / denied mean it was force-set through the shell, not earned through verification. Useful for testing, but not how production should look.
  • migrated / restored mean the state was preserved from a legacy verification result or a user data restore, not freshly verified.
  • system_configured means 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:

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:

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.

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:

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

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.

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