Deeplinkly

Glossary/Implementation artifacts

Android App Link Verification

Definition

Android App Link verification is the process by which Android confirms, at install time, that a domain named in an app's intent filter publishes an assetlinks.json file authorising that app to handle its URLs.

Verification is what separates an App Link from an ordinary web intent. It runs once, in the background, without user involvement, and when it fails it does so quietly — the app installs normally and its HTTPS links simply open Chrome. Since Android 12 there is a proper command for inspecting the result, which turns the most opaque part of Android deep linking into something you can read off a terminal.

What happens at install

The chain is short, and every step is a place it stops. The system reads your manifest, collects every host in an intent filter marked android:autoVerify="true", and asks the domain verification agent to check them.

  1. The package installs and its intent filters are parsed.
  2. Each host in an autoVerify filter is queued for verification.
  3. The agent fetches https://<host>/.well-known/assetlinks.json — over HTTPS, following no more than the redirects the fetcher allows, with no credentials.
  4. Each statement is checked for the delegate_permission/common.handle_all_urls relation, your package_name, and a sha256_cert_fingerprints entry matching the certificate that actually signed the installed APK.
  5. On success the host is marked verified and its links open the app directly. On failure the host stays unverified and its links go to the browser.

One failed host does not fail the rest — but one bad filter does

Hosts are verified independently, so example.com can be verified while www.example.com is not. But autoVerify applies to a whole intent filter: put a custom scheme in the same filter as your HTTPS hosts and verification of that filter fails outright, taking every host in it down with it.

Reading the verification state

On Android 12 and later, pm get-app-links prints the truth. This is the first command to run on any App Links problem — it answers "is this a manifest problem or a domain problem" in one shot.

Inspect and reset verification
# Android 12+: current state for every declared host
adb shell pm get-app-links com.example.shop

# Force a fresh verification attempt
adb shell pm verify-app-links --re-verify com.example.shop

# Clear the stored result first, then re-verify
adb shell pm set-app-links --package com.example.shop 0 all
adb shell pm verify-app-links --re-verify com.example.shop

# Android 11 and earlier
adb shell dumpsys package domain-preferred-apps

# Approve a host manually, as a user would in Settings.
# Confirms routing works while you fix the real cause.
adb shell pm set-app-links-user-selection --user cur \
  --package com.example.shop true example.com
States reported by pm get-app-links, and what each one means.
StateMeaningWhat to do
verifiedOwnership confirmed; links open the app directlyNothing
noneNever attempted, or the attempt has not completedRe-verify; check the device had network at install
legacy_failureThe fetch or the statement check failedThe domain side is wrong — fetch the file yourself
1024 / 1025Internal error, or a retry is scheduled with backoffRe-verify after fixing the domain; wait out the backoff
system_configuredPre-approved by the device OEM or manufacturer imageNothing; not something an app controls
Listed under User selectionNot verified, but the user approved the domain by handRouting works for that user only — fix verification properly

The distinction that matters most is none versus legacy_failure. none usually means the check never ran — no network at install is the common cause. legacy_failure means it ran and your domain gave the wrong answer, which moves the investigation to assetlinks.json.

Why it fails, ranked

Causes of failed verification, most common first.
CauseSymptomFix
Fingerprint is the upload key, not the app signing keyWorks on a locally built APK, fails from PlayTake the SHA-256 from Play Console → App integrity
assetlinks.json behind a redirectlegacy_failure; the file looks fine in a browserServe a 200 directly on the https host
Wrong Content-Typelegacy_failureapplication/json; not text/plain or text/html
www host declared but no file on itOne host verified, one notHost the file on every declared host
Custom scheme in the autoVerify filterAll hosts in that filter failSplit into separate intent filters
No network at installnone, indefinitelypm verify-app-links --re-verify, or reinstall
applicationIdSuffix on debug buildsDebug build never verifiesAdd a statement for com.example.shop.debug

Check the domain the way the verifier does, not the way a browser does. Google's Statement List API applies the same rules and returns an explicit error:

Fetch as the verifier does
# Raw fetch: watch for redirects and the content type
curl -sSIL https://example.com/.well-known/assetlinks.json

# Google's own checker, same rules as the on-device agent
curl -sS "https://digitalassetlinks.googleapis.com/v1/statements:list\
?source.web.site=https://example.com\
&relation=delegate_permission/common.handle_all_urls"

# The fingerprint of the APK that is actually installed
adb shell pm list packages -f com.example.shop
apksigner verify --print-certs app-release.apk | grep SHA-256

What changed in Android 12

Android 12 made verification stricter and much more visible, and it is the reason a link that worked for years started opening the browser.

Unverified HTTPS link behaviour, before and after Android 12.
Android 11 and earlierAndroid 12 and later
Unverified HTTPS linkApp chooser dialog listing your appOpens the browser with no dialog
Verification result visibledumpsys only, hard to readpm get-app-links, explicit per host
User can approve manuallyVia the chooser's "Always"Settings → Apps → Open by default → Add link
Retry on failureEffectively noneAutomatic retries with backoff

The chooser dialog was the safety net

Before Android 12, an unverified App Link still reached the app because the user could pick it from a chooser — which meant broken verification looked like a working setup with an extra tap. Android 12 removed the net. Nothing about your app changed; the tolerance did.

Deep link debugger

Enter your domain and package name and it fetches /.well-known/assetlinks.json the way the on-device verifier does — checking the redirect chain, content type, relation string, package name and fingerprints — and names the step that fails. It answers the domain half of the question pm get-app-links asks.

Open the deep link debugger

Frequently asked questions

How do I check whether my Android App Links are verified?
Run adb shell pm get-app-links com.example.shop on a device running Android 12 or later. It prints each declared host with its verification state — verified, none, legacy_failure, or a numeric retry code — plus any domains the user approved by hand. On Android 11 and earlier the equivalent is adb shell dumpsys package domain-preferred-apps, which is far less readable.
Why did my App Links stop working after Android 12?
Android 12 stopped showing the app chooser for unverified HTTPS links and sends them straight to the browser instead. If your verification was already failing, the chooser was hiding it: users could still reach the app by picking it from the dialog. Run pm get-app-links to see the real state, which is usually legacy_failure caused by a fingerprint or hosting problem.
How long does Android App Link verification take?
It normally completes within seconds of install, in the background. If the device has no network connection at that moment the state stays none, and Android retries with an increasing backoff rather than immediately. You can force an attempt at any time with adb shell pm verify-app-links --re-verify followed by the package name.
Can a user approve a domain manually if verification fails?
Yes. On Android 12 and later they can open Settings, then Apps, then your app, then Open by default, then Add link, and select the domain. It is a per-user, per-device workaround rather than a fix — it does nothing for anyone else — but it is a useful way to confirm that your intent filters and routing code are correct while you fix the domain side.
Does verification need to succeed for every declared host?
Each host is verified independently, so example.com can pass while www.example.com fails, and links to the failing host go to the browser. The exception is the intent filter itself: autoVerify is a property of the whole filter, so if the filter also contains a custom scheme, verification fails for every host declared inside it.

Related terms

  • assetlinks.jsonassetlinks.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.
  • Intent FilterAn intent filter is an element in an Android app's manifest that declares which intents an activity can handle, including the URL patterns that should open it.
  • SHA-256 Certificate FingerprintA SHA-256 certificate fingerprint is the SHA-256 hash of an app signing certificate, written as 32 colon-separated hexadecimal bytes, used to prove that a specific build was signed by a specific key.
  • Validating assetlinks.jsonValidating 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.
  • Android App Links not workingAndroid 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.