This guide walks through all four steps end to end: declaring the intent filter, hosting the Digital Asset Links file, handling the incoming link in your Activity, and verifying it works on Android 12 and later, where the verification rules changed.
App Links vs. Deep Links: What Verification Buys You
A custom-scheme deep link (myapp://) opens your app but isn't a real web URL and shows a chooser if multiple apps claim it. An App Link is a standard https URL that Android has cryptographically verified belongs to you, so it opens your app instantly and silently.
| Type | URL form | Verified? | Chooser dialog? |
|---|---|---|---|
| Custom scheme deep link | myapp://product/42 | No | Possible |
| Unverified http(s) deep link | https://... (no autoVerify) | No | Yes |
| Android App Link | https://... (autoVerify) | Yes | No — opens app directly |
Step 1: Declare the Intent Filter with autoVerify
In your AndroidManifest.xml, add an intent filter to the Activity that should receive links. The android:autoVerify="true" attribute is what upgrades a plain https deep link into a verified App Link.
<!-- AndroidManifest.xml -->
<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" />
<!-- Must be https for a verified App Link -->
<data android:scheme="https"
android:host="links.yourdomain.com" />
</intent-filter>
</activity>Both categories are required. BROWSABLE lets the link be opened from a browser, and DEFAULT lets your Activity handle an implicit intent. Omitting either silently prevents verification. android:exported="true" is mandatory on Android 12+ for any Activity with an intent filter.
Step 2: Host the Digital Asset Links File
Android verifies ownership by fetching a file called assetlinks.json from your domain. It must live at exactly this path, served over HTTPS with no redirects:
https://links.yourdomain.com/.well-known/assetlinks.jsonThe file maps your package name to the SHA256 fingerprint of your app's signing certificate:
[
{
"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"
]
}
}
]Get the SHA256 fingerprint from your signing keystore, or from Gradle's signing report:
# From the keystore directly
keytool -list -v -keystore my-release-key.keystore -alias my-alias
# Or via Gradle (prints debug + release fingerprints)
./gradlew signingReportIf you use Play App Signing, this is the #1 gotcha. Google re-signs your app with its own key, so the fingerprint that matters in production is the one under Play Console → Setup → App signing → App signing key certificate, not your upload key. Add *both* fingerprints to sha256_cert_fingerprints so debug, upload, and Play-signed builds all verify.
The file must be served with Content-Type application/json and return a 200 with no redirect. A common failure is a host that 301-redirects /.well-known paths or serves the file as text/html.
Step 3: Handle the Incoming Link
Verification gets the user into your Activity — now you read the URL and route. Handle both the cold-start case (via the launch intent) and the case where your Activity is already running (via onNewIntent).
// MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
handleIntent(intent) // cold start
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleIntent(intent) // already running
}
private fun handleIntent(intent: Intent?) {
val data: Uri = intent?.data ?: return
// e.g. https://links.yourdomain.com/product/42
when (data.pathSegments.firstOrNull()) {
"product" -> openProduct(data.lastPathSegment)
"promo" -> openPromo(data.getQueryParameter("campaign"))
else -> openHome()
}
}
}Step 4: Test and Verify
First, confirm the link routes into your app at all by firing an intent directly with adb — this tests the intent filter independently of verification:
adb shell am start -a android.intent.action.VIEW \
-d "https://links.yourdomain.com/product/42" \
com.yourcompany.yourappThen check the verification state. On Android 12 and later, the domain verification agent handles this, and you can inspect and re-trigger it from the shell:
# Show verification status for your package (Android 12+)
adb shell pm get-app-links com.yourcompany.yourapp
# Look for your domain with state: "verified"
# Force a re-verification (useful after fixing assetlinks.json)
adb shell pm verify-app-links --re-verify com.yourcompany.yourapp
# Manually approve for local testing without waiting on the network
adb shell pm set-app-links-user-selection --user 0 --package \
com.yourcompany.yourapp true links.yourdomain.comAndroid 12+ changed the rules. Verification became stricter and now runs asynchronously through Google's servers. If get-app-links shows none or failed, the cause is almost always the assetlinks.json file: wrong fingerprint, a redirect, or the wrong content type. Fix the file, then --re-verify.
Common Failures
| Symptom | Most likely cause |
|---|---|
| Verification shows 'failed' in production only | assetlinks.json has your upload key fingerprint, not the Play App Signing key. Add both. |
| Chooser dialog still appears | autoVerify missing, or DEFAULT/BROWSABLE category omitted from the intent filter. |
| Works via adb but not from a real link | The intent filter is fine but verification failed — the assetlinks.json fetch is redirecting or returning the wrong MIME type. |
| Verified on Wi-Fi, not on install | Verification needs network access at install time; it retries, but a flaky first fetch can leave it unverified until re-triggered. |
| Multiple hosts, only one verifies | Every host in your intent filters must be covered by a reachable assetlinks.json — verification is all-or-nothing per host. |
To confirm your file is valid before you rely on Android's agent, validate it from outside the device. Our free deep link debugger fetches your assetlinks.json (and iOS apple-app-site-association), checks the status code, content type, and redirects, and validates the JSON structure and fingerprint format — so you can rule the file out in seconds.
App Links Route Existing Users — New Installs Need More
App Links solve routing for users who already have your app. A user who taps the link without the app installed goes to Play Store — and after they install and open, Android hands your app a launch with no URL. The destination and any campaign context are gone.
Carrying that context through the install is deferred deep linking, and it needs a matching layer on top of App Links. On Android you can build a limited version with the Play Install Referrer API, or use a service that does it across iOS and Android. Deeplinkly hosts your assetlinks.json on a branded domain with SSL and adds deferred deep linking plus install attribution, so a new user who taps your App Link lands on the right screen after installing — with the install tied back to the campaign that drove it.
App Links plus deferred routing, hosted for you.
Deeplinkly hosts your assetlinks.json and apple-app-site-association on a branded domain, and routes new users to the right screen after install — with attribution built in.
Start Free
Read the docs
Frequently Asked Questions
What is the difference between Android App Links and deep links?
A deep link is any URI that opens your app, including custom schemes like myapp://. An Android App Link is specifically a verified https URL — Android confirms you own the domain via an assetlinks.json file, so the link opens your app directly without the 'Open with' chooser dialog. App Links are a safer, verified subset of deep links.
Where does assetlinks.json need to be hosted?
At https://yourdomain.com/.well-known/assetlinks.json, served over HTTPS with a valid certificate, a content type of application/json, no redirects, and a 200 response. It must contain your package name and the SHA256 fingerprint of the certificate that actually signs your production app.
Why do my App Links fail verification only in production?
Almost always because of Play App Signing. Google re-signs your app with its own key, so the production fingerprint differs from your upload key. Add the SHA256 fingerprint from Play Console → App signing → App signing key certificate to assetlinks.json, alongside your upload and debug fingerprints.
How do I check if my App Links are verified?
On Android 12 and later, run adb shell pm get-app-links your.package.name and look for your domain in the 'verified' state. If it shows 'none' or 'failed', fix your assetlinks.json and re-trigger with adb shell pm verify-app-links --re-verify your.package.name.
Do Android App Links work for users who don't have the app installed?
No. App Links route users who already have the app. A user without the app is sent to the Play Store, and after install the app launches with no URL. Routing new users to the intended screen after install is deferred deep linking, which requires the Play Install Referrer API or an install-matching service on top of App Links.
Ship It in the Right Order
App Links are mechanical once you know the four steps: declare the intent filter with autoVerify, host a correct assetlinks.json with the right signing fingerprint, handle the incoming URI in your Activity, and verify with adb. The signing-key mismatch under Play App Signing is the one that catches almost everyone — check that first if production verification fails.
Get standard App Links verified first, then decide whether you need deferred routing for new installs. If you'd rather not host and maintain association files or build install matching yourself, Deeplinkly's documentation covers the hosted setup across Android and iOS in one place.
Back to all articles
© Deeplinkly