Android · App Links
Android App Links not verifying: fix the assetlinks.json SHA-256 fingerprint mismatch
Your assetlinks.json is in the right place, the JSON is valid, the package name is correct — and your App Links still open in Chrome instead of your app. Nine times out of ten the culprit is one value: the SHA-256 signing fingerprint no longer matches the key your app is actually signed with.
The reason it is so common is Play App Signing: Google re-signs your app with a key you may have never looked at, and the fingerprint that matters is that key's — not the upload key on your machine.
The symptom
- Tapping a verified-domain link opens the browser, not your app.
autoVerify="true"is set, but the domain shows as not verified.- It worked with a local debug APK, then broke once you shipped through Play.
- A second app/flavor verifies fine; this one doesn't.
What the file has to say
Android reads Digital Asset Links from exactly this URL, over HTTPS, served directly, as a JSON array of statements:
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.yourcompany.yourapp",
"sha256_cert_fingerprints": [
"AB:CD:EF:12:34:56:78:90:...:FF"
]
}
}
]Three things must all be true:
- The statement grants the relation
delegate_permission/common.handle_all_urls(missing it →ASSETLINKS_NO_HANDLE_ALL_URLS). package_namematches your application ID exactly (ASSETLINKS_PACKAGE_MISMATCHif not).sha256_cert_fingerprintscontains the SHA-256 of the cert your app is signed with in production (ASSETLINKS_FINGERPRINT_DRIFTif the expected one is missing).
It is the third that bites.
Why the fingerprint drifts: Play App Signing
When you enroll in Play App Signing (the default for new apps), there are two keys:
- Your upload key — what you sign the AAB with locally.
- Google's app signing key — what Google re-signs the app with before distributing it to users.
Users install the app signed with Google's key. So the fingerprint Android checks at verification time is the app signing key's SHA-256 — and if you generated assetlinks.json from your local keystore, it will be wrong even though it looks legitimate. This is also why it breaks after a migration: enrolling an existing app into Play App Signing, rotating the key, or different flavors using different keys all change which fingerprint users' installs present.
Get the fingerprint that actually matters
The source of truth is the Play Console, not your laptop:
Play Console → your app → Test and release → App integrity → App signing → copy the SHA-256 certificate fingerprint under App signing key certificate.
Google even gives you a ready-made assetlinks.json on that screen (Digital Asset Links JSON) — use it; it already has the correct fingerprint. To compute one from a keystore or the artifact yourself:
# From a keystore
keytool -list -v -keystore my-release-key.jks -alias my-alias | grep SHA256
# Straight from the artifact users run
keytool -printcert -jarfile app-release.aab | grep -i sha256List every key that can sign an install
You usually want more than one fingerprint, because installs can be signed by different keys:
- The Play app signing key (production installs).
- The upload key (some internal-testing flows).
- Your debug key (
~/.android/debug.keystore) if you verify debug builds.
Add all of them to sha256_cert_fingerprints. The check passes as long as the install's key is present; listing extras is safe.
Diagnose on a device (Android 12+)
On Android 12 and newer, you can inspect and force domain verification with adb:
# Show verification state for each declared domain
adb shell pm get-app-links com.yourcompany.yourapp
# Re-trigger verification
adb shell pm verify-app-links --re-verify com.yourcompany.yourapp
# Manually approve locally to confirm the intent-filter side is correct
adb shell pm set-app-links --package com.yourcompany.yourapp 1 allIf the domain becomes verified only after the manual set-app-links, your manifest intent filter is fine and the failure is server-side — the assetlinks.json fingerprint. Confirm the manifest has an autoVerify intent filter for the domain:
<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" />
</intent-filter>Don't forget the delivery
Like iOS's AASA, the Android verifier reads assetlinks.json directly and does not follow redirects. The same apex ↔ www 301 trap applies (ASSETLINKS_REDIRECT), as does serving HTML/login pages to anonymous clients. Quick check, again without -L:
curl -sS -D - -o /dev/null \
https://yourdomain.com/.well-known/assetlinks.jsonYou want a direct 200 with a JSON array body. A 3xx, a 404, or an HTML body all mean the file never reaches Google's verifier.
Fix checklist
- Copy the App signing key SHA-256 from Play Console → App integrity (not your local keystore).
- Put it (plus upload/debug keys if you test those) in
sha256_cert_fingerprints. - Confirm
relationincludesdelegate_permission/common.handle_all_urlsandpackage_namematches. - Serve the file at
/.well-known/assetlinks.jsonas a direct 200, JSON array, no redirect. - Manifest intent filter has
android:autoVerify="true"for the https host. - Re-verify with
adb shell pm verify-app-links --re-verify <package>and re-test.