Glossary/Failure modes
Validating assetlinks.json
Definition
Validating assetlinks.json means confirming four separate things: that Android can fetch the file, that it parses as a valid statement list, that it names the fingerprint of the shipped build, and that verification passed on a device.
Each of those four can fail while the other three pass, which is why a single validator returning green is weak evidence. Google's own Statement List API, the most commonly cited check, confirms the first two and is structurally blind to the second two — it has never seen your app and does not know which key signed it. Run all four; together they take about two minutes.
The four checks
| Check | Catches | Blind to |
|---|---|---|
| Google's Statement List API | Unreachable file, bad JSON, wrong relation, wrong content type | Whether the fingerprint matches your build |
Raw curl with no redirects | Redirects, auth walls, SPA catch-alls, wrong content type | Whether the statement contents are correct |
adb shell pm get-app-links | The real verdict on a real device | Why it failed |
| Fingerprint comparison against the installed APK | The single most common cause: the wrong signing key | Hosting problems |
A green Statement List API result does not mean App Links work
The API fetches your file and validates its shape. It cannot know which key signed the build on a user's device, so it returns success for a file listing a fingerprint that matches nothing you ship. This is why teams report "Google's validator says it is fine" while every link opens the browser.
1 and 2: the file itself
curl -sS "https://digitalassetlinks.googleapis.com/v1/statements:list\
?source.web.site=https://example.com\
&relation=delegate_permission/common.handle_all_urls" | python3 -m json.tool
# Success looks like this — note "maxAge" and an empty debugString:
# {
# "statements": [
# {
# "source": { "web": { "site": "https://example.com." } },
# "relation": "delegate_permission/common.handle_all_urls",
# "target": { "androidApp": {
# "packageName": "com.example.shop",
# "certificate": { "sha256Fingerprint": "14:6D:E9:83:..." }
# }}
# }
# ],
# "maxAge": "21600s",
# "debugString": "..."
# }An empty statements array means the file was not usable — read debugString, which names the reason. The most common values describe a non-JSON content type, a redirect, or a fetch failure, all of which are hosting problems rather than content problems.
# --max-redirs 0 is the whole point: Android does not follow redirects,
# so a 301 here is a failure even though a browser would sail through it.
curl -sSI --max-redirs 0 https://example.com/.well-known/assetlinks.json
# Want: HTTP/2 200 and content-type: application/json
# Anything 3xx, a 403, or content-type: text/html is your answer.
# The body, checked for valid JSON and the correct top-level type
curl -sS --max-redirs 0 https://example.com/.well-known/assetlinks.json \
| python3 -c "
import json,sys
d = json.load(sys.stdin)
assert isinstance(d, list), 'must be a JSON array, not an object'
for s in d:
print(s['target']['package_name'], s['relation'])
for fp in s['target']['sha256_cert_fingerprints']:
print(' ', fp)
"| Mistake | Looks like | Why it fails |
|---|---|---|
| Object instead of array | { "relation": … } | The format is a statement list; it must start with [ |
| Relation as a string | "relation": "delegate_…" | Must be an array of strings |
| Lowercase fingerprint | 14:6d:e9:… | Uppercase hex is expected |
| Fingerprint without colons | 146DE983C5… | Colon-separated byte pairs are required |
| SHA-1 instead of SHA-256 | 20 byte pairs, not 32 | Only SHA-256 is accepted |
Java package as package_name | com.example.app.ui | It is the Gradle applicationId |
| Trailing comma | …], } | Not valid JSON; nothing parses |
3 and 4: the device and the key
The first two checks are about your domain. These two are about your app, and they are the ones that catch the failure that actually happens most.
# 3. What the device concluded. "verified" or it did not work.
adb shell pm get-app-links com.example.shop
adb shell pm verify-app-links --re-verify com.example.shop
# 4. The fingerprint of the build ACTUALLY INSTALLED on the device
adb shell pm path com.example.shop
adb pull /data/app/~~abc123==/com.example.shop-xyz==/base.apk
keytool -printcert -jarfile base.apk | grep -A1 "SHA256:"
# Compare that value against what the file declares. If your app comes
# from Play, expect it to match the Play app signing key, NOT your
# upload key — Google re-signs with a key you do not hold.Check the installer before comparing anything
adb shell pm list packages -i com.example.shop names the installer. com.android.vending means the build came from Play and was therefore signed by Google, so the Play app signing key is the fingerprint that matters. A build installed by adb was signed by your local key instead — comparing against the wrong one wastes an afternoon.
The reason this check ranks above the others is that a fingerprint mismatch produces exactly the same symptom as every hosting problem — links open the browser — while being invisible to every hosting check. Google's API is happy, curl is happy, and the file is genuinely correct except for one field naming a key you do not ship with. See assetlinks.json for which key each distribution channel uses.
Keeping it valid
Most assetlinks.json regressions are not edits to the file. They are infrastructure changes made by people with no reason to know the file exists, which means the durable protection is a check that runs without anyone remembering to run it.
#!/usr/bin/env bash
set -euo pipefail
URL="https://example.com/.well-known/assetlinks.json"
EXPECTED_FP="14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:..."
# One 200, no redirects, correct content type
read -r code type < <(
curl -sS --max-redirs 0 -o /tmp/al.json \
-w '%{http_code} %{content_type}\n' "$URL"
)
[[ "$code" == "200" ]] || { echo "FAIL: HTTP $code (redirect or block)"; exit 1; }
[[ "$type" == application/json* ]] || { echo "FAIL: type $type"; exit 1; }
# Valid statement list naming the fingerprint we actually ship
python3 - "$EXPECTED_FP" <<'PY'
import json, sys
want = sys.argv[1]
data = json.load(open('/tmp/al.json'))
assert isinstance(data, list), "FAIL: not a JSON array"
found = any(
want in s["target"].get("sha256_cert_fingerprints", [])
for s in data
)
assert found, "FAIL: shipped fingerprint is not in the file"
print("assetlinks.json OK")
PYRun it against production on every web deploy, not only on app releases. The redirect that breaks the file will be introduced by a web change, and this is the point at which it becomes visible instead of six weeks later in a support queue.
assetlinks.json validator
Enter a domain and it runs the hosting checks in one pass — redirect chain, status, content type, JSON shape, relation string, and the fingerprints declared — then shows the corrected file if anything is wrong. It validates the iOS association file at the same time, since a domain misconfigured for one is usually misconfigured for both.
Open the assetlinks.json validator →Frequently asked questions
- How do I validate my assetlinks.json file?
- Run four independent checks. Google's Statement List API at digitalassetlinks.googleapis.com confirms the file is reachable and well-formed. A curl with --max-redirs 0 confirms there are no redirects and the content type is application/json. adb shell pm get-app-links gives the real verdict on a device. And comparing the fingerprint in the file against the certificate of the installed APK catches the most common failure of all.
- Why does Google's validator say my assetlinks.json is valid when App Links still fail?
- Because the Statement List API validates the file, not the match. It fetches your domain, parses the statements, and checks the relation — but it has never seen your app and cannot know which key signed the build users install. A file that lists your upload key instead of the Play app signing key passes the API and fails on every device.
- What is the Digital Asset Links API URL?
- https://digitalassetlinks.googleapis.com/v1/statements:list with source.web.site set to your https origin and relation set to delegate_permission/common.handle_all_urls. An empty statements array in the response means the file could not be used, and the debugString field names the reason, which is usually a redirect, a wrong content type, or a failed fetch.
- What are the most common assetlinks.json mistakes?
- Listing the upload key fingerprint instead of the Play app signing key is the most common by far. After that: a top-level JSON object instead of an array, the relation as a string rather than an array of strings, a SHA-1 fingerprint where SHA-256 is required, fingerprints without colon separators, and using the Java package name instead of the Gradle applicationId for package_name.
- How do I check which fingerprint my installed app actually uses?
- Get the APK path with adb shell pm path, pull it with adb pull, then run keytool -printcert -jarfile against it. First check the installer with adb shell pm list packages -i — if it reports com.android.vending the build came from Play and was signed by Google, so the Play app signing key is the one that has to be in the file.
- Should I validate assetlinks.json in CI?
- Yes, and run it on web deploys rather than only app releases. Most regressions come from infrastructure changes — a new redirect rule, a WAF policy, a CDN configuration — made by people who have no reason to know the file is load-bearing for the mobile app. A check asserting one 200 with no redirects, a JSON content type, and the presence of your shipped fingerprint catches all of them at the deploy that causes them.
Related terms
- assetlinks.json — assetlinks.json is a Digital Asset Links statement file hosted at a domain's /.well-known/ path that authorises a named Android app, identified by package name and signing certificate fingerprint, to handle that domain's URLs.
- Android App Links not working — 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.
- Debugging deep links with adb — adb provides direct access to Android's domain verification state, intent resolution, and package manifest data, which together identify why a deep link opens the browser instead of the app.