Skip to content
App Signing & PushPart 40 of 44
App SigningMobile DevelopmentFlutter

assetlinks.json: Setting Up Android App Links, and Why They Fail

assetlinks.json tells Android which apps may open your web links. The file format, hosting rules, autoVerify, testing tools, and the Play app signing fingerprint trap.

By Bimal Khatri·13 min read·Sep 17, 2026·Updated Sep 17, 2026
assetlinks.json: Setting Up Android App Links, and Why They Fail

assetlinks.json is a small public file on your website that tells Android which apps may open your web links. It lives at https://your-domain/.well-known/assetlinks.json and lists, for each app, its package name and the SHA-256 fingerprint of the certificate that signs it.

When an app's manifest asks for verified links (android:autoVerify="true"), Android fetches that file and compares it with the installed app. If they match, a tap on a link to your site opens the app straight away, with no "open with" chooser. That is an Android App Link.

If your links open the browser only for people who installed the app from Google Play, the usual reason is that the file lists your upload key's fingerprint instead of the key Google signs with. The fix is in the section on fingerprints below; the rest of this guide covers the format, the hosting rules and how to test.

Android has had deep links for a long time: an app can say "I can open links to example.com". But any app can say that, so Android used to ask the user which app to use, or fall back to the browser.

App Links add proof. Your website publishes assetlinks.json, saying which apps it trusts. Your app asks Android to check. Once Android has confirmed that both sides agree, links to your site go straight to your app.

A few facts from Android's documentation frame the rest:

  • App Links work on Android 6.0 and later, on devices with Google services.
  • From Android 12, a plain web link opens in an app only if that app is approved for the link's domain. Otherwise it opens in the default browser. Verification through assetlinks.json is how an app gets approved automatically.
  • On a given device, only one app at a time can be associated with a domain.

A sequence chart with three participants: the user, Android on the phone, and your website. The user installs the app, whose intent filters ask for verification. Android fetches /.well-known/assetlinks.json over HTTPS. The website answers 200 with package names and SHA-256 fingerprints. Android compares them with the installed app's package and signing certificate. A note says a match verifies the domain, while no match, a redirect or an error does not. Later the user taps a link to the site, and Android opens the app directly with no chooser.

The file, field by field

This is the smallest useful file. It lets one app, com.example.app, open every link on the site that serves it:

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.app",
      "sha256_cert_fingerprints": [
        "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99"
      ]
    }
  }
]
FieldWhat it meansWhat to put there
The outer [ ]A list of statements. One file can hold severalAlways an array, even for one app
relationWhat the website allowsdelegate_permission/common.handle_all_urls for App Links
target.namespaceWhat kind of thing the statement is aboutandroid_app
target.package_nameWhich appThe application ID from your Gradle build, such as com.example.app
target.sha256_cert_fingerprintsWhich signing certificates count as that appUpper-case SHA-256 fingerprints with colons. Several are allowed
relation_extensionsOptional extra rulesDynamic App Links on Android 15 and later (see below)

The Digital Asset Links documentation, which defines this format, calls the whole file a statement list. The file is plain JSON, so a missing comma or quote breaks it for everyone; run it through a JSON validator before publishing.

Several apps or several fingerprints

sha256_cert_fingerprints takes a list, so one entry can accept the app however it was signed: for example Google's app signing key for Play users and a debug key for testing.

To let two different apps open links on the same site, add a second statement to the array:

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.app",
      "sha256_cert_fingerprints": [
        "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99",
        "11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00"
      ]
    }
  },
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.app.lite",
      "sha256_cert_fingerprints": [
        "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99"
      ]
    }
  }
]

Android's guide adds a caution for that case: if two installed apps can handle exactly the same host and path, such as a lite and a full version, only the most recently installed one gets the links.

The reverse also works. Several websites can each publish a file naming the same app. Each site must serve its own copy.

Which fingerprint goes in the file

The fingerprint must belong to the certificate that signed the copy installed on the phone. Picking the wrong key is an easy mistake to make.

A map of the Play app signing trap. An assetlinks.json that lists only the upload key's SHA-256 matches your own release build, which is signed with the upload key, so that link opens the app. It does not match the copy from Google Play, which is signed with Google's app signing key, so that link opens the browser.

With Play App Signing, which every app bundle on Google Play uses, you sign uploads with your upload key and Google signs what users install with its app signing key. Android's guide says it plainly: the fingerprint you get by running keytool locally will usually not match the one on users' devices.

