{"id":559,"date":"2026-03-21T09:00:00","date_gmt":"2026-03-21T14:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=559"},"modified":"2026-03-07T03:32:53","modified_gmt":"2026-03-07T08:32:53","slug":"android-app-link-verification-process","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/android-app-link-verification-process\/","title":{"rendered":"Android App Link Verification: How the Process Works"},"content":{"rendered":"\n<p>Android App Link verification is what separates a fully functional deep link from a URL that opens a browser chooser. When verification succeeds, Android routes matching URLs directly to your app. When it fails, users see a dialog asking which app to open, or the link opens in a browser entirely.<\/p>\n\n\n\n<p>Most App Link problems trace back to verification. Understanding exactly how the process works, what Android checks, when it checks, and what causes failures will save significant debugging time.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Verification Model<\/h2>\n\n\n\n<p>The foundation of App Link verification is the <a href=\"https:\/\/developers.google.com\/digital-asset-links\/v1\/getting-started\" rel=\"nofollow noopener\" target=\"_blank\">Digital Asset Links<\/a> protocol. Your app claims ownership of a domain by declaring intent filters with <code>android:autoVerify=&quot;true&quot;<\/code>. Android then verifies that claim by checking whether your domain publishes a file that acknowledges your app&#39;s signing certificate.<\/p>\n\n\n\n<p>This is a two-way handshake: your app says &quot;I handle URLs on this domain,&quot; and your domain says &quot;I authorize this app (identified by its signing certificate) to handle my URLs.&quot; Both sides must agree, or verification fails.<\/p>\n\n\n\n<p>The file Android checks is <code>assetlinks.json<\/code>, hosted at <code>https:\/\/yourdomain.com\/.well-known\/assetlinks.json<\/code>. The path and filename are fixed by the protocol.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What the assetlinks.json File Must Contain<\/h2>\n\n\n\n<p>The minimum valid structure for a single app:<\/p>\n\n\n\n<pre><code class=\"language-json\">[{\n  &quot;relation&quot;: [&quot;delegate_permission\/common.handle_all_urls&quot;],\n  &quot;target&quot;: {\n    &quot;namespace&quot;: &quot;android_app&quot;,\n    &quot;package_name&quot;: &quot;com.yourcompany.app&quot;,\n    &quot;sha256_cert_fingerprints&quot;: [\n      &quot;A1:B2:C3:D4:E5:F6:...&quot;\n    ]\n  }\n}]\n<\/code><\/pre>\n\n\n\n<p>The <code>sha256_cert_fingerprints<\/code> array accepts multiple fingerprints, which matters if you have both a debug build and a release build, or if you use multiple signing keys. Each fingerprint is a colon-delimited uppercase hex string.<\/p>\n\n\n\n<p>To get the fingerprint for your release keystore:<\/p>\n\n\n\n<pre><code class=\"language-bash\">keytool -list -v \\\n  -keystore \/path\/to\/release.keystore \\\n  -alias your-alias\n<\/code><\/pre>\n\n\n\n<p>If your app is signed by Google Play (using Play App Signing), get the fingerprint from the Play Console under Setup &gt; App integrity &gt; App signing key certificate. The fingerprint from your upload key will not match what Play uses to sign the distributed APK.<\/p>\n\n\n\n<p>This is one of the most common causes of verification failure: the fingerprint in <code>assetlinks.json<\/code> matches the upload key, not the Play signing key.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Server Requirements for Verification<\/h2>\n\n\n\n<p><img decoding=\"async\" src=\"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/platform-platform-domains.png\" alt=\"Tolinku custom domain configuration for deep links\"><\/p>\n\n\n\n<p>Android&#39;s verification agent has specific requirements for how your server must respond. All of these must be true:<\/p>\n\n\n\n<p><strong>HTTPS only.<\/strong> The file must be accessible via HTTPS. HTTP is not checked. Your SSL certificate must be valid (not expired, not self-signed, not a certificate for a different domain).<\/p>\n\n\n\n<p><strong>Status 200.<\/strong> The server must return HTTP 200. Redirects are not followed. If <code>\/.well-known\/assetlinks.json<\/code> redirects to another path, verification will fail. This catches setups where a CDN redirects bare paths, or where <code>www.yourdomain.com<\/code> redirects to <code>yourdomain.com<\/code> but the intent filter specifies the former.<\/p>\n\n\n\n<p><strong>Correct Content-Type.<\/strong> The response must have <code>Content-Type: application\/json<\/code>. Some servers serve files from <code>.well-known\/<\/code> with <code>application\/octet-stream<\/code> or <code>text\/plain<\/code>. Both will cause verification to fail.<\/p>\n\n\n\n<p><strong>No authentication.<\/strong> The file must be publicly accessible without any authentication headers, cookies, or IP restrictions. Android&#39;s verification system cannot authenticate.<\/p>\n\n\n\n<p><strong>Reasonable response time.<\/strong> Android has a timeout for the verification check. If your server is slow or the file is behind a CDN edge that takes too long on first request, verification may time out.<\/p>\n\n\n\n<p><strong>Valid JSON.<\/strong> The file must parse as valid JSON. A trailing comma, unclosed bracket, or encoding issue will cause the verification to fail silently.<\/p>\n\n\n\n<p>You can test server compliance with:<\/p>\n\n\n\n<pre><code class=\"language-bash\">curl -v &quot;https:\/\/yourdomain.com\/.well-known\/assetlinks.json&quot;\n<\/code><\/pre>\n\n\n\n<p>Check the response code, Content-Type header, and that the body is valid JSON.<\/p>\n\n\n\n<p>For multi-domain setups, each domain needs its own <code>assetlinks.json<\/code>. A common mistake is hosting the file only on the primary domain when intent filters cover subdomains.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">When Android Runs Verification<\/h2>\n\n\n\n<p>Android does not verify on every link click. Verification happens at specific points:<\/p>\n\n\n\n<p><strong>On app install.<\/strong> When your app is installed (or updated), Android reads the intent filters with <code>android:autoVerify=&quot;true&quot;<\/code> and schedules verification for each unique domain.<\/p>\n\n\n\n<p><strong>Immediately after install.<\/strong> Android attempts verification shortly after the install completes, using background network access.<\/p>\n\n\n\n<p><strong>On periodic re-verification.<\/strong> Android re-verifies periodically to detect if the domain&#39;s <code>assetlinks.json<\/code> has changed. This is not on a fixed schedule and is not user-visible.<\/p>\n\n\n\n<p><strong>After a failed verification.<\/strong> If verification fails, Android does not continuously retry. It marks the domain as unverified and may attempt again at the next install or update.<\/p>\n\n\n\n<p>This timing has practical implications. If you ship your app before your server is ready to serve <code>assetlinks.json<\/code>, the app will install without App Link verification. Users who installed during that window will have unverified links until they reinstall or update the app.<\/p>\n\n\n\n<p>The verification result is stored per domain per app. It is not re-checked at launch time.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Verification States<\/h2>\n\n\n\n<p>You can inspect the current verification state using ADB:<\/p>\n\n\n\n<pre><code class=\"language-bash\">adb shell pm get-app-links com.yourcompany.app\n<\/code><\/pre>\n\n\n\n<p>Output looks like:<\/p>\n\n\n\n<pre><code>com.yourcompany.app:\n    ID: a1b2c3d4\n    Signatures: [AA:BB:CC:...]\n    Domain verification state:\n      yourdomain.com: verified\n      sub.yourdomain.com: failed\n<\/code><\/pre>\n\n\n\n<p>The possible states are:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>verified<\/code>: Android confirmed the domain and the app are linked.<\/li>\n<li><code>failed<\/code>: Verification was attempted and failed.<\/li>\n<li><code>pending<\/code>: Verification has been scheduled but not yet completed.<\/li>\n<li><code>none<\/code>: No verification has been attempted for this domain.<\/li>\n<\/ul>\n\n\n\n<p>If you see <code>failed<\/code>, the next step is determining why. On Android 12 and higher, the <a href=\"https:\/\/developer.android.com\/training\/app-links\/verify-android-applinks#request-verify\" rel=\"nofollow noopener\" target=\"_blank\">Domain Verification API<\/a> lets you request re-verification for testing:<\/p>\n\n\n\n<pre><code class=\"language-bash\">adb shell pm set-app-links --package com.yourcompany.app 0 all\nadb shell pm verify-app-links --re-verify com.yourcompany.app\n<\/code><\/pre>\n\n\n\n<p>The first command resets the verification state. The second triggers a fresh verification attempt. Wait a few seconds, then run <code>get-app-links<\/code> again to see the new state.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Multiple Domains<\/h2>\n\n\n\n<p>If your app handles URLs on multiple domains, each domain needs its own <code>assetlinks.json<\/code> and each domain must be listed in your intent filters.<\/p>\n\n\n\n<p><img decoding=\"async\" src=\"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/doc-android-add-domains-dialog.svg\" alt=\"Android add domains dialog for App Link verification\"><\/p>\n\n\n\n<p><em>Source: <a href=\"https:\/\/developer.android.com\/training\/app-links\/verify-android-applinks\" rel=\"nofollow noopener\" target=\"_blank\">Android Developer Documentation<\/a><\/em><\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;intent-filter android:autoVerify=&quot;true&quot;&gt;\n    &lt;action android:name=&quot;android.intent.action.VIEW&quot; \/&gt;\n    &lt;category android:name=&quot;android.intent.category.DEFAULT&quot; \/&gt;\n    &lt;category android:name=&quot;android.intent.category.BROWSABLE&quot; \/&gt;\n    &lt;data android:scheme=&quot;https&quot; android:host=&quot;yourdomain.com&quot; \/&gt;\n    &lt;data android:scheme=&quot;https&quot; android:host=&quot;www.yourdomain.com&quot; \/&gt;\n    &lt;data android:scheme=&quot;https&quot; android:host=&quot;partner.yourdomain.com&quot; \/&gt;\n&lt;\/intent-filter&gt;\n<\/code><\/pre>\n\n\n\n<p>Note that <code>yourdomain.com<\/code> and <code>www.yourdomain.com<\/code> are treated as distinct domains. Both need intent filter entries and both need <code>assetlinks.json<\/code> files. If <code>www<\/code> redirects to the apex domain, you still need the <code>assetlinks.json<\/code> on the <code>www<\/code> domain to be served directly (not via redirect).<\/p>\n\n\n\n<p>For subdomains controlled by a platform or CDN, verify that each subdomain can independently serve the file. Some CDN configurations serve the file correctly on the primary domain but not on subdomains.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Include Mechanism<\/h2>\n\n\n\n<p>If you manage many subdomains, maintaining <code>assetlinks.json<\/code> on each one is cumbersome. The Digital Asset Links protocol supports an <code>include<\/code> statement:<\/p>\n\n\n\n<pre><code class=\"language-json\">[{\n  &quot;include&quot;: &quot;https:\/\/yourdomain.com\/.well-known\/assetlinks.json&quot;\n}]\n<\/code><\/pre>\n\n\n\n<p>A subdomain&#39;s <code>assetlinks.json<\/code> can include the primary domain&#39;s file. Android will follow the include and use the authoritative file. Includes can be nested, but keep the chain short to avoid timeout issues.<\/p>\n\n\n\n<p>This is useful when you have many subdomains (for example, per-user subdomains in a SaaS product) and want to maintain a single authoritative file.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Wildcards<\/h2>\n\n\n\n<p>Android App Links do not support wildcard domains in intent filters. You cannot specify <code>android:host=&quot;*.yourdomain.com&quot;<\/code> and have it match all subdomains. Each subdomain must be explicitly listed.<\/p>\n\n\n\n<p>This is a meaningful constraint for apps with dynamic subdomains. One common pattern is to not use subdomains for the deep-linked paths, and instead use path parameters: <code>yourdomain.com\/user\/username\/<\/code> rather than <code>username.yourdomain.com\/<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why Verification Fails in Production<\/h2>\n\n\n\n<p>Beyond the server configuration issues covered above, these are the most common production verification failures:<\/p>\n\n\n\n<p><strong>Play App Signing fingerprint mismatch.<\/strong> This is the single most common cause. If your app is enrolled in Play App Signing (which is now the default for new apps), the fingerprint in <code>assetlinks.json<\/code> must match the signing key that Google Play uses, not your upload key. Get the correct fingerprint from the Play Console.<\/p>\n\n\n\n<p><strong>CDN caching the wrong response.<\/strong> If you initially served a 404 or wrong Content-Type, and a CDN cached that response, Android may see the cached error response even after you fix the underlying file. Purge your CDN cache after making changes.<\/p>\n\n\n\n<p><strong>Different fingerprints for debug and release.<\/strong> Debug builds use a different signing certificate than release builds. If you test with a debug APK against a <code>assetlinks.json<\/code> that only has the release fingerprint, verification will fail on your test device.<\/p>\n\n\n\n<p><strong>File served only on certain paths.<\/strong> Some routing configurations only handle certain paths. Verify that <code>\/.well-known\/assetlinks.json<\/code> is not being caught by a catch-all redirect rule or authentication middleware.<\/p>\n\n\n\n<p><strong>Clock skew on SSL certificates.<\/strong> If your server&#39;s clock is significantly wrong, SSL certificate validation may fail, causing the HTTPS request to fail before the file is even fetched.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Verification on Android 11 and Earlier<\/h2>\n\n\n\n<p>The verification behavior changed with Android 12. On Android 11 and earlier, a single failed domain verification would cause all App Links in the same intent filter to fall back to browser behavior. On Android 12 and later, each domain is verified independently.<\/p>\n\n\n\n<p>If you support Android 11, split intent filters by domain rather than combining them:<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;!-- Separate intent filter per domain for Android 11 compatibility --&gt;\n&lt;intent-filter android:autoVerify=&quot;true&quot;&gt;\n    &lt;action android:name=&quot;android.intent.action.VIEW&quot; \/&gt;\n    &lt;category android:name=&quot;android.intent.category.DEFAULT&quot; \/&gt;\n    &lt;category android:name=&quot;android.intent.category.BROWSABLE&quot; \/&gt;\n    &lt;data android:scheme=&quot;https&quot; android:host=&quot;yourdomain.com&quot; \/&gt;\n&lt;\/intent-filter&gt;\n\n&lt;intent-filter android:autoVerify=&quot;true&quot;&gt;\n    &lt;action android:name=&quot;android.intent.action.VIEW&quot; \/&gt;\n    &lt;category android:name=&quot;android.intent.category.DEFAULT&quot; \/&gt;\n    &lt;category android:name=&quot;android.intent.category.BROWSABLE&quot; \/&gt;\n    &lt;data android:scheme=&quot;https&quot; android:host=&quot;www.yourdomain.com&quot; \/&gt;\n&lt;\/intent-filter&gt;\n<\/code><\/pre>\n\n\n\n<p>This ensures a verification failure on one domain does not affect the other.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Testing Verification Locally<\/h2>\n\n\n\n<p>During development, you cannot verify against <code>localhost<\/code> or an internal IP. App Links require a publicly accessible HTTPS domain. Options for local testing:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Use a service like ngrok to expose a local server via a public HTTPS URL<\/li>\n<li>Deploy to a staging environment with your real domain under a subdomain<\/li>\n<li>Manually override App Link handling during development using ADB to set the domain as verified without actual network verification<\/li>\n<\/ul>\n\n\n\n<p>The ADB override approach is useful for UI testing:<\/p>\n\n\n\n<pre><code class=\"language-bash\">adb shell pm set-app-links-user-selection \\\n  --user 0 \\\n  --package com.yourcompany.app \\\n  enabled \\\n  yourdomain.com\n<\/code><\/pre>\n\n\n\n<p>This tells Android to route App Links for that domain to your app without verification, which is useful for automated tests that do not have a live server.<\/p>\n\n\n\n<p>For a practical setup guide covering hosting and configuration, see the <a href=\"https:\/\/tolinku.com\/docs\/developer\/app-links\/\">Android App Links developer documentation<\/a> and the <a href=\"https:\/\/tolinku.com\/docs\/user-guide\/configuring-android\/\">configuring Android guide<\/a>. If you are working through verification failures, the <a href=\"https:\/\/tolinku.com\/docs\/troubleshooting\/android\/\">troubleshooting guide<\/a> has a diagnostic checklist.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Summary<\/h2>\n\n\n\n<p>Android App Link verification is straightforward when everything is configured correctly, and opaque when something is wrong. The key points:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The <code>assetlinks.json<\/code> file must be served at exactly the right path, with HTTPS, status 200, and <code>Content-Type: application\/json<\/code><\/li>\n<li>The SHA-256 fingerprint must match the certificate used to sign the distributed APK, not your upload key or debug key<\/li>\n<li>Verification happens at install and update time, not at click time<\/li>\n<li>Each domain is verified independently on Android 12 and later<\/li>\n<li><code>adb shell pm get-app-links<\/code> is your primary diagnostic tool<\/li>\n<li>Redirects on the <code>assetlinks.json<\/code> path will silently fail verification<\/li>\n<\/ul>\n\n\n\n<p>Getting verification right once means your deep links work reliably across all Android versions your app supports. The investment in understanding this process pays off every time a user clicks a link and lands exactly where they expected.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Android App Link verification is the mechanism that lets your app intercept HTTPS URLs without a chooser dialog. This deep dive covers the full verification process, server requirements, timing, retry behavior, and how to diagnose failures.<\/p>\n","protected":false},"author":2,"featured_media":558,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Android App Link Verification: How It Works (2026)","rank_math_description":"A deep dive into Android App Link verification. Learn server requirements, when Android verifies, retry behavior, multi-domain setup, and how to fix verification failures.","rank_math_focus_keyword":"app link verification process","rank_math_canonical_url":"","rank_math_facebook_title":"","rank_math_facebook_description":"","rank_math_facebook_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-android-app-link-verification-process.png","rank_math_facebook_image_id":"","rank_math_twitter_title":"","rank_math_twitter_description":"","rank_math_twitter_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-android-app-link-verification-process.png","footnotes":""},"categories":[10],"tags":[25,23,92,93,87,91],"class_list":["post-559","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-android","tag-android","tag-app-links","tag-assetlinks","tag-security","tag-troubleshooting","tag-verification"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/559","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/comments?post=559"}],"version-history":[{"count":1,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/559\/revisions"}],"predecessor-version":[{"id":560,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/559\/revisions\/560"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/558"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=559"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=559"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=559"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}