Every Android deep link starts with an intent filter in AndroidManifest.xml. 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't.
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 Android App Links Complete Guide.
The Basic Structure
An intent filter for deep links lives inside an <activity> element in your AndroidManifest.xml. Here is the minimal configuration for an App Link (a verified https link):
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="example.com" />
</intent-filter>
</activity>
Each element in this filter has a specific role:
<action android:name="android.intent.action.VIEW" />: Required. Tells Android this activity can display content.<category android:name="android.intent.category.DEFAULT" />: Required. Makes the activity reachable via implicit intents.<category android:name="android.intent.category.BROWSABLE" />: Required for links opened from a browser or other apps. Without this, a tap on a URL in Chrome will never reach your app.<data>: Defines which URLs this filter matches.android:autoVerify="true": On the<intent-filter>element itself, not on<data>. This triggers the App Links verification process, which is what turns a regular deep link into a verified App Link that skips the disambiguation dialog.
The android:exported="true" 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.
The <data> Element Attributes
The <data> element is where you specify what URLs match. It supports several attributes that combine to describe a URL pattern.
android:scheme
The URL scheme. For App Links, use "https". For regular deep links, you might use a custom scheme like "myapp". You can also use "http", though App Links verification only applies to https schemes.
<data android:scheme="https" />
android:host
The hostname the URL must match. This can be an exact hostname or use a wildcard prefix.
<!-- Exact match -->
<data android:host="example.com" />
<!-- Match any subdomain of example.com -->
<data android:host="*.example.com" />
The wildcard *.example.com matches www.example.com, app.example.com, and any other single-level subdomain, but it does not match example.com itself. If you want both the root domain and subdomains, you need separate <data> elements or separate intent filters.
Important for App Links: When you use a wildcard host with autoVerify="true", Android attempts verification for each specific subdomain it encounters in links, not for the wildcard pattern itself. You need the assetlinks.json file on each subdomain you want to handle. This can get complicated; most teams find it simpler to use explicit hostnames.
android:port
Optional. If your server runs on a non-standard port during development, you can specify it:
<data android:scheme="https" android:host="localhost" android:port="8443" />
For production App Links, you will not normally use this attribute.
android:path, android:pathPrefix, and android:pathPattern
These attributes restrict which paths within the host the filter matches.
android:path matches one exact path:
<data android:path="/invite" />
This matches https://example.com/invite but not https://example.com/invite/abc123.
android:pathPrefix matches any path that starts with the given string:
<data android:pathPrefix="/products" />
This matches /products, /products/123, /products/abc/details, and so on.
android:pathPattern matches paths using a simplified pattern syntax. The . character matches any single character, and .* matches any sequence of characters (including an empty sequence):
<!-- Match /user/ followed by any characters -->
<data android:pathPattern="/user/.*" />
<!-- Match /item/ followed by exactly one character -->
<data android:pathPattern="/item/." />
Note that this pattern syntax is not the same as standard regex. The only special characters are . (any single character) and * (zero or more of the preceding character). To match a literal dot, escape it with a double backslash: \\..
android:pathAdvancedPattern (added in API 31) supports standard regex syntax for more precise matching:
<data android:pathAdvancedPattern="/products/[0-9]+" />
If your minSdkVersion is below 31, you cannot rely on this attribute alone. You would need a fallback with pathPattern or handle path validation in code.
android:mimeType
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.
How Android Evaluates Multiple Attributes
When you put multiple attributes on a single <data> element, they all apply together (AND logic). The URL must match every attribute that is specified.
When you have multiple <data> elements inside a single <intent-filter>, Android pools all of the values across all <data> elements within that filter (OR-like pooling). This is a common source of confusion.
Consider this filter:
<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="example.com" />
<data android:scheme="https" android:host="www.example.com" />
</intent-filter>
This filter matches both https://example.com/... and https://www.example.com/.... The two <data> elements each contribute their host value to the pool.
But the pooling behavior can also cause unintended matches. If you write:
<intent-filter>
<data android:scheme="https" android:host="example.com" />
<data android:scheme="myapp" />
</intent-filter>
Android pools scheme values (https, myapp) and host values (example.com). The result is that this filter matches both https://example.com/... AND myapp://example.com/.... It also potentially matches https:// and myapp:// without any host restriction (because the second <data> element contributes no host). This is almost certainly not what you intended.
Best practice: Use separate <intent-filter> elements for different schemes. Do not mix schemes in a single filter.
Separate Filters for Different URL Patterns
You can add multiple <intent-filter> elements to the same activity. This is the correct way to handle different URL structures cleanly:
<activity
android:name=".MainActivity"
android:exported="true">
<!-- App Links for the main domain -->
<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="example.com"
android:pathPrefix="/app" />
</intent-filter>
<!-- Custom scheme for legacy compatibility -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" />
</intent-filter>
</activity>
When autoVerify="true" 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.
Routing to Different Activities
You are not limited to a single activity. Different URL patterns can route to different activities by placing intent filters on each:
<!-- Main activity for general links -->
<activity android:name=".MainActivity" android:exported="true">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="example.com" />
</intent-filter>
</activity>
<!-- Product activity for product pages -->
<activity android:name=".ProductActivity" android:exported="true">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="example.com"
android:pathPrefix="/products" />
</intent-filter>
</activity>
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, https://example.com/products/123 matches both filters, but Android chooses ProductActivity because its path prefix makes it the more specific match.
If two filters have equal specificity across different apps, Android shows the disambiguation dialog (unless one of the apps has verified App Links status).
The autoVerify Attribute and Partial Verification
android:autoVerify="true" belongs on the <intent-filter> element, and it triggers verification for every HTTPS domain listed in that filter's <data> elements.
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.
See the Tolinku App Links developer documentation for details on how Tolinku handles multi-domain configurations and the assetlinks.json setup that goes alongside your manifest configuration.
Reading the Intent in Your Activity
Once Android routes a URL to your activity, you read the incoming URL from the intent:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = intent
val action = intent.action
val data = intent.data
if (Intent.ACTION_VIEW == action && data != null) {
val path = data.path // e.g. "/products/123"
val productId = data.lastPathSegment // e.g. "123"
val queryParam = data.getQueryParameter("ref")
// Navigate to the appropriate screen
handleDeepLink(path, productId, queryParam)
}
}
The Intent.getData() method returns a Uri object. The Uri class provides methods to extract each component of the URL: getScheme(), getHost(), getPath(), getPathSegments(), getQueryParameter(), and others. You do not need to parse the URL string manually. For more on handling deep link data in Kotlin, see Kotlin Deep Link Handling: Patterns and Best Practices.
For links that arrive while the app is already running in the background, also override onNewIntent():
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
// Handle the new deep link intent
intent.data?.let { handleDeepLink(it) }
}
Testing Your Configuration
After configuring your intent filters, verify the setup with adb before testing manually:
adb shell am start -a android.intent.action.VIEW \
-d "https://example.com/products/123" \
com.example.myapp
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.
For App Links specifically, also check verification status:
adb shell pm get-app-links --package com.example.myapp
For a complete testing walkthrough, see the Tolinku Android troubleshooting guide and the testing Android App Links guide.
Summary
Intent filters are the first step in any Android deep link implementation. Use android.intent.action.VIEW, both the DEFAULT and BROWSABLE categories, and the <data> element to specify your URL patterns. For App Links, add android:autoVerify="true" 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 Intent.getData() in your activity's onCreate() and onNewIntent() methods. For a detailed walkthrough of the full manifest setup, see Android Manifest Deep Link Configuration.
Get deep linking tips in your inbox
One email per week. No spam.