Deep links that "sometimes open the app" are broken. That qualifier — sometimes — means the setup is partially working or intermittently failing, and you are losing users to the browser on every failure without a log entry, a crash report, or any visible signal that anything went wrong. The user sees a web page instead of the app screen they expected. Most of them do not retry. They are gone.
Android App Links — the HTTP URL-based deep linking mechanism introduced in Android 6.0 and significantly evolved through Android 12 and Android 15 — are the correct modern solution for this class of problem. When properly configured, a verified App Link opens your app instantly, on the right screen, with the correct content loaded, with no user choice required. When misconfigured, it fails silently in ways that are invisible unless you specifically test for them.
This guide is the complete implementation and testing reference for Android App Links in 2026 — covering the three-component architecture, the assetlinks.json file in full detail, every known failure mode and its fix, the fallback chain across Android versions, Dynamic App Links introduced in Android 15, the aftermath of Firebase Dynamic Links' deprecation, and the exact ADB commands and test matrix your QA team should run before every release that touches deep linking.
The Three Core Components of Android App Links
Android App Links require three components to work correctly. Missing or misconfiguring any one of them causes silent failures. All three must be correct simultaneously — and when they are, Android routes matching HTTP URLs directly to your app without user interaction.
| Component | Where It Lives | What It Does | Failure Mode |
|---|---|---|---|
| Intent Filters | AndroidManifest.xml | Declares which HTTP URL patterns your app can handle, with android:autoVerify="true" to trigger domain verification | Missing autoVerify, overlapping path patterns across activities, or incorrect android:host value causes verification skip or disambiguation dialog |
| Digital Asset Links File | https://yourdomain.com/.well-known/assetlinks.json | Declares that your domain trusts your Android app (by package name and SHA-256 signing fingerprint) to handle its URLs | Wrong content-type, HTTP redirect, fingerprint mismatch, missing host entries, or file size over 128 KB — all cause verification failure |
| Verification System | Android OS — fires at install/update time | Fetches and validates assetlinks.json for every host in your intent filters, stores result, uses it for all subsequent link resolution | Cache delays mean fixed config may not reflect on device for hours; requires reinstall to force re-verification during development |
Part One: The AndroidManifest.xml Intent Filter
The intent filter in your manifest tells Android which URLs your app wants to handle. For App Links — as opposed to basic deep links — the critical element is android:autoVerify="true", which tells Android to verify domain ownership at install time rather than just presenting a disambiguation dialog.
<activity android:name=".MainActivity"> <!-- Standard intent filter for launcher --> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <!-- App Links intent filter — autoVerify triggers domain verification --> <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 App Links — http alone is insufficient --> <data android:scheme="https" android:host="example.com" android:pathPrefix="/product" /> </intent-filter> <!-- Separate filter for www subdomain — Android treats subdomains as separate hosts --> <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="www.example.com" android:pathPrefix="/product" /> </intent-filter> </activity>
example.com and www.example.com are different hosts to the Android verification system. If your app handles links on both, you need separate intent filters for each and an assetlinks.json entry on each. The system queries https://example.com/.well-known/assetlinks.json and https://www.example.com/.well-known/assetlinks.json independently. A host mismatch — android:host in your manifest does not exactly match a hostname verified in your assetlinks.json — is one of the most common silent failure causes.
Part Two: The assetlinks.json File — The Most Failure-Prone Component
The Digital Asset Links file is the contract between your domain and your app. It must be hosted at exactly https://yourdomain.com/.well-known/assetlinks.json, served with the correct content-type header, without any HTTP redirect, and containing the correct package name and SHA-256 certificate fingerprint.
Every one of those requirements is a potential failure point. A redirect from HTTP to HTTPS, a CDN caching the wrong content-type, or an incorrect certificate fingerprint (using the debug signing fingerprint in a production build, for example) will all cause the verification to fail — silently, with no visible error on the device, only a fallback to the browser or disambiguation dialog when the link is tapped.
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.yourapp",
// SHA-256 fingerprint of your PRODUCTION signing certificate
// Get it with: keytool -list -v -keystore your-key.jks
// Or from Google Play Console → App Integrity → App signing key certificate
"sha256_cert_fingerprints": [
"AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78",
// Optional: include debug fingerprint for development testing
"12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB"
]
}
},
// Android 15+: Dynamic App Links configuration (optional)
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.yourapp",
"sha256_cert_fingerprints": ["AB:CD:EF:..."],
"dynamic_app_deep_link_components": [{
"include_patterns": ["/product/*", "/profile/*"],
"exclude_patterns": ["/product/admin"]
}]
}
}]How to Get Your SHA-256 Fingerprint
There are three sources for the correct fingerprint, and using the wrong one is a very common misconfiguration error.
# Method 1: From your keystore file (local signing) keytool -list -v -keystore /path/to/your-release.jks -alias your-alias # Look for: SHA256: AB:CD:EF:... in the output # Method 2: From Google Play App Signing (if using Play-managed signing) # Go to: Play Console → Your App → Setup → App Integrity → App signing key certificate # Copy the SHA-256 certificate fingerprint shown there # ⚠️ This is the ONLY correct fingerprint when using Play App Signing # Your local keystore fingerprint will NOT match — verification will fail # Method 3: From an installed APK (verify what's actually deployed) apksigner verify --print-certs /path/to/your-release.apk # Or: keytool -printcert -jarfile /path/to/your-release.apk # Verify your assetlinks.json is live and correct: curl -v https://yourdomain.com/.well-known/assetlinks.json # Confirm: HTTP/2 200, Content-Type: application/json, no redirects in the chain
If your app uses Google Play App Signing — which is the default for new apps since 2021 — Google re-signs your APK with a Google-managed certificate before distributing it. The SHA-256 fingerprint of that certificate is different from your upload key fingerprint. If you use your local upload key fingerprint in assetlinks.json, verification will fail for all Play Store installs. The correct fingerprint is found in Play Console → Your App → Setup → App Integrity → App signing key certificate. This is one of the most common causes of App Links working in local test builds but failing in production.
Part Three: Server and CDN Requirements for assetlinks.json
The file must be served exactly as the Android system expects it. Deviations that seem minor — a redirect, a slightly wrong content-type, a CDN caching header — fail the verification silently.
Serve at exactly
https://yourdomain.com/.well-known/assetlinks.json— no redirectsThe Android system fetches this path directly. If your server redirects HTTP to HTTPS, or
wwwto non-www, the fetch fails. The file must be available on HTTPS at the canonical path without any redirect in the chain. If your CDN or server adds a redirect on/.well-known/paths, you must add an explicit exception rule for that directory. On Cloudflare, add a Page Rule to exclude/.well-known/*from "Always Use HTTPS" redirect enforcement.Content-Type header must be
application/jsonSome servers serve
.jsonfiles with atext/plaincontent-type or no content-type at all. The Android verification system requiresapplication/json. For Firebase Hosting, add explicit headers to yourfirebase.json. For Vercel, add a headers rule innext.config.js. For Nginx, add atypesblock or explicitadd_headerfor the.well-knowndirectory.File size must be under 128 KB
If your app has many hosts and many certificates, the
assetlinks.jsonfile can grow large. The Android system has a 128 KB size limit. Files over this limit are rejected. If you are approaching this limit, use wildcards in yourpathPrefixvalues and consolidate entries rather than listing every individual path.Ensure the file is served from the exact domain — not a CDN subdomain
The Android system queries
https://yourdomain.com/.well-known/assetlinks.json— the same domain declared in your intent filter'sandroid:host. If your CDN routes the file from a different origin domain, or if a reverse proxy serves it from a different hostname, the verification fails. The response must come from the canonical domain as it appears in your manifest.
{
"hosting": {
"headers": [{
"source": "/.well-known/assetlinks.json",
"headers": [{
"key": "Content-Type",
"value": "application/json"
}]
}, {
"source": "/.well-known/apple-app-site-association",
"headers": [{
"key": "Content-Type",
"value": "application/json"
}]
}]
}
}Part Four: The Fallback Chain — What Actually Happens When Verification Fails
Understanding the fallback chain is essential for writing an adequate test plan. What Android does when App Link verification fails changed significantly in Android 12, and the change has ongoing implications for apps that have not been updated or tested since.
assetlinks.json is valid, fingerprint matches, host matches. User taps link → app opens to the correct content immediately. This is the state your implementation must achieve on all declared hosts.autoVerify="true". Android presents a dialog: "Open with [Your App] or [Browser]." Most users choose the browser or dismiss the dialog. Conversion rate from this state is very low. Still the behaviour on Android 6–11 when verification is absent.Before Android 12, a misconfigured App Link still gave the user a chance to choose your app via the disambiguation dialog. Many apps had broken or partially configured App Links that still "worked" because users would select the app from the dialog. Android 12 removed this fallback: broken verification now routes directly to the browser, with no dialog. Apps that had been living on the disambiguation dialog as an unofficial fallback for broken App Links saw a sudden, unexplained drop in app opens from deep links at the Android 12 rollout — not from an app change, but from an OS change to how fallbacks work.
Part Five: Dynamic App Links — Android 15's Server-Side Control
Android 15 introduced Dynamic App Links, one of the most significant changes to the App Links system since its introduction. Previously, assetlinks.json was used primarily for basic domain verification. With Dynamic App Links, it becomes a configuration file that can update your app's deep link behaviour without requiring a new app release.
Android devices with Google services periodically refresh your assetlinks.json file and apply new deep link rules dynamically. This means you can add, modify, or exclude URL patterns server-side — and those changes propagate to user devices without requiring them to update the app.
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.yourapp",
"sha256_cert_fingerprints": ["AB:CD:EF:..."],
"dynamic_app_deep_link_components": [{
// Paths that SHOULD open the app
"include_patterns": [
"/product/*",
"/profile/*",
"/order/*"
],
// Paths that should NEVER open the app even if they match
"exclude_patterns": [
"/product/admin",
"/profile/settings/delete"
]
}]
}
}]Practical applications of Dynamic App Links include: immediately excluding a path from App Link handling when a feature is sunset (without waiting for a release cycle), adding new product categories or sections to App Link coverage as they launch, and A/B testing deep link destinations by updating server-side routing rules. The dynamic refresh is not instantaneous — devices periodically fetch the file based on their own schedules — so Dynamic App Links are appropriate for non-time-critical configuration changes, not real-time routing decisions.
Part Six: Firebase Dynamic Links Is Dead — What to Do Now
Firebase Dynamic Links was deprecated in August 2023 and shut down completely in 2025. Any app still using Firebase Dynamic Links has broken deep links — affected users are experiencing failures silently, often without the development team knowing because the links resolve without crashing the app.
Firebase Dynamic Links links no longer work as of their shutdown date in 2025. If your app's deep linking, sharing functionality, or email campaigns use page.link or Firebase short link domains, those links are now broken or producing degraded experiences. Users who click them may see an error page, a generic web fallback, or nothing at all.
Migration requires: auditing all Firebase Dynamic Links in production; designing equivalent URL patterns using your own domain; configuring App Links intent filters and assetlinks.json; updating link generation in your app code and any email, SMS, or social campaigns; testing all user journeys end-to-end; and monitoring conversion rates during the transition period to catch silent regressions.
Short Links That Work With App Links — Not Against Them
Trimrly dynamic short links use 301 server-side redirects that preserve UTM parameters and full URLs through the redirect chain. Unlike generic shorteners, Trimrly short links are designed to work alongside App Links setups — the redirect passes cleanly to your tagged destination URL without interfering with App Link verification.
Part Seven: The Complete Test Matrix
Deep link behaviour varies across Android versions, install states, entry points, and browser environments. Testing only the happy path — link tapped on a device where the app is installed and verification passed — covers one of the roughly twelve distinct scenarios that affect users in the wild. Here is the complete matrix.
ADB Commands for Development Testing
# Test a specific App Link URL (replace with your domain and path) adb shell am start -W -a android.intent.action.VIEW \ -d "https://yourdomain.com/product/123" com.yourpackage.name # Expected: App launches directly to product screen # Failure: Browser opens or disambiguation dialog appears # Verify the assetlinks.json file is correctly served and parseable curl -v https://yourdomain.com/.well-known/assetlinks.json # Confirm: HTTP/2 200 OK, Content-Type: application/json, no redirects # Check App Link verification status on a connected device (Android Studio) # Tools → App Links Assistant → Verify Asset Links # Force re-verification after changing assetlinks.json (development only) # Uninstall the app, then reinstall — verification only runs at install/update adb uninstall com.yourpackage.name # Reinstall via Android Studio or adb install # Check Android's verification log for your app adb shell pm get-app-links --user cur com.yourpackage.name # Look for: "verified" = true for each host # Test fallback: what happens when the app is NOT installed adb uninstall com.yourpackage.name adb shell am start -a android.intent.action.VIEW -d "https://yourdomain.com/product/123" # Expected: Opens in browser — confirm your web fallback page is correct
Full Test Matrix
| Test Scenario | Android Version | Expected Behaviour | How to Test |
|---|---|---|---|
| Verified link, app installed, user taps | Android 6+ | App opens, correct screen | ADB command; Chrome URL bar; WhatsApp/SMS message link |
| Verified link, app not installed | All | Browser opens — check web fallback page is correct and mobile-optimised | Uninstall app; tap link |
| Unverified link (assetlinks broken), app installed | Android 6–11 | Disambiguation dialog shown | Break assetlinks.json temporarily; reinstall; tap link |
| Unverified link (assetlinks broken), app installed | Android 12+ | Browser opens, no dialog — silent failure | Same as above on Android 12+ device |
| Link from Gmail (app on Android) | All | Gmail may wrap link — may open browser instead of app | Email yourself the link; tap from Gmail |
| Link from WhatsApp | All | WhatsApp taps usually respect App Links | Send link to yourself in WhatsApp; tap |
| Link via email marketing (HubSpot, Mailchimp) | All | Tracking wrapper rewrites URL — App Link will not trigger. Browser opens | Send a tracked campaign email; tap link from Gmail/Android mail |
| Link via bit.ly or generic shortener | All | Redirect domain is not your verified domain. Browser always opens | Create a bit.ly link to your App Link URL; tap |
| Link opened inside Chrome Custom Tab | All | Chrome Custom Tabs support App Link interception in most cases | Test from apps that use CCT (Twitter, LinkedIn) |
| JavaScript redirect to App Link URL | All | JavaScript redirects are not user-initiated — App Link interception skipped | Build test page with window.location.href redirect; test on device |
| QR code scan → App Link URL | Android 9+ | QR scan via camera app triggers App Links correctly | Print or display QR code; scan with default camera app |
| Path not declared in manifest | All | Browser opens — intent filter must cover the path | Test a URL path not in your pathPrefix list |
The Complete Failure Mode Checklist
- ✕
Wrong SHA-256 fingerprint — most common cause of production failure. Using the local keystore fingerprint when the app is distributed via Play App Signing. The correct fingerprint is in Play Console → App Integrity, not in your local
.jksfile. Verification works in local debug builds and fails in production. Fix: updateassetlinks.jsonwith the Play App Signing certificate fingerprint. - ✕
HTTP redirect on the
assetlinks.jsonpath. The most common CDN issue. Cloudflare's "Always Use HTTPS" and similar redirect rules intercept the/.well-known/path. Android's verification system does not follow redirects. Fix: add a Page Rule or routing exception that serves/.well-known/assetlinks.jsondirectly from origin without redirect. - ✕
Wrong
Content-Typeheader onassetlinks.json. Servers serving.jsonfiles astext/plaincause verification failure. Add explicit content-type configuration to your web server, CDN, or hosting platform for the/.well-known/directory. - ✕
Missing host entries in
assetlinks.json. If your manifest declaresandroid:host="example.com"andandroid:host="www.example.com", both must have their ownassetlinks.jsonat their respective paths. Android verifies each host independently. - ✕
Overlapping intent-filter paths across activities. If two activities in your app both have intent filters that match the same URL pattern, Android may show a disambiguation dialog or fail to determine a single handler. Intent filters across activities must not overlap. The Android Docs troubleshooting guide identifies this as a common disambiguation dialog trigger even when App Links are otherwise correctly configured.
- ✕
Not reinstalling after fixing verification config during development. Android verification runs only at install or update time. Changes to
assetlinks.jsonor the manifest do not take effect until the app is reinstalled. Uninstall and reinstall — do not simply rebuild and run — after making any verification-related change. Server-side cache may add additional delay; retry after a few hours if the issue persists after reinstall. - ✕
Email marketing tracking wrappers. SendGrid, HubSpot, Mailchimp, and similar platforms wrap all tracked links through their own redirect domains. A link to
https://yourapp.com/product/123becomeshttps://click.sendgrid.net/wf/click?upn=.... Android only trusts your verified domain. Tracking-wrapped links will always open in the browser — not the app. This is expected and by design, not a bug in your setup.
"Deep links that sometimes open the app are not intermittently working. They are systematically broken in the scenarios you have not tested."
Frequently Asked Questions
Android 12 (API level 31) changed the fallback behaviour for unverified App Links. Before Android 12, a link that your app declared in its intent filters but could not verify would show a disambiguation dialog — giving users a chance to choose your app. Starting Android 12, unverified web links resolve directly to the browser with no dialog. Apps that relied on the disambiguation dialog as a fallback for broken verification saw a sudden unexplained drop in app opens from links at the Android 12 rollout.
Dynamic App Links, introduced in Android 15, allow app developers to update deep linking rules server-side via assetlinks.json without releasing a new app version. You can add, modify, or exclude URL patterns using the dynamic_app_deep_link_components field. Android devices with Google services periodically refresh the file and apply new rules automatically. This enables scenarios like adding new sections to App Link coverage at launch, excluding deprecated paths from app handling, and managing deep link routing without requiring app updates.
The most common cause is a SHA-256 fingerprint mismatch. If your app uses Google Play App Signing, the certificate used to sign the distributed APK is Google's App Signing certificate — not your upload key. Your assetlinks.json must contain the App Signing certificate fingerprint from Play Console → Setup → App Integrity, not the fingerprint from your local .jks file. Debug builds are signed with a different certificate, so a debug fingerprint in assetlinks.json works for debug installs but fails for production Play Store installs.
Yes — generic shorteners break App Links by design. Android App Links verification is domain-specific: only your verified domain triggers your app. When a user clicks a bit.ly link that redirects to your App Link URL, the OS intercepts the bit.ly domain, not your domain. Since bit.ly is not your verified domain, the link opens in the browser. The same applies to any shortener domain that is not your own verified domain. If you need short links that preserve App Link behaviour, the destination URL must eventually be your verified domain and the redirect must be a server-side redirect without a JavaScript or client-side hop that prevents the OS from resolving the final URL at the system level.
Uninstall and reinstall the app. Verification only runs at install or update time. After reinstalling, run adb shell pm get-app-links --user cur com.yourpackage.name to check verification status. Note that server-side CDN caches may delay the delivery of an updated assetlinks.json file even after reinstalling. If verification still fails after reinstall, wait a few hours for the cache to expire and try again. In Android Studio, the App Links Assistant (Tools → App Links Assistant → Verify Asset Links) runs on-device verification and shows the result without requiring a manual reinstall.