The fix is in Play Console:

  • Open the Play app signing page. At the time of writing it sits under Protected with Play; older Google pages still say Release → Setup → App signing or App Integrity. Search for "app signing" if it has moved.
  • Copy the SHA-256 of the app signing key. Android's guide notes that the same page also shows a ready-made Digital Asset Links JSON snippet for your app.
  • If your app uses Play's quantum-ready hybrid signing, Play Console Help says to copy the fingerprints for all three keys it lists and update assetlinks.json with them. Play's hybrid signing explains the three keys.

Add other fingerprints only for copies that need working links: your debug key while developing, or the internal app sharing key for testers who install from an internal sharing link.

To read a SHA-256 yourself, the Digital Asset Links documentation gives two commands for a certificate file:

keytool -printcert -file upload_certificate.pem | grep SHA256
openssl x509 -in upload_certificate.pem -noout -fingerprint -sha256

Both print upper case with colons, which is the form the file needs. How to get SHA-1 and SHA-256 fingerprints covers every key, including reading one from an APK Google signed.

Hosting rules

When the fingerprints are right and verification still fails, look at how the file is served.

A grid of hosting rules. Passes: the file at https://host/.well-known/assetlinks.json on every host, HTTPS with a certificate that verifies, HTTP 200, content type application/json, public access, and the upper-case SHA-256 of the key that signed the installed copy. Fails: any other path or a missing host, a certificate chain the device cannot verify, 301 or 302 redirects, 404 or any other status, any other content type, a login or VPN in the way, and lower-case or SHA-1 fingerprints, or a key that did not sign that copy.

The rules, from Android's guide and the Digital Asset Links documentation:

  • Exact location. https://your-domain/.well-known/assetlinks.json, with the dot in .well-known. A file anywhere else does not count.
  • HTTPS, always. Even if your intent filters also accept http links. A certificate chain that does not verify counts as no file.
  • HTTP 200, no redirects. Redirects (301 or 302) are not followed, and any other status is treated as an empty file. Android's troubleshooting page gives two examples that stop verification: a redirect from http://example.com to https://example.com, and one from example.com to www.example.com.
  • Content type application/json. Check your server or CDN does not send it as text.
  • Every host. If your intent filters list www.example.com and mobile.example.com, each must serve the file.
  • Wildcards use the parent domain. An intent filter host of *.example.com is verified against https://example.com/.well-known/assetlinks.json.
  • Public. No login, no VPN. Android's guide warns against shipping test URLs that only work inside a private network.
  • The trailing-dot name. The troubleshooting page asks you to check that example.com. (with a final dot) serves the same content as example.com.

You can check the status and content type from any terminal:

curl -sI https://www.example.com/.well-known/assetlinks.json

Look for 200 on the first line and content-type: application/json in the headers, and make sure there is no location: header, which would mean a redirect.

The manifest side

The website half is useless unless the app asks for verification. Android's guide gives this pattern: an intent filter with android:autoVerify="true", the VIEW action, the DEFAULT and BROWSABLE categories, and data elements for both http and https:

<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="http" />
        <data android:scheme="https" />
        <data android:host="www.example.com" />
    </intent-filter>
</activity>

Two version differences matter:

  • Android 12 and later verify each host on its own. If one host fails, the others can still be verified.
  • Android 11 and earlier verify the app only if every host in the manifest's App Links filters passes. One forgotten test domain breaks them all.

Flutter apps use the same manifest entry in android/app/src/main/AndroidManifest.xml; nothing about the website side changes.

Testing

Work from the outside in: first the file, then the app's configuration, then a real device.

The file. Google's Statement List Generator and Tester can generate a statement file and test an existing one. You can also ask the Digital Asset Links API directly, which Android's testing guide recommends:

curl "https://digitalassetlinks.googleapis.com/v1/statements:list?source.web.site=https://www.example.com&relation=delegate_permission/common.handle_all_urls"

A working site returns a statements list with your package name and fingerprints. A broken one returns an empty result with a debugString explaining the problem. For a site with no file, the answer looks like this (shortened):

"debugString": "********************* ERRORS *********************\n* Error: unavailable: Error fetching statements from https://www.example.com./.well-known/assetlinks.json ... 404 Not Found ...",
"errorCode": ["ERROR_CODE_FETCH_ERROR"]

The configuration. Android Studio's App Links Assistant (in the Tools menu) lists your deep links, flags misconfiguration and can generate assetlinks.json, though Android's page says it does not yet support dynamic rules. After you upload the app, Play Console's Deep links page shows the setup and its errors. Flutter projects can use Flutter's deep link validator.

A device. From Android 12, you can re-run verification by hand. With the app installed, run these in order, replacing com.example.app:

adb shell pm set-app-links --package com.example.app 0 all
adb shell pm verify-app-links --re-verify com.example.app
adb shell pm get-app-links com.example.app

The first command resets the app's link state, the second starts verification, and the third shows the result. Android's guide says to wait a few minutes before the third. If your app targets Android 11 or lower, first run adb shell am compat enable 175408749 com.example.app to switch on the newer verification process.

The result lists a state per domain:

StateMeaning
verifiedThe domain passed verification for this app
noneNothing recorded yet. Wait a few minutes and verify again
approvedForce-approved, usually by a shell command
deniedForce-denied, usually by a shell command
migratedA result kept from the older verification process
restoredApproved after the user restored data
legacy_failureRejected by the older verifier, reason unknown
system_configuredApproved by the device's configuration
A number of 1024 or moreAn error code specific to the device's verifier. Check the network and try again

On Android 17 and later, one more command explains how a single link is resolved, including which intent filters and dynamic rules matched:

adb shell am start --debug-link -a android.intent.action.VIEW -d "https://www.example.com/offers"

When changes take effect

Updating the file on your server does not update phones immediately. Android's guide says:

  • Android 15 and later re-verify in the background, and changes can take up to seven days to reach every device.
  • Android 14 and earlier do not re-verify on their own. They pick up changes when the app is installed or updated.

While testing, uninstalling and reinstalling the app forces a fresh check, but server-side caches can still serve the old file for a few hours.

Android 15 added optional rules inside assetlinks.json itself, under relation_extensions and dynamic_app_link_components. They let you include or exclude paths, query parameters and fragments from the server, without shipping an app update. Three limits from the guide are worth knowing: the rules can only narrow what the manifest already allows, they are evaluated in order, and a malformed rules block is thrown away so the device falls back to the manifest. Android 14 and earlier ignore the rules entirely.

What about iOS?

iPhones do not read assetlinks.json. Universal links use a separate file named apple-app-site-association, with no extension, at https://your-domain/.well-known/apple-app-site-association. It identifies apps by Team ID and bundle ID rather than by certificate fingerprint, and it is also served over HTTPS with no redirects. The app side is an entitlement; iOS entitlements explained covers that half.

SymptomUsual cause
Links open the browser only for Play Store installsThe file lists the upload key, not Play's app signing key
Links work in release but not in debug buildsThe debug key's SHA-256 is not listed
Verification fails although the JSON looks rightA redirect, a non-200 status or the wrong content type
It works for www.example.com but not example.comOnly one host serves the file
Every host fails on Android 11 and olderOne host in the manifest cannot be verified
The fingerprint "matches" but still failsIt is lower case, or it is a SHA-1
A fixed file makes no differenceThe device has not re-verified yet; reinstall while testing
Links open a different app of yoursAnother installed app handles the same host and path

Questions people ask

Where do I put assetlinks.json?

In a folder called .well-known at the root of your website, so it loads from https://your-domain/.well-known/assetlinks.json. Every host in your intent filters needs its own copy.

What content type should assetlinks.json use?

application/json. The file must also load over HTTPS with a 200 response and no redirects.

Does assetlinks.json need SHA-256 or SHA-1?

SHA-256, in upper case with colons, as the field name sha256_cert_fingerprints says.

Can one assetlinks.json list several apps?

Yes. Add one statement per app to the array, and list several fingerprints inside a statement if the same app is signed by more than one key.

Usually because the file does not match the installed app's signing key, often Play's app signing key, or because the file is redirected or served with the wrong content type. Check the file with the Digital Asset Links API, then run adb shell pm get-app-links on a device.

How long do assetlinks.json changes take?

On Android 15 and later, up to seven days, according to Android's guide. On Android 14 and earlier, the change is picked up when the app is installed or updated.

Does iOS use assetlinks.json?

No. iOS reads apple-app-site-association, from the same .well-known folder, and identifies apps by Team ID and bundle ID.

Do Flutter apps need anything different?

No. The file and hosting rules are the same. The intent filter goes in the Android manifest inside the Flutter project, and Flutter offers its own deep link validator.

Where this comes from

Keep reading

More writing

Keep reading