{"id":544,"date":"2026-03-19T13:00:00","date_gmt":"2026-03-19T18:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=544"},"modified":"2026-03-07T04:45:40","modified_gmt":"2026-03-07T09:45:40","slug":"android-intent-filters-deep-links","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/android-intent-filters-deep-links\/","title":{"rendered":"Android Intent Filters for Deep Links: Configuration Guide"},"content":{"rendered":"\n<p>Every Android deep link starts with an intent filter in <code>AndroidManifest.xml<\/code>. This XML configuration tells Android which URLs your activity can handle. Get it right, and your app intercepts the correct links. Get it wrong, and you end up either missing links you want to handle or claiming links you shouldn&#39;t.<\/p>\n\n\n\n<p>This guide covers the full intent filter configuration for deep links and Android App Links: what each element and attribute does, how path matching patterns work, how to set up multiple filters for different URL structures, and how Android decides which app handles a given URL when multiple candidates exist. For a broader overview of the App Links system, see the <a href=\"https:\/\/tolinku.com\/blog\/android-app-links-complete-guide\/\">Android App Links Complete Guide<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Basic Structure<\/h2>\n\n\n\n<p>An intent filter for deep links lives inside an <code>&lt;activity&gt;<\/code> element in your <code>AndroidManifest.xml<\/code>. Here is the minimal configuration for an App Link (a verified <code>https<\/code> link):<\/p>\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;example.com&quot; \/&gt;\n    &lt;\/intent-filter&gt;\n\n&lt;\/activity&gt;\n<\/code><\/pre>\n\n\n\n<p>Each element in this filter has a specific role:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>&lt;action android:name=&quot;android.intent.action.VIEW&quot; \/&gt;<\/code>: Required. Tells Android this activity can display content.<\/li>\n<li><code>&lt;category android:name=&quot;android.intent.category.DEFAULT&quot; \/&gt;<\/code>: Required. Makes the activity reachable via implicit intents.<\/li>\n<li><code>&lt;category android:name=&quot;android.intent.category.BROWSABLE&quot; \/&gt;<\/code>: Required for links opened from a browser or other apps. Without this, a tap on a URL in Chrome will never reach your app.<\/li>\n<li><code>&lt;data&gt;<\/code>: Defines which URLs this filter matches.<\/li>\n<li><code>android:autoVerify=&quot;true&quot;<\/code>: On the <code>&lt;intent-filter&gt;<\/code> element itself, not on <code>&lt;data&gt;<\/code>. This triggers the <a href=\"https:\/\/developer.android.com\/training\/app-links\/verify-android-applinks\" rel=\"nofollow noopener\" target=\"_blank\">App Links verification process<\/a>, which is what turns a regular deep link into a verified App Link that skips the disambiguation dialog.<\/li>\n<\/ul>\n\n\n\n<p>The <code>android:exported=&quot;true&quot;<\/code> attribute on the activity is required on Android 12 (API 31) and later for any activity that declares an intent filter. Android will refuse to install apps that omit this on API 31+ targets.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The <code>&lt;data&gt;<\/code> Element Attributes<\/h2>\n\n\n\n<p>The <code>&lt;data&gt;<\/code> element is where you specify what URLs match. It supports several attributes that combine to describe a URL pattern.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>android:scheme<\/code><\/h3>\n\n\n\n<p>The URL scheme. For App Links, use <code>&quot;https&quot;<\/code>. For regular deep links, you might use a custom scheme like <code>&quot;myapp&quot;<\/code>. You can also use <code>&quot;http&quot;<\/code>, though App Links verification only applies to <code>https<\/code> schemes.<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;data android:scheme=&quot;https&quot; \/&gt;\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><code>android:host<\/code><\/h3>\n\n\n\n<p>The hostname the URL must match. This can be an exact hostname or use a wildcard prefix.<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;!-- Exact match --&gt;\n&lt;data android:host=&quot;example.com&quot; \/&gt;\n\n&lt;!-- Match any subdomain of example.com --&gt;\n&lt;data android:host=&quot;*.example.com&quot; \/&gt;\n<\/code><\/pre>\n\n\n\n<p>The wildcard <code>*.example.com<\/code> matches <code>www.example.com<\/code>, <code>app.example.com<\/code>, and any other single-level subdomain, but it does not match <code>example.com<\/code> itself. If you want both the root domain and subdomains, you need separate <code>&lt;data&gt;<\/code> elements or separate intent filters.<\/p>\n\n\n\n<p><strong>Important for App Links:<\/strong> When you use a wildcard host with <code>autoVerify=&quot;true&quot;<\/code>, Android attempts verification for each specific subdomain it encounters in links, not for the wildcard pattern itself. You need the <code>assetlinks.json<\/code> file on each subdomain you want to handle. This can get complicated; most teams find it simpler to use explicit hostnames.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>android:port<\/code><\/h3>\n\n\n\n<p>Optional. If your server runs on a non-standard port during development, you can specify it:<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;data android:scheme=&quot;https&quot; android:host=&quot;localhost&quot; android:port=&quot;8443&quot; \/&gt;\n<\/code><\/pre>\n\n\n\n<p>For production App Links, you will not normally use this attribute.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>android:path<\/code>, <code>android:pathPrefix<\/code>, and <code>android:pathPattern<\/code><\/h3>\n\n\n\n<p>These attributes restrict which paths within the host the filter matches.<\/p>\n\n\n\n<p><strong><code>android:path<\/code><\/strong> matches one exact path:<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;data android:path=&quot;\/invite&quot; \/&gt;\n<\/code><\/pre>\n\n\n\n<p>This matches <code>https:\/\/example.com\/invite<\/code> but not <code>https:\/\/example.com\/invite\/abc123<\/code>.<\/p>\n\n\n\n<p><strong><code>android:pathPrefix<\/code><\/strong> matches any path that starts with the given string:<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;data android:pathPrefix=&quot;\/products&quot; \/&gt;\n<\/code><\/pre>\n\n\n\n<p>This matches <code>\/products<\/code>, <code>\/products\/123<\/code>, <code>\/products\/abc\/details<\/code>, and so on.<\/p>\n\n\n\n<p><strong><code>android:pathPattern<\/code><\/strong> matches paths using a simplified pattern syntax. The <code>.<\/code> character matches any single character, and <code>.*<\/code> matches any sequence of characters (including an empty sequence):<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;!-- Match \/user\/ followed by any characters --&gt;\n&lt;data android:pathPattern=&quot;\/user\/.*&quot; \/&gt;\n\n&lt;!-- Match \/item\/ followed by exactly one character --&gt;\n&lt;data android:pathPattern=&quot;\/item\/.&quot; \/&gt;\n<\/code><\/pre>\n\n\n\n<p>Note that this pattern syntax is not the same as standard regex. The only special characters are <code>.<\/code> (any single character) and <code>*<\/code> (zero or more of the preceding character). To match a literal dot, escape it with a double backslash: <code>\\\\.<\/code>.<\/p>\n\n\n\n<p><strong><code>android:pathAdvancedPattern<\/code><\/strong> (added in API 31) supports standard regex syntax for more precise matching:<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;data android:pathAdvancedPattern=&quot;\/products\/[0-9]+&quot; \/&gt;\n<\/code><\/pre>\n\n\n\n<p>If your <code>minSdkVersion<\/code> is below 31, you cannot rely on this attribute alone. You would need a fallback with <code>pathPattern<\/code> or handle path validation in code.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>android:mimeType<\/code><\/h3>\n\n\n\n<p>You can also match by MIME type. This is more relevant for content sharing than for deep linking, but it is available if your use case requires it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">How Android Evaluates Multiple Attributes<\/h2>\n\n\n\n<p>When you put multiple attributes on a single <code>&lt;data&gt;<\/code> element, they all apply together (AND logic). The URL must match every attribute that is specified.<\/p>\n\n\n\n<p>When you have multiple <code>&lt;data&gt;<\/code> elements inside a single <code>&lt;intent-filter&gt;<\/code>, Android pools all of the values across all <code>&lt;data&gt;<\/code> elements within that filter (OR-like pooling). This is a common source of confusion.<\/p>\n\n\n\n<p>Consider this filter:<\/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;example.com&quot; \/&gt;\n    &lt;data android:scheme=&quot;https&quot; android:host=&quot;www.example.com&quot; \/&gt;\n&lt;\/intent-filter&gt;\n<\/code><\/pre>\n\n\n\n<p>This filter matches both <code>https:\/\/example.com\/...<\/code> and <code>https:\/\/www.example.com\/...<\/code>. The two <code>&lt;data&gt;<\/code> elements each contribute their <code>host<\/code> value to the pool.<\/p>\n\n\n\n<p>But the pooling behavior can also cause unintended matches. If you write:<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;intent-filter&gt;\n    &lt;data android:scheme=&quot;https&quot; android:host=&quot;example.com&quot; \/&gt;\n    &lt;data android:scheme=&quot;myapp&quot; \/&gt;\n&lt;\/intent-filter&gt;\n<\/code><\/pre>\n\n\n\n<p>Android pools <code>scheme<\/code> values (<code>https<\/code>, <code>myapp<\/code>) and <code>host<\/code> values (<code>example.com<\/code>). The result is that this filter matches both <code>https:\/\/example.com\/...<\/code> AND <code>myapp:\/\/example.com\/...<\/code>. It also potentially matches <code>https:\/\/<\/code> and <code>myapp:\/\/<\/code> without any host restriction (because the second <code>&lt;data&gt;<\/code> element contributes no host). This is almost certainly not what you intended.<\/p>\n\n\n\n<p><strong>Best practice:<\/strong> Use separate <code>&lt;intent-filter&gt;<\/code> elements for different schemes. Do not mix schemes in a single filter.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Separate Filters for Different URL Patterns<\/h2>\n\n\n\n<p>You can add multiple <code>&lt;intent-filter&gt;<\/code> elements to the same activity. This is the correct way to handle different URL structures cleanly:<\/p>\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;!-- App Links for the main domain --&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\n            android:scheme=&quot;https&quot;\n            android:host=&quot;example.com&quot;\n            android:pathPrefix=&quot;\/app&quot; \/&gt;\n    &lt;\/intent-filter&gt;\n\n    &lt;!-- Custom scheme for legacy compatibility --&gt;\n    &lt;intent-filter&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;myapp&quot; \/&gt;\n    &lt;\/intent-filter&gt;\n\n&lt;\/activity&gt;\n<\/code><\/pre>\n\n\n\n<p>When <code>autoVerify=&quot;true&quot;<\/code> is on a filter, it only triggers verification for the domains declared in that filter. The custom scheme filter has no effect on App Links verification.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Routing to Different Activities<\/h2>\n\n\n\n<p>You are not limited to a single activity. Different URL patterns can route to different activities by placing intent filters on each:<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;!-- Main activity for general links --&gt;\n&lt;activity android:name=&quot;.MainActivity&quot; android:exported=&quot;true&quot;&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;example.com&quot; \/&gt;\n    &lt;\/intent-filter&gt;\n&lt;\/activity&gt;\n\n&lt;!-- Product activity for product pages --&gt;\n&lt;activity android:name=&quot;.ProductActivity&quot; android:exported=&quot;true&quot;&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\n            android:scheme=&quot;https&quot;\n            android:host=&quot;example.com&quot;\n            android:pathPrefix=&quot;\/products&quot; \/&gt;\n    &lt;\/intent-filter&gt;\n&lt;\/activity&gt;\n<\/code><\/pre>\n\n\n\n<p>When a URL matches multiple filters across multiple activities, Android applies a specificity rule: more specific filters win. A filter with both a host and a path prefix is more specific than one with only a host. In the example above, <code>https:\/\/example.com\/products\/123<\/code> matches both filters, but Android chooses <code>ProductActivity<\/code> because its path prefix makes it the more specific match.<\/p>\n\n\n\n<p>If two filters have equal specificity across different apps, Android shows the disambiguation dialog (unless one of the apps has verified App Links status).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The autoVerify Attribute and Partial Verification<\/h2>\n\n\n\n<p><code>android:autoVerify=&quot;true&quot;<\/code> belongs on the <code>&lt;intent-filter&gt;<\/code> element, and it triggers verification for every HTTPS domain listed in that filter&#39;s <code>&lt;data&gt;<\/code> elements.<\/p>\n\n\n\n<p>If verification fails for any one domain in a filter, Android will not grant verified status for the domains in that filter (on Android 11 and earlier). On Android 12 and later, verification is per-domain: a failure on one domain does not affect the verified status of other domains that passed.<\/p>\n\n\n\n<p>See the <a href=\"https:\/\/tolinku.com\/docs\/developer\/app-links\/\">Tolinku App Links developer documentation<\/a> for details on how Tolinku handles multi-domain configurations and the <code>assetlinks.json<\/code> setup that goes alongside your manifest configuration.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Reading the Intent in Your Activity<\/h2>\n\n\n\n<p>Once Android routes a URL to your activity, you read the incoming URL from the intent:<\/p>\n\n\n\n<pre><code class=\"language-kotlin\">override fun onCreate(savedInstanceState: Bundle?) {\n    super.onCreate(savedInstanceState)\n\n    val intent = intent\n    val action = intent.action\n    val data = intent.data\n\n    if (Intent.ACTION_VIEW == action &amp;&amp; data != null) {\n        val path = data.path          \/\/ e.g. &quot;\/products\/123&quot;\n        val productId = data.lastPathSegment  \/\/ e.g. &quot;123&quot;\n        val queryParam = data.getQueryParameter(&quot;ref&quot;)\n\n        \/\/ Navigate to the appropriate screen\n        handleDeepLink(path, productId, queryParam)\n    }\n}\n<\/code><\/pre>\n\n\n\n<p>The <code>Intent.getData()<\/code> method returns a <code>Uri<\/code> object. The <code>Uri<\/code> class provides methods to extract each component of the URL: <code>getScheme()<\/code>, <code>getHost()<\/code>, <code>getPath()<\/code>, <code>getPathSegments()<\/code>, <code>getQueryParameter()<\/code>, and others. You do not need to parse the URL string manually. For more on handling deep link data in Kotlin, see <a href=\"https:\/\/tolinku.com\/blog\/kotlin-deep-link-handling\/\">Kotlin Deep Link Handling: Patterns and Best Practices<\/a>.<\/p>\n\n\n\n<p>For links that arrive while the app is already running in the background, also override <code>onNewIntent()<\/code>:<\/p>\n\n\n\n<pre><code class=\"language-kotlin\">override fun onNewIntent(intent: Intent) {\n    super.onNewIntent(intent)\n    \/\/ Handle the new deep link intent\n    intent.data?.let { handleDeepLink(it) }\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Testing Your Configuration<\/h2>\n\n\n\n<p>After configuring your intent filters, verify the setup with <code>adb<\/code> before testing manually:<\/p>\n\n\n\n<pre><code class=\"language-bash\">adb shell am start -a android.intent.action.VIEW \\\n  -d &quot;https:\/\/example.com\/products\/123&quot; \\\n  com.example.myapp\n<\/code><\/pre>\n\n\n\n<p>If the activity launches, the intent filter is configured correctly. If nothing happens, check the filter configuration and make sure the package name is correct.<\/p>\n\n\n\n<p>For App Links specifically, also check verification status:<\/p>\n\n\n\n<pre><code class=\"language-bash\">adb shell pm get-app-links --package com.example.myapp\n<\/code><\/pre>\n\n\n\n<p>For a complete testing walkthrough, see the <a href=\"https:\/\/tolinku.com\/docs\/troubleshooting\/android\/\">Tolinku Android troubleshooting guide<\/a> and the <a href=\"https:\/\/tolinku.com\/blog\/testing-android-app-links\/\">testing Android App Links guide<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Summary<\/h2>\n\n\n\n<p>Intent filters are the first step in any Android deep link implementation. Use <code>android.intent.action.VIEW<\/code>, both the <code>DEFAULT<\/code> and <code>BROWSABLE<\/code> categories, and the <code>&lt;data&gt;<\/code> element to specify your URL patterns. For App Links, add <code>android:autoVerify=&quot;true&quot;<\/code> to the intent filter element. Keep different schemes in separate filters to avoid unintended URL matches. Use multiple intent filters or multiple activities to route different URL patterns to different destinations. Once the manifest is configured, read the incoming URL from <code>Intent.getData()<\/code> in your activity&#39;s <code>onCreate()<\/code> and <code>onNewIntent()<\/code> methods. For a detailed walkthrough of the full manifest setup, see <a href=\"https:\/\/tolinku.com\/blog\/android-manifest-deep-link-configuration\/\">Android Manifest Deep Link Configuration<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Intent filters in AndroidManifest.xml control which URLs your app claims to handle. This guide covers the full configuration: data elements, categories, path matching patterns, multiple filters, and how Android resolves conflicts between them.<\/p>\n","protected":false},"author":2,"featured_media":543,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Android Intent Filters for Deep Links: Configuration Guide","rank_math_description":"Configure Android intent filters for deep links. Covers scheme, host, pathPrefix, pathPattern, categories, multiple filters, and App Links autoVerify.","rank_math_focus_keyword":"android intent filters deep 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-intent-filters-deep-links.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-intent-filters-deep-links.png","footnotes":""},"categories":[10],"tags":[25,23,20,34,33],"class_list":["post-544","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-android","tag-android","tag-app-links","tag-deep-linking","tag-kotlin","tag-user-experience"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/544","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=544"}],"version-history":[{"count":2,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/544\/revisions"}],"predecessor-version":[{"id":2804,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/544\/revisions\/2804"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/543"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=544"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=544"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=544"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}