When an App Link opens the browser, it almost always means Android tried to verify your domain and the verification did not pass, so it treated your HTTPS URL as an ordinary web link and sent it to the default browser. Verification is a quiet background step, so it fails quietly too. Nothing throws. You just get Chrome. This walks the causes from most to least common, starting with the one command that tells you which camp you are in.
First, ask Android what it thinks
Before changing anything, read the verification state Android records per domain:
adb shell pm get-app-links com.example.app
That prints each host your app declares and its state. The values you can see:
verified· Android trusted yourassetlinks.json. Links should open the app.none· not verified yet. Usually a missingautoVerify, or verification has not run.approved/denied· force-approved or force-denied, typically by a shell command.migrated/restored· carried over from legacy verification or a data restore.legacy_failure· rejected by the legacy verifier.system_configured· approved automatically by the device configuration.1024or greater · a custom, device-specific verifier error.1026is common in the wild, but its precise meaning is a community interpretation, not an officially documented code.
If this says verified but links still open the browser, the problem is on the device
(remembered defaults, an in-app browser), not your file. Anything else points at the file or the
manifest. The commands cheatsheet keeps these one click away.
Cause 1: the manifest never opted in
Verification only runs if the intent filter asks for it. The filter needs
android:autoVerify="true", the VIEW action, and both the
DEFAULT and BROWSABLE categories. Missing BROWSABLE and it
is not web-openable; missing autoVerify and the domain stays at none:
<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="example.com" />
</intent-filter>
The Intent-Filter Generator produces this block for any URL pattern and picks the correct path attribute.
Cause 2: the assetlinks.json file is not served correctly
This is the big one. Android fetches the file from
https://example.com/.well-known/assetlinks.json and is strict about how it is served.
Per Google's documentation:
- "The
assetlinks.jsonfile is served with content-typeapplication/json." - "The
assetlinks.jsonfile must be accessible over an HTTPS connection, regardless of whether your app's intent filters declare HTTPS as the data scheme." - "The
assetlinks.jsonfile must be accessible without any redirects (no 301 or 302 redirects)."
The redirect rule trips people most. A CDN that upgrades http to https,
adds a trailing slash, or rewrites /.well-known/ returns a 301 that the browser
follows, so the file looks fine to you while Android records a failure. The content type bites
too: a host serving it as text/plain fails even with perfect JSON. Check both:
curl -sSI https://example.com/.well-known/assetlinks.json
You want a single 200, no Location: redirect, and
content-type: application/json. If curl shows a 301 or 302, fix routing
first.
Cause 3: the fingerprint does not match the installed build
A minimal valid file:
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.app",
"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"
]
}
}]
Two fields cause most mismatches. package_name must be the exact
applicationId of the installed build, easy to get wrong with a .debug
suffix or a flavor. And sha256_cert_fingerprints must match the certificate that
signed the installed APK. With Play App Signing, that is the key Google holds, not your upload
key, so use the Play App Signing fingerprint from the Play Console. A domain can list multiple
fingerprints, so add both the upload key and the Play key while testing.
Cause 4: verification has not re-run since you fixed it
Verification happens at install time, so editing the hosted file changes nothing until Android checks again. Force a clean check by resetting the state and re-verifying, or just reinstall:
adb shell pm set-app-links --package com.example.app 0 all adb shell pm verify-app-links --re-verify com.example.app
Wait a few seconds, then run pm get-app-links again. If the domain flips to
verified, you are done. If not, go back to causes 2 and 3.
Cause 5: it is verified, but the device routes around you
If pm get-app-links says verified and the link still opens elsewhere,
something on the device is intercepting the tap:
- An in-app browser. Links tapped inside Instagram, Slack threads, or some email clients open in the app's own web view and never reach the system. Test from a plain SMS, a notes app, or a QR code.
- A remembered "open in browser" default. The app's "Open by default" screen can have supported links toggled off, which sends them to the browser regardless of verification.
To rule the device out, send the OS the intent directly, which the adb deep link testing guide covers.
The short checklist
- Run
pm get-app-linksand read the state. It tells you whether this is a file or a device problem. - Manifest has
autoVerify="true",VIEW,DEFAULT,BROWSABLE. assetlinks.jsonis at/.well-known/, HTTPS,application/json, no redirects.package_namematches the installedapplicationId; fingerprint matches the signing key that reached the device.- Reset and re-verify, or reinstall, after any change.
- Confirm with a real tap from a non-intercepting source, like the QR code tester.
Keep reading
-
App Links vs Deep Links vs Universal Links
The pillar guide to how all three fit together.
-
Test a deep link with adb
Drive the OS directly and skip the device-side noise.
Official docs
-
Android · Verify App Links
autoVerify, the assetlinks.json file, and the domain states.
-
Digital Asset Links
The statement-list format behind assetlinks.json.