KappS Knowledge Base · Blog

Android App Links in 2026: A Step-by-Step Verification Tutorial

Blog Tutorial

An Android App Link is the only deep link mechanism Google treats as proof of ownership: your app declares an HTTPS host it is willing to handle, publishes a Digital Asset Links statement at that host, and Android verifies the two match before it will open your app instead of the browser. Two artefacts decide the outcome — a /.well-known/assetlinks.json file you control and the SHA-256 signing fingerprint inside it — and most failures come from one of them being slightly wrong rather than from anything exotic.

This tutorial follows the order the work actually happens in: declare the autoVerify intent filter, publish the assetlinks.json statement list, pick the correct fingerprint when Play App Signing re-signs your build, install and read the verification state, diagnose the failures that are common in production, design a fallback that does not lie to users, and keep app link verification alive through key rotations and domain moves. The rule underneath all of it: verification is a promise that the domain and the app have the same owner, so the only legitimate way to pass it is to actually own both.


1. What a verified link proves, and what it does not

Three ideas get conflated, and separating them prevents most bad design decisions.

  • A deep link is any intent that opens a screen inside your app. It can come from a custom scheme such as myapp://product/42, which any app on the device is allowed to register, so a malicious app can claim the same scheme and receive the URL instead of you.
  • An App Link is an HTTPS URL — https://shop.example.com/product/42 — that Android has verified you are entitled to handle, because that host publishes a statement naming your package and your signing certificate. The user sees a normal web URL, the browser can always open it, and only one app can win the verification.
  • A web fallback is the same URL opened in a browser when your app is not installed. A well-built link strategy makes all three pieces agree, so the URL resolves to the desired content whether or not the app is present.

What verification does not do: it does not prove the app is installed, it does not grant you any rights over the domain's content, and it is not a substitute for authentication. If a screen shows account data, the token check on the server is still what protects it. Verification only answers "may this app open links for this host" — nothing more.

One practical consequence: keep custom schemes for internal handoffs at most, and never rely on them for password resets or payment redirects, because scheme ownership is not enforceable.

2. Step 1 — declare autoVerify intent filters in the manifest

Start in AndroidManifest.xml. The filter must be an https filter with android:autoVerify="true", and it needs the VIEW action plus both the DEFAULT and BROWSABLE categories:

<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="shop.example.com" />
</intent-filter>

Details that decide whether this works in practice:

  • One filter per host. Verification is evaluated per host, and Google's guidance is a separate <intent-filter> for each domain. Grouping five hosts into one filter makes a single bad host poison the rest.
  • Hosts are literal. android:host does not accept wildcards. Reach subdomains with separate filters, and narrow the range with android:path, android:pathPrefix or android:pathPattern so the app does not claim URLs it cannot render.
  • http is not verifiable. Only https participates in automatic verification. Keeping an http filter around just creates duplicate chooser prompts.
  • Claim only what you route. A filter that matches the whole host while the app handles three paths sends every other URL into a blank screen — and that behaviour shows up in store reviews long before it shows up in your crash dashboard.

If your app handles both a marketing domain and an app domain, decide deliberately which one is canonical for sharing. Every host you add is a file to publish and a certificate to keep in sync.

3. Step 2 — publish /.well-known/assetlinks.json

The statement list lives at exactly one path, over HTTPS, with no redirect in front of it:

https://shop.example.com/.well-known/assetlinks.json

The body is a JSON array of statements. For an ordinary app handling all URLs on the host, one statement is enough:

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.shop",
    "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"
    ]
  }
}]

Several packages, or one package with multiple certificates, simply add more statements to the same array. Serving specifics matter as much as the content:

  • Content type. Serve it as application/json. Some Android versions and some CDN edge configurations are strict about this.
  • No redirects. A 301 or 302 in front of the file causes the fetch to fail, and the failure is silent on the device. This is the most common hosting mistake — a site-wide "force HTTPS" or "strip trailing slash" rule quietly breaks it.
  • No authentication, no bot wall. Google's verifier fetches the file anonymously. A WAF challenge or a geo-block returns an HTML error page where JSON is expected.
  • Valid JSON, no comments. A trailing comma or a stray // comment invalidates the whole file, and every statement in it.

Before you debug anything on the device, fetch the file the way the verifier would: curl -sI https://shop.example.com/.well-known/assetlinks.json should show 200 and no Location header, and the body should parse. Google publishes a statement list generator and tester that will validate the structure and the fingerprint format for you — use it before shipping.

4. Step 3 — use the fingerprint the device actually sees

This is where most verification fails, and the reason is Play App Signing. When you enrol, Google holds the app signing key and re-signs every artefact it delivers. The certificate installed on a user's device is therefore the app signing certificate — not the upload key you have on your build machine.

  • Play installs need the app signing key SHA-256 certificate, found in the Play Console under App integrity → App signing (the "App signing key certificate" block).
  • Local and sideloaded builds are signed with your upload or debug key and need that fingerprint — read it with keytool -list -v -keystore <keystore> and copy the SHA-256 line.
  • Ship both. Listing the app signing certificate and the upload certificate in the same sha256_cert_fingerprints array is normal and expected; it lets Play installs verify while your QA builds keep working.

Two more traps in the same area. If you use a debug keystore, its fingerprint is specific to the machine that generated it — fine for your own testing, useless in the published file. And if you ever use Play's key upgrade, the certificate changes: the statement list must be updated before the upgrade is rolled out, or links break for new installs while old ones keep working.

