Skip to content
All posts

Android App Links in 2026: why your https:// links still open in the browser

Digital Asset Links verification, the assetlinks.json gotchas, and the adb commands that tell you why Android won't hand a link to your app.

MFKAPPS 5 min read

A custom URI scheme like myapp://item/42 is easy to wire up and easy to get wrong in a different way: any app can register the same scheme, so the OS can’t promise your app is the one that opens it. An Android App Link — a real https://yourdomain.com/... URL that opens your app directly instead of a browser tab — fixes that, but only if the OS can verify your app actually owns the domain. Most of the time a link like this quietly falls back to Chrome, and the manifest looks completely correct. The missing piece is almost always Digital Asset Links verification, not the intent filter.

The two things that have to agree

An App Link only auto-verifies when two independent pieces line up: the app’s intent filter, and a JSON file the domain itself serves. Neither side knowing about the other is the point — it’s what stops a random app from claiming your domain.

The intent filter goes in the manifest:

<activity android:name=".MainActivity" android:exported="true">
    <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="mfkapps.com"
              android:pathPrefix="/apps" />
    </intent-filter>
</activity>

android:autoVerify="true" is what triggers Android to actually check ownership at install time, rather than just matching the URL pattern. Skip it and the filter still matches links, but only after the user has explicitly chosen your app once from the disambiguation sheet — it never gets promoted to “always open automatically.”

The domain side is a static file at a fixed, non-negotiable path:

https://mfkapps.com/.well-known/assetlinks.json
[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.mfk.granyn",
    "sha256_cert_fingerprints": [
      "14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A3:88:1E:22:0B:99:19:33:4B:C0:5B:0B:64:0C:1D"
    ]
  }
}]

package_name and sha256_cert_fingerprints both have to match the exact build that’s installed. That fingerprint detail is where almost every broken setup actually breaks.

The fingerprint mismatch that breaks everything silently

sha256_cert_fingerprints needs the SHA-256 of the signing certificate for the APK the user has installed — and if the app ships through Play App Signing, that’s Google’s signing key, not the upload key you sign locally. Get the wrong one in assetlinks.json and verification fails with no error the user ever sees; the link just opens in a browser, forever, and nothing in Logcat points at the file.

Get the right fingerprint from the Play Console, not from your local keystore:

Play Console → Setup → App integrity → App signing key certificate → SHA-256

For a debug build during development, use the debug keystore’s fingerprint instead, and list both in the array if you want debug and release to verify against the same assetlinks.json:

keytool -list -v -keystore ~/.android/debug.keystore \
  -alias androiddebugkey -storepass android -keypass android

sha256_cert_fingerprints accepts an array, so both fingerprints can sit side by side — verification checks whether any entry matches the installed app’s signature.

Verifying it actually worked

Android runs the verification check at install time and caches the result, so the fastest way to see the real status is asking the OS directly rather than guessing from behavior:

adb shell pm get-app-links com.mfk.granyn

A working setup reports the domain as verified:

com.mfk.granyn:
  ID: d3a1f0b2-...
  Signatures: [14:6D:E9:...]
  Domain verification state:
    mfkapps.com: verified

If it says legacy_failure or none, the fingerprint or JSON is wrong — fix assetlinks.json, then force a re-check without reinstalling:

adb shell pm verify-app-links --re-verify com.mfk.granyn
adb shell pm get-app-links com.mfk.granyn

To test the actual tap-through behavior once verification passes, fire an intent the way a browser or another app would:

adb shell am start -a android.intent.action.VIEW \
  -d "https://mfkapps.com/apps/granyn"

If the app opens with no chooser sheet, verification is working. If a disambiguation dialog appears, verification failed and the OS is treating it as an ordinary, unverified link match.

Three things that quietly break it after launch

The JSON has to be served with the right content type and no redirect. A CDN or reverse proxy that 301-redirects /.well-known/assetlinks.json to a canonical URL, or serves it as text/html, fails verification even though a browser renders it fine. Curl it directly and check both:

curl -sI https://mfkapps.com/.well-known/assetlinks.json | grep -i content-type

Re-signing the app changes the fingerprint. Rotating a signing key, switching from a local keystore to Play App Signing, or onboarding a new build pipeline all silently break every previously-verified link until assetlinks.json is updated with the new fingerprint. This is worth a line in a release checklist, because the failure mode — links quietly downgrading to browser fallback — produces no crash and no error report.

Multiple apps sharing one domain need multiple statements. If more than one app on the same team claims paths under the same host, assetlinks.json is a JSON array — one object per app, each with its own package_name and fingerprint list. A file with only one app’s statement silently fails verification for every other app on that domain.

What this earns you beyond a nicer tap

Once a domain is verified, the same intent filter also lets the app act as the default handler for that domain everywhere the OS hands off a URL — a notification action, a QR code, a link shared from another app, a Smart Lock credential flow. None of that needs separate wiring; it’s the same autoVerify filter and the same assetlinks.json, doing more once the OS actually trusts the claim behind it.

If a link into the app still opens a browser after all of this, check pm get-app-links before touching the manifest again — in practice the intent filter is almost never the broken half.