{"id":367,"date":"2026-03-05T17:00:00","date_gmt":"2026-03-05T22:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=367"},"modified":"2026-03-07T04:33:12","modified_gmt":"2026-03-07T09:33:12","slug":"android-app-links-complete-guide","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/android-app-links-complete-guide\/","title":{"rendered":"Android App Links: Complete Implementation Guide"},"content":{"rendered":"\n<p>Android App Links let your app open directly when a user taps a URL that belongs to your domain. No disambiguation dialog. No browser redirect. The user taps a link, and your app handles it immediately.<\/p>\n\n\n\n<p>This is different from regular deep links or custom URI schemes. Android App Links are verified: Google confirms that you own the domain and that the domain explicitly trusts your app. Once verification passes, Android routes matching URLs straight to your app without asking the user to choose between your app and a browser.<\/p>\n\n\n\n<p>This guide covers every step of the implementation process, from hosting the Digital Asset Links file to handling incoming links in Kotlin, testing with <code>adb<\/code>, and debugging common verification failures. For iOS, see the equivalent guide: <a href=\"https:\/\/tolinku.com\/blog\/universal-links-everything-you-need-to-know\/\">Universal Links: everything you need to know<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">How Android App Links Differ from Regular Deep Links<\/h2>\n\n\n\n<p>Android supports three types of links that can open apps:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><p><strong>Custom URI schemes<\/strong> (e.g., <code>myapp:\/\/product\/123<\/code>). These are not web URLs. They work only if the app is installed, and any app can claim the same scheme. There is no ownership verification.<\/p>\n\n\n\n<p><strong>Standard deep links<\/strong> (regular <code>http<\/code>\/<code>https<\/code> intent filters without <code>autoVerify<\/code>). These trigger the disambiguation dialog, where Android asks the user which app or browser should handle the URL.<\/p>\n\n\n\n<p><strong>Android App Links<\/strong> (<code>http<\/code>\/<code>https<\/code> intent filters with <code>autoVerify=&quot;true&quot;<\/code>). These skip the disambiguation dialog entirely because Android has verified that the domain owner authorized the app.<\/p>\n\n\n\n<p>The key difference is trust. With App Links, you prove domain ownership through a <a href=\"https:\/\/developers.google.com\/digital-asset-links\/v1\/getting-started\" rel=\"nofollow noopener\" target=\"_blank\">Digital Asset Links<\/a> file hosted on your server. Android downloads this file during app installation (or update) and checks it against your app&#39;s signing certificate. If everything matches, your app becomes the default handler for those URLs.<\/p>\n\n\n\n<p>For a broader overview of how App Links fit into the deep linking landscape, see the <a href=\"https:\/\/tolinku.com\/docs\/concepts\/app-links\/\">Tolinku App Links concepts documentation<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Verification Flow<\/h2>\n\n\n\n<p>Here is what happens under the hood when a user installs your app:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Android reads your app&#39;s manifest and finds intent filters with <code>android:autoVerify=&quot;true&quot;<\/code>.<\/li>\n<li>For each declared domain, Android sends a request to <code>https:\/\/{domain}\/.well-known\/assetlinks.json<\/code>.<\/li>\n<li>Android parses the JSON response and checks whether your app&#39;s package name and SHA-256 certificate fingerprint are listed.<\/li>\n<li>If the file is valid and the fingerprint matches, Android marks your app as the verified handler for that domain.<\/li>\n<li>From that point forward, any URL matching your intent filter pattern opens your app directly.<\/li>\n<\/ol>\n\n\n\n<p>If verification fails for any domain in your intent filter, Android falls back to the disambiguation dialog for URLs on that domain. Verification must succeed for every domain you declare; a single failure does not block other domains from verifying.<\/p>\n\n\n\n<p>On Android 12 (API 31) and later, the verification process changed. For details on Android 12+ changes, see <a href=\"https:\/\/tolinku.com\/blog\/android-12-app-links-changes\/\">Android 12 App Links changes<\/a>. The system now uses a <a href=\"https:\/\/developer.android.com\/training\/app-links\/verify-android-applinks\" rel=\"nofollow noopener\" target=\"_blank\">domain verification API<\/a> that re-verifies links periodically rather than only at install time. This means fixing a broken <code>assetlinks.json<\/code> file can resolve verification without requiring users to reinstall your app.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Digital Asset Links File<\/h2>\n\n\n\n<p>The Digital Asset Links file is the cornerstone of App Links verification. It is a JSON file hosted at a specific path on your domain.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">File Location<\/h3>\n\n\n\n<p>The file must be accessible at:<\/p>\n\n\n\n<pre><code>https:\/\/yourdomain.com\/.well-known\/assetlinks.json\n<\/code><\/pre>\n\n\n\n<p>For a step-by-step setup walkthrough, see the <a href=\"https:\/\/tolinku.com\/blog\/digital-asset-links-setup\/\">Digital Asset Links setup guide<\/a>. No redirects. No HTTP (it must be HTTPS). The response must have a <code>Content-Type<\/code> of <code>application\/json<\/code>. The URL must return a <code>200<\/code> status code.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">File Format<\/h3>\n\n\n\n<pre><code class=\"language-json\">[\n  {\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.example.myapp&quot;,\n      &quot;sha256_cert_fingerprints&quot;: [\n        &quot;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:90&quot;\n      ]\n    }\n  }\n]\n<\/code><\/pre>\n\n\n\n<p>The file is a JSON array. Each object declares a relationship between your domain and an Android app. You can list multiple apps (for example, a production build and a debug build) by adding more objects to the array.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Getting Your SHA-256 Fingerprint<\/h3>\n\n\n\n<p>For your debug keystore:<\/p>\n\n\n\n<pre><code class=\"language-bash\">keytool -list -v -keystore ~\/.android\/debug.keystore -alias androiddebugkey -storepass android -keypass android\n<\/code><\/pre>\n\n\n\n<p>For your release keystore:<\/p>\n\n\n\n<pre><code class=\"language-bash\">keytool -list -v -keystore \/path\/to\/your-release-key.keystore -alias your-alias\n<\/code><\/pre>\n\n\n\n<p>If you use <a href=\"https:\/\/support.google.com\/googleplay\/android-developer\/answer\/9842756\" rel=\"nofollow noopener\" target=\"_blank\">Google Play App Signing<\/a>, you need the fingerprint from the Play Console, not from your local upload key. Go to <strong>Play Console &gt; Your App &gt; Setup &gt; App signing<\/strong> and copy the SHA-256 fingerprint from the &quot;App signing key certificate&quot; section.<\/p>\n\n\n\n<p>This is a common mistake. If you use Play App Signing (which is now required for new apps), your local keystore fingerprint will not match what Android uses for verification on production devices. You need both fingerprints in your <code>assetlinks.json<\/code>: the Play signing key for production and your upload key for local testing.<\/p>\n\n\n\n<pre><code class=\"language-json\">[\n  {\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.example.myapp&quot;,\n      &quot;sha256_cert_fingerprints&quot;: [\n        &quot;AA:BB:CC:...:PLAY_SIGNING_KEY_FINGERPRINT&quot;,\n        &quot;DD:EE:FF:...:UPLOAD_KEY_FINGERPRINT&quot;\n      ]\n    }\n  }\n]\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Setting Up Intent Filters<\/h2>\n\n\n\n<p>Your app declares which URLs it handles through intent filters in <code>AndroidManifest.xml<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Basic Intent Filter<\/h3>\n\n\n\n<pre><code class=\"language-xml\">&lt;activity\n    android:name=&quot;.MainActivity&quot;\n    android:exported=&quot;true&quot;&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\n            android:scheme=&quot;https&quot;\n            android:host=&quot;yourdomain.com&quot;\n            android:pathPrefix=&quot;\/product&quot; \/&gt;\n    &lt;\/intent-filter&gt;\n\n&lt;\/activity&gt;\n<\/code><\/pre>\n\n\n\n<p>The critical attribute is <code>android:autoVerify=&quot;true&quot;<\/code>. Without it, Android treats the intent filter as a regular deep link and shows the disambiguation dialog.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Path Matching Options<\/h3>\n\n\n\n<p>Android supports three path matching attributes:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Attribute<\/th>\n<th>Example<\/th>\n<th>Matches<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td><code>android:path<\/code><\/td>\n<td><code>\/product\/featured<\/code><\/td>\n<td>Exact path only<\/td>\n<\/tr>\n<tr>\n<td><code>android:pathPrefix<\/code><\/td>\n<td><code>\/product<\/code><\/td>\n<td><code>\/product<\/code>, <code>\/product\/123<\/code>, <code>\/product\/abc\/details<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>android:pathPattern<\/code><\/td>\n<td><code>\/product\/.*\/details<\/code><\/td>\n<td>Paths matching the pattern (uses <code>.*<\/code> for wildcards)<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<p>For most deep linking setups, <code>pathPrefix<\/code> provides the right balance between specificity and flexibility.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Multiple Domains<\/h3>\n\n\n\n<p>If your app handles links from multiple domains, add separate <code>&lt;data&gt;<\/code> elements or separate intent filters:<\/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; \/&gt;\n    &lt;data android:host=&quot;yourdomain.com&quot; \/&gt;\n    &lt;data android:host=&quot;www.yourdomain.com&quot; \/&gt;\n    &lt;data android:pathPrefix=&quot;\/product&quot; \/&gt;\n&lt;\/intent-filter&gt;\n<\/code><\/pre>\n\n\n\n<p>Remember: each domain listed here must have its own valid <code>assetlinks.json<\/code> file. If <code>www.yourdomain.com<\/code> does not serve the file correctly, verification fails for that domain (though <code>yourdomain.com<\/code> can still verify independently on Android 12+).<\/p>\n\n\n\n<p>For more details on configuring your Android app with Tolinku, see the <a href=\"https:\/\/tolinku.com\/docs\/user-guide\/configuring-android\/\">Android configuration guide<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Handling Deep Links in Your Activity<\/h2>\n\n\n\n<p>Once Android opens your activity through an App Link, you need to read the incoming URL and navigate the user to the right screen.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Basic Kotlin Handling<\/h3>\n\n\n\n<pre><code class=\"language-kotlin\">class MainActivity : ComponentActivity() {\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        handleDeepLink(intent)\n    }\n\n    override fun onNewIntent(intent: Intent) {\n        super.onNewIntent(intent)\n        handleDeepLink(intent)\n    }\n\n    private fun handleDeepLink(intent: Intent) {\n        if (intent.action != Intent.ACTION_VIEW) return\n\n        val uri = intent.data ?: return\n        val path = uri.path ?: return\n\n        when {\n            path.startsWith(&quot;\/product\/&quot;) -&gt; {\n                val productId = uri.lastPathSegment\n                navigateToProduct(productId)\n            }\n            path.startsWith(&quot;\/profile\/&quot;) -&gt; {\n                val username = uri.lastPathSegment\n                navigateToProfile(username)\n            }\n            path.startsWith(&quot;\/promo&quot;) -&gt; {\n                val code = uri.getQueryParameter(&quot;code&quot;)\n                navigateToPromo(code)\n            }\n            else -&gt; {\n                \/\/ Fallback: open the home screen\n                navigateToHome()\n            }\n        }\n    }\n}\n<\/code><\/pre>\n\n\n\n<p>Two things to notice. First, you must handle the intent in both <code>onCreate<\/code> (cold start) and <code>onNewIntent<\/code> (app already running). If you skip <code>onNewIntent<\/code>, links tapped while the app is in the background will be silently ignored. Second, always include a fallback for unrecognized paths so users do not land on a blank screen.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Jetpack Compose with Navigation<\/h3>\n\n\n\n<p>If you are using <a href=\"https:\/\/developer.android.com\/develop\/ui\/compose\" rel=\"nofollow noopener\" target=\"_blank\">Jetpack Compose<\/a> with the Navigation component, you can declare deep links directly in your navigation graph:<\/p>\n\n\n\n<pre><code class=\"language-kotlin\">@Composable\nfun AppNavigation() {\n    val navController = rememberNavController()\n\n    NavHost(\n        navController = navController,\n        startDestination = &quot;home&quot;\n    ) {\n        composable(&quot;home&quot;) {\n            HomeScreen()\n        }\n\n        composable(\n            route = &quot;product\/{productId}&quot;,\n            arguments = listOf(navArgument(&quot;productId&quot;) { type = NavType.StringType }),\n            deepLinks = listOf(\n                navDeepLink {\n                    uriPattern = &quot;https:\/\/yourdomain.com\/product\/{productId}&quot;\n                }\n            )\n        ) { backStackEntry -&gt;\n            val productId = backStackEntry.arguments?.getString(&quot;productId&quot;)\n            ProductScreen(productId = productId)\n        }\n\n        composable(\n            route = &quot;profile\/{username}&quot;,\n            deepLinks = listOf(\n                navDeepLink {\n                    uriPattern = &quot;https:\/\/yourdomain.com\/profile\/{username}&quot;\n                }\n            )\n        ) { backStackEntry -&gt;\n            val username = backStackEntry.arguments?.getString(&quot;username&quot;)\n            ProfileScreen(username = username)\n        }\n    }\n}\n<\/code><\/pre>\n\n\n\n<p>The Navigation component handles the URL parsing and argument extraction for you. When the activity receives an App Link intent, the NavHost matches the URI pattern and navigates to the correct composable. You still need the intent filter in your manifest; the <code>navDeepLink<\/code> declaration only handles routing within Compose, not the system-level registration.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Extracting Query Parameters<\/h3>\n\n\n\n<p>Deep links often carry data as query parameters. Here is how to extract them:<\/p>\n\n\n\n<pre><code class=\"language-kotlin\">private fun handleDeepLink(intent: Intent) {\n    val uri = intent.data ?: return\n\n    \/\/ For a URL like: https:\/\/yourdomain.com\/referral?code=ABC123&amp;source=email\n    val referralCode = uri.getQueryParameter(&quot;code&quot;)      \/\/ &quot;ABC123&quot;\n    val source = uri.getQueryParameter(&quot;source&quot;)           \/\/ &quot;email&quot;\n    val allParams = uri.queryParameterNames                \/\/ Set&lt;String&gt;\n\n    \/\/ Use the extracted data\n    if (referralCode != null) {\n        applyReferralCode(referralCode, source)\n    }\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">App Links Verification Details<\/h2>\n\n\n\n<p>Understanding how Android verifies App Links helps you avoid common pitfalls.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Verification Timing<\/h3>\n\n\n\n<p>On Android 11 and earlier, verification happens once: when the app is installed or updated. If your <code>assetlinks.json<\/code> file is not ready at install time, verification fails and stays failed until the user reinstalls the app.<\/p>\n\n\n\n<p>On Android 12+, the system uses a background <a href=\"https:\/\/developer.android.com\/training\/app-links\/verify-android-applinks#request-verify\" rel=\"nofollow noopener\" target=\"_blank\">domain verification process<\/a> that can re-check domains. You can also manually trigger re-verification:<\/p>\n\n\n\n<pre><code class=\"language-bash\"># Reset verification state (Android 12+)\nadb shell pm set-app-links --package com.example.myapp 0 all\n\n# Trigger re-verification\nadb shell pm verify-app-links --re-verify com.example.myapp\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Subdomain Handling<\/h3>\n\n\n\n<p>Each subdomain is treated as a separate domain. If your manifest declares <code>app.yourdomain.com<\/code>, Android will check <code>https:\/\/app.yourdomain.com\/.well-known\/assetlinks.json<\/code>. It will not fall back to <code>yourdomain.com<\/code>. You must host the file on the exact subdomain listed in your intent filter.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Server Requirements<\/h3>\n\n\n\n<p>Your server must meet these requirements for the <code>assetlinks.json<\/code> file:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Served over HTTPS with a valid certificate<\/li>\n<li>No redirects (a <code>301<\/code> or <code>302<\/code> to another URL will cause verification to fail)<\/li>\n<li><code>Content-Type: application\/json<\/code><\/li>\n<li>HTTP status <code>200<\/code><\/li>\n<li>File size under 1 MB (keep it small; the file is fetched on every verification)<\/li>\n<li>Accessible from Google&#39;s servers (not blocked by robots.txt, geo-restrictions, or firewalls)<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Common Problems and Solutions<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Verification Fails Silently<\/h3>\n\n\n\n<p>Verification failures do not produce user-visible errors. The app simply does not open for App Links. Check the device logs:<\/p>\n\n\n\n<pre><code class=\"language-bash\">adb shell dumpsys package d | grep -i &quot;yourdomain.com&quot;\n<\/code><\/pre>\n\n\n\n<p>On Android 12+, use the more specific command:<\/p>\n\n\n\n<pre><code class=\"language-bash\">adb shell pm get-app-links com.example.myapp\n<\/code><\/pre>\n\n\n\n<p>This outputs the verification status for each domain. Look for <code>verified<\/code> (success) or <code>none<\/code> (failure).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Wrong Certificate Fingerprint<\/h3>\n\n\n\n<p>The most common cause of verification failure. If you use Google Play App Signing, the fingerprint in your <code>assetlinks.json<\/code> must match the Play signing certificate, not your upload key. Grab the correct fingerprint from the Play Console.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Redirects on assetlinks.json<\/h3>\n\n\n\n<p>Some hosting providers redirect <code>http<\/code> to <code>https<\/code> or add <code>www.<\/code> prefixes. The <code>assetlinks.json<\/code> URL itself must not redirect. If <code>https:\/\/yourdomain.com\/.well-known\/assetlinks.json<\/code> redirects to <code>https:\/\/www.yourdomain.com\/.well-known\/assetlinks.json<\/code>, verification fails.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">CDN or Caching Issues<\/h3>\n\n\n\n<p>If you use a CDN, make sure it does not transform the JSON response, add HTML wrappers, or serve a cached error page. Test the raw response:<\/p>\n\n\n\n<pre><code class=\"language-bash\">curl -v https:\/\/yourdomain.com\/.well-known\/assetlinks.json\n<\/code><\/pre>\n\n\n\n<p>Confirm the response is valid JSON with the correct <code>Content-Type<\/code> header.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Multiple Apps Sharing a Domain<\/h3>\n\n\n\n<p>You can list multiple apps in the same <code>assetlinks.json<\/code> file. Each app gets its own object in the array:<\/p>\n\n\n\n<pre><code class=\"language-json\">[\n  {\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.example.myapp&quot;,\n      &quot;sha256_cert_fingerprints&quot;: [&quot;AA:BB:CC:...&quot;]\n    }\n  },\n  {\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.example.myapp.debug&quot;,\n      &quot;sha256_cert_fingerprints&quot;: [&quot;DD:EE:FF:...&quot;]\n    }\n  }\n]\n<\/code><\/pre>\n\n\n\n<p>For more help with App Links issues, the <a href=\"https:\/\/tolinku.com\/docs\/troubleshooting\/android\/\">Tolinku Android troubleshooting guide<\/a> covers additional edge cases.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Testing App Links<\/h2>\n\n\n\n<p>Thorough testing prevents surprises in production. For a complete testing reference, see <a href=\"https:\/\/tolinku.com\/blog\/testing-android-app-links\/\">testing Android App Links<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Using adb<\/h3>\n\n\n\n<p>The fastest way to test App Links locally is with <code>adb<\/code>:<\/p>\n\n\n\n<pre><code class=\"language-bash\"># Test a specific URL\nadb shell am start -a android.intent.action.VIEW \\\n    -c android.intent.category.BROWSABLE \\\n    -d &quot;https:\/\/yourdomain.com\/product\/123&quot; \\\n    com.example.myapp\n<\/code><\/pre>\n\n\n\n<p>If the app opens and navigates to the product screen, your intent filter and deep link handling are working.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Checking Verification Status<\/h3>\n\n\n\n<pre><code class=\"language-bash\"># Android 12+ (API 31+)\nadb shell pm get-app-links com.example.myapp\n\n# Older Android versions\nadb shell dumpsys package com.example.myapp | grep -A 5 &quot;App Links&quot;\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Google Digital Asset Links Validator<\/h3>\n\n\n\n<p>Google provides an <a href=\"https:\/\/developers.google.com\/digital-asset-links\/tools\/generator\" rel=\"nofollow noopener\" target=\"_blank\">online validator<\/a> for your <code>assetlinks.json<\/code> file. Enter your domain and package name, and the tool checks whether your file is correctly formatted and accessible.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Real Device Testing<\/h3>\n\n\n\n<p>Emulator testing has limitations. Some verification behaviors differ on real devices, especially around Play Store signing. Always test on a physical device with a release build before shipping.<\/p>\n\n\n\n<p>Steps for a production-like test:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Build a signed release APK or App Bundle.<\/li>\n<li>Install it on a device: <code>adb install app-release.apk<\/code>.<\/li>\n<li>Wait 20-30 seconds for verification to complete.<\/li>\n<li>Open a browser and tap a link to your domain.<\/li>\n<li>Confirm the app opens without a disambiguation dialog.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">App Links with Tolinku<\/h2>\n\n\n\n<p>Hosting and maintaining the <code>assetlinks.json<\/code> file is straightforward for a single app, but it gets complicated when you manage multiple domains, staging environments, or frequently rotating signing keys.<\/p>\n\n\n\n<p><a href=\"https:\/\/tolinku.com\/features\/deep-linking\">Tolinku<\/a> handles the <code>assetlinks.json<\/code> hosting for you. When you configure an Appspace with your Android app details (package name and SHA-256 fingerprints), Tolinku automatically generates and serves the Digital Asset Links file on your configured domain. Every <a href=\"https:\/\/tolinku.com\/docs\/user-guide\/routes\/\">route<\/a> you create in Tolinku becomes a working App Link without manual server configuration.<\/p>\n\n\n\n<p>This is especially useful when you need to support multiple environments. Your debug build, staging build, and production build can all have their fingerprints registered in one place. Tolinku&#39;s platform serves the combined <code>assetlinks.json<\/code> with all the correct entries.<\/p>\n\n\n\n<p>For the full setup walkthrough, see the <a href=\"https:\/\/tolinku.com\/docs\/developer\/app-links\/\">App Links developer guide<\/a> in the Tolinku documentation.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Best Practices<\/h2>\n\n\n\n<p><strong>Always include both <code>www<\/code> and non-<code>www<\/code> domains.<\/strong> Users might encounter either version of your URL. Declare both in your manifest and host <code>assetlinks.json<\/code> on both.<\/p>\n\n\n\n<p><strong>Use <code>pathPrefix<\/code> over <code>pathPattern<\/code> when possible.<\/strong> Path patterns use a custom regex syntax that behaves differently from standard regex. Prefix matching is simpler and less error-prone.<\/p>\n\n\n\n<p><strong>Keep your <code>assetlinks.json<\/code> file small.<\/strong> Only include the apps that actually need to handle links on that domain. Bloated files with dozens of entries can slow verification.<\/p>\n\n\n\n<p><strong>Monitor verification status after releases.<\/strong> If your signing key changes or your CDN configuration shifts, App Links can break silently. Add a health check that periodically fetches your <code>assetlinks.json<\/code> and validates the contents.<\/p>\n\n\n\n<p><strong>Handle fallback gracefully.<\/strong> Even with App Links configured, some users will have link handling disabled in their device settings. Your website should still provide a good experience for those users, including a banner or redirect to the Play Store.<\/p>\n\n\n\n<p><strong>Support both <code>http<\/code> and <code>https<\/code> in your intent filter.<\/strong> While <code>assetlinks.json<\/code> must be served over HTTPS, some links in the wild may use <code>http<\/code>. Including both schemes in your manifest ensures your app catches those URLs too (Android will verify using HTTPS regardless).<\/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; \/&gt;\n    &lt;data android:scheme=&quot;http&quot; \/&gt;\n    &lt;data android:host=&quot;yourdomain.com&quot; \/&gt;\n    &lt;data android:pathPrefix=&quot;\/product&quot; \/&gt;\n&lt;\/intent-filter&gt;\n<\/code><\/pre>\n\n\n\n<p><strong>Test on multiple Android versions.<\/strong> Verification behavior changed significantly in Android 12. If your minimum SDK target is below API 31, test on both older and newer devices.<\/p>\n\n\n\n<p><strong>Version your deep link paths.<\/strong> If you restructure your URL patterns, old links will break. Consider keeping old path prefixes active or setting up server-side redirects for deprecated patterns.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>Android App Links give you a direct path from a URL tap to your app&#39;s content. The setup has a few moving parts (manifest declarations, the Digital Asset Links file, certificate fingerprints, verification timing) but each piece is straightforward on its own.<\/p>\n\n\n\n<p>The most important things to get right: host your <code>assetlinks.json<\/code> without redirects, use the correct signing key fingerprint (especially with Play App Signing), and test on real devices before release. Once verification passes, your users get a fast, direct experience with no extra taps or choices.<\/p>\n\n\n\n<p>If you want to skip the manual <code>assetlinks.json<\/code> management and get working App Links alongside <a href=\"https:\/\/tolinku.com\/features\/deep-linking\">deep linking features<\/a> like deferred deep links, analytics, and smart banners, <a href=\"https:\/\/tolinku.com\">Tolinku<\/a> handles the server-side configuration for you.<\/p>\n\n\n\n<p>For further reading, check the official <a href=\"https:\/\/developer.android.com\/training\/app-links\" rel=\"nofollow noopener\" target=\"_blank\">Android App Links documentation<\/a> and the <a href=\"https:\/\/developers.google.com\/digital-asset-links\/v1\/getting-started\" rel=\"nofollow noopener\" target=\"_blank\">Digital Asset Links specification<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The definitive guide to Android App Links. Learn Digital Asset Links, intent filters, verification, and deep link handling for Android apps.<\/p>\n","protected":false},"author":2,"featured_media":366,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Android App Links: Complete Implementation Guide for 2026","rank_math_description":"The definitive guide to Android App Links. Learn Digital Asset Links, intent filters, verification, and deep link handling for Android apps.","rank_math_focus_keyword":"android app links","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-links-complete-guide.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-links-complete-guide.png","footnotes":""},"categories":[10],"tags":[25,23,20,35,34,30,33],"class_list":["post-367","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-android","tag-android","tag-app-links","tag-deep-linking","tag-digital-asset-links","tag-kotlin","tag-sdks","tag-user-experience"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/367","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=367"}],"version-history":[{"count":4,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/367\/revisions"}],"predecessor-version":[{"id":2762,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/367\/revisions\/2762"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/366"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=367"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=367"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=367"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}