Copy fingerprints from the console rather than retyping them. A single transposed pair of hex digits produces a file that is valid JSON, serves with 200, and still fails verification with no error message anywhere.

5. Step 4 — install and read the real verification state

Verification is attempted when the app is installed, and again after updates, so you need the app installed from the channel you are testing — a Play internal-testing install behaves differently from an adb install of an upload-signed build.

On Android 12 and higher, ask the platform directly:

adb shell pm get-app-links com.example.shop

You get a per-domain report: the domain, whether the user has approved it, and the verification state — verified when the statement list matched, denied or legacy_failure when it did not. On older releases the equivalent is a dump of the domain verification records:

adb shell dumpsys package d

If a host shows as denied and you have just fixed the file, force a fresh attempt instead of reinstalling blindly:

adb shell pm set-app-links --package com.example.shop --domain shop.example.com all

Then test the flow a user performs. A link typed in a browser, a link tapped in a mail client, and a link inside another app do not always resolve the same way, and only one of them may route through the chooser. Check the app's Open by default screen in system settings too: a user can turn a verified link off, and your code should survive that without breaking.

6. Step 5 — diagnose the failures you will actually hit

Almost every production failure falls into one of five buckets. Check them in this order; each takes seconds.

  1. Wrong fingerprint. The file parses, the host is right, and the certificate in the statement is the upload key while the device holds the Play-signed build. Compare the console fingerprint character by character against the file.
  2. Redirect or wrong content type. curl -sI shows a 3xx, text/html, or a CDN-cached error page. Fix the header, purge the cache, then re-verify.
  3. Package name or host mismatch. An application ID changed for a white-label build, or a filter still lists a staging host that no longer serves the file. Grep the manifest for every host and confirm each one has a live statement.
  4. Verification never ran. The install happened offline, or the app has never been launched since installation. Open the app once with connectivity, then re-run pm get-app-links.
  5. User override. The domain was disabled in settings — sometimes months earlier, sometimes by an OEM migration tool. pm set-app-links … all resets it for testing; for real users, the fix is education, not force.

Keep a one-line smoke test in your release checklist: fetch the file, parse it, and assert the fingerprint equals the one the console currently shows. A domain move or a key upgrade that skips that check is exactly how a working link setup dies quietly.

7. Step 6 — build a fallback that tells the truth

App Links make the app the preferred handler; they do not make it the only one. Design the web side first, because it is the case that always exists.

  • The URL must render without the app. A share link that only works when installed is a broken link, and it also breaks the crawlers that index it.
  • Detect, then offer — do not hijack. A small banner with "Open in app" and a store link converts better than an automatic redirect that traps users who deliberately want the browser.
  • Keep the deep-link path identical to the web path. If the app renders /product/42, the web page at the same path should show the same product; the mapping table you avoid building is the one that will break next quarter.
  • Handle the pending-install case. Deferred deep linking — where a new user installs and lands on the shared item — needs a server-side or install-referrer mechanism. Firebase Dynamic Links, the tool most teams used for this, was retired, so plan for the Install Referrer API or a first-party equivalent rather than assuming a hosted service.

And resist the temptation to make verification invisible. If the app cannot render a URL, letting the browser handle it is a better outcome than an app-side "not found" screen, and it keeps the domain's SEO intact.

8. Keeping verification alive across releases and domains

Verification is not a one-time setup. It is a standing agreement between three moving parts: the manifest, the published file, and the certificate the store actually signs with. Anything that touches one of them is a release-checklist item.

  • Key upgrade or re-signing. Update assetlinks.json with the new certificate and publish it before the rollout, then keep the old one listed until the previous version is fully retired.
  • Domain change. Publish statements on the new host first, ship the manifest change second. A filter that points at a host with no file takes links offline immediately.
  • Brand and regional domains. Each host is its own statement and its own file. Automate the deployment so a new regional domain cannot ship without one.
  • Ownership checks. The file is a public claim that you control the domain. Only publish it for domains your organisation owns and operates, and never host another party's statement for them — that is exactly the pattern abuse detection looks for, and it puts both the domain and the app account at risk.

Store the manifest filters, the published JSON template and the current fingerprints in one place, generated from a single source of truth. When they are three separate manual edits, they drift within two release cycles.

9. Where KappS fits

We work on the operational side of shipping: keeping accounts, releases and store records in a state that stands up to platform review — preparing and submitting compliant material, never defeating a check. On link verification that usually means:

  • Release hygiene. Certificates and application IDs consistent between the console, the build and the published statement list, so the fingerprint the device sees is the one you documented.
  • Account readiness. Developer verification, payment profile and tax details aligned with the legal entity behind the account, so a launch is not stalled by an unrelated review.
  • Domain and hosting review. Checking that the well-known path actually serves JSON with no redirects, no WAF challenge and no cached error page in front of it.

What we do not do: publish statements for domains a client does not own, spoof a fingerprint, or help an app claim a host it has no right to. Verification exists to protect users from exactly that, and it is not a service we offer.

Sources (official documentation): Android Developers — Verify Android App Links; Add Android App Links; Digital Asset Links specification; Play Console Help — App signing key certificate and key upgrade; Android Developers — Adb shell pm commands for app links.

Running developer accounts at scale? We handle the operational side.

Click to choose a file · right-click → Paste, or press Ctrl/⌘+V to paste a screenshot

You can also drag a file onto this box.

We reply to the email you provide. Your details stay with KappS.

✔ Submitted

We received your message and will reply within 24 hours.


← Back to edit