On this page
- scope
- prerequisites
- step 1 — choose the domain
- step 2 — get your SHA-256 cert fingerprint
- step 3 — publish the assetlinks.json file
- step 4 — declare the intent filter
- step 5 — implement the handler
- step 6 — install and verify
- step 7 — test on device
- step 8 — common failure modes
- step 9 — fallback for unverified paths
- step 10 — production hardening
- related dev reading
- references
scope
A complete walkthrough for setting up Android App Links from zero. Covers the assetlinks.json file, the <intent-filter android:autoVerify="true"> declaration, the Activity handlers, and the most common debugging steps when App Links fail verification or surface the "Open with" disambiguation dialog.
Cluster C hub: /guides/firebase-dynamic-links-replacement. Pair: /guides/ios-universal-links-setup.
prerequisites
- An Android app project in Android Studio with a valid
applicationIdinbuild.gradle. - The release-signing keystore for your app (or, if testing in debug, the debug keystore at
~/.android/debug.keystore). - A domain you control with the ability to serve a static file over HTTPS and a valid TLS certificate.
- Google Play Console access if you intend to use Play App Signing (which changes the SHA-256 fingerprint Android uses for verification).
step 1 — choose the domain
The domain that will host your App Links must:
- Be HTTPS with a publicly trusted certificate.
- Serve
/.well-known/assetlinks.jsondirectly, withContent-Type: application/json. - Be reachable from Google's verification crawler (no IP allowlists, no aggressive bot protection that blocks
Mozilla/5.0 Android-DigitalAssetLinks).
The domain you choose is what users will see in HTTPS URLs. Apex (yourdomain.com) is most common. Subdomains work — each subdomain requires its own assetlinks.json file at its own /.well-known/ path.
step 2 — get your SHA-256 cert fingerprint
The assetlinks.json file binds your domain to a specific app signature. You need the SHA-256 fingerprint of the cert your app is signed with.
If signing locally
keytool -list -v -keystore /path/to/your-keystore.jks -alias your-alias
Look for the SHA256: line. Format is 14:6D:E9:83:C5:... — 64 hex characters separated by colons.
If using Play App Signing
Google re-signs your app with a Play-managed key when distributing. The fingerprint Android uses for verification is the Play-managed one, not your upload key.
In the Play Console: → Your App → Setup → App Integrity → App Signing → copy the SHA-256 certificate fingerprint.
For debug builds (Android Studio Run button on a development device):
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
For multi-environment setups (debug + release + staging), include all relevant fingerprints in the assetlinks.json. The file accepts multiple entries.
step 3 — publish the assetlinks.json file
Create the file at https://yourdomain.com/.well-known/assetlinks.json. Format details: /guides/assetlinks-json-explained.
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.yourcompany.yourapp",
"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"
]
}
}
]
For multiple build variants:
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.yourcompany.yourapp",
"sha256_cert_fingerprints": [
"<release fingerprint>",
"<debug fingerprint>"
]
}
}
]
Verify the file is reachable and valid:
curl -sI https://yourdomain.com/.well-known/assetlinks.json | head
# Expected:
# HTTP/2 200
# content-type: application/json
curl -s "https://digitalassetlinks.googleapis.com/v1/statements:list?source.web.site=https://yourdomain.com&relation=delegate_permission/common.handle_all_urls"
# Should return your statement, validated by Google
The Google validation endpoint is authoritative — if it doesn't see your statement, neither will Android.
step 4 — declare the intent filter
In AndroidManifest.xml, add an intent-filter to the Activity that should handle deep links:
<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="yourdomain.com"
android:pathPrefix="/product/" />
<data android:scheme="https"
android:host="yourdomain.com"
android:pathPrefix="/profile/" />
</intent-filter>
</activity>
Key elements:
android:autoVerify="true"— this is what makes the intent filter an App Link rather than a plain deep link. Without it, the user sees the disambiguation dialog ("Open with") instead of automatic routing.android:exported="true"— required since API 31 (Android 12).android:scheme="https"— must be HTTPS.android:host="yourdomain.com"— must match the assetlinks.json hosting domain.android:pathPrefix— restricts which paths the app handles. Use multiple<data>elements for multiple prefixes.
step 5 — implement the handler
In MainActivity.kt:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
handleIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleIntent(intent)
}
private fun handleIntent(intent: Intent) {
val data: Uri = intent.data ?: return
if (data.scheme != "https" || data.host != "yourdomain.com") return
val segments = data.pathSegments
when (segments.firstOrNull()) {
"product" -> segments.getOrNull(1)?.let { id -> navigateToProduct(id) }
"profile" -> segments.getOrNull(1)?.let { id -> navigateToProfile(id) }
}
}
onCreate handles cold launches (app was not running, user tapped link). onNewIntent handles warm launches (app is running, user tapped link).
step 6 — install and verify
Install a release-signed (or debug-signed, matching your fingerprint) build on a real device. Then check verification status:
adb shell pm get-app-links com.yourcompany.yourapp
Expected output:
com.yourcompany.yourapp:
ID: ...
Signatures: [...]
Domain verification state:
yourdomain.com: verified
If you see none or 1024 (legacy unverified) instead of verified:
# Force re-verification (Android 11+)
adb shell pm verify-app-links --re-verify com.yourcompany.yourapp
For Android 12+ (API 31+), the system requires the user to grant the verified-link preference in some cases:
adb shell pm set-app-links --package com.yourcompany.yourapp 0 all
This resets the user-preference state and allows verification to proceed cleanly.
step 7 — test on device
- From Chrome on the device, type
https://yourdomain.com/product/123and navigate. - The app should open directly, not the disambiguation dialog.
- If the disambiguation dialog appears, verification failed — see step 8.
- From another app's share sheet, share the URL. The "Your App" option should appear.
For warm-launch testing, open the app, then tap a link from Mail or Messages. The app should receive the URL via onNewIntent.
step 8 — common failure modes
| Symptom | Likely cause |
|---|---|
| Disambiguation dialog ("Open with") appears | autoVerify="true" missing or verification failed |
adb shell pm get-app-links shows none |
assetlinks.json not reachable or fingerprint mismatch |
| Works on debug build but not Play Store install | Play App Signing fingerprint not in assetlinks.json |
| Works for some paths but not others | pathPrefix in intent filter doesn't cover the URL pattern |
| Verification fails sporadically | assetlinks.json hosted behind a 301 redirect or with wrong Content-Type |
| Works on Android 10 but fails Android 12+ | android:exported="true" missing on the Activity |
| Link tapped inside Instagram or TikTok opens web instead of app | Expected — App Links don't trigger from inside in-app WebView. See /guides/in-app-browser-logged-out |
The Play App Signing fingerprint mismatch is the most common production bug. The fingerprint you used during local testing is your upload key, but Google replaces it with the Play-managed key when distributing. Update assetlinks.json with both fingerprints, or use only the Play-managed fingerprint for the production statement.
step 9 — fallback for unverified paths
If a user has the app but verification failed (network error at install time, for instance), the disambiguation dialog appears. To bypass this for known scenarios, you can construct a web-side link that explicitly targets your app package with a web fallback URL — so if the app isn't found, the user lands on the web page instead.
step 10 — production hardening
Before Play Store release:
- Verify all production fingerprints are in assetlinks.json (release, plus Play-managed if using Play App Signing).
- Run
pm get-app-linkson a test device after installing from Play Store, not just from Android Studio. - Add monitoring on the assetlinks.json URL.
- Use the Google Statement List API as a continuous validation check.
For multi-domain setups (apex + www, or production + staging), publish a separate assetlinks.json on each host. Statements don't propagate across subdomains.
related dev reading
- Pair (iOS): /guides/ios-universal-links-setup
- assetlinks.json format: /guides/assetlinks-json-explained
- Intent URL fallback: /guides/intent-url-android
- Universal vs deep vs app link: /guides/universal-link-vs-deep-link-vs-app-link
- Open-in-app pattern: /guides/open-in-app-from-web
- Cluster C hub: /guides/firebase-dynamic-links-replacement
- Bridge: /guides/in-app-browser-logged-out
For non-app linking infrastructure where App Links don't apply, the no-SDK linkboo path is at link.boo/api.
references
- Android App Links Verification documentation
- Google Digital Asset Links protocol specification
- Play App Signing documentation
- Android Manifest intent-filter reference