When a user taps a link, the operating system decides what happens in milliseconds. Does the link open the browser? An app? A disambiguation dialog? The decision depends on the link type, domain verification status, user preferences, and the context of the tap. This guide explains exactly how iOS and Android make that decision.
For the full deep link lifecycle, see the deep linking lifecycle: from click to content. For the technical overview, see how deep linking works: a technical overview.
iOS Link Resolution
Decision Flow
When a user taps an HTTPS link on iOS:
User taps HTTPS link
↓
Is the tap from an in-app browser (WebView)?
├─ Yes → Open in WebView (Universal Links do not activate)
└─ No ↓
Has the user previously chosen "Open in Safari" for this domain?
├─ Yes → Open in Safari
└─ No ↓
Is there a cached AASA for this domain?
├─ No → Open in Safari
└─ Yes ↓
Does the URL path match an entry in the AASA?
├─ No → Open in Safari
└─ Yes ↓
Is the associated app installed?
├─ No → Open in Safari
└─ Yes → Launch the app with the URL
AASA Path Matching
iOS matches the URL path against the AASA's components (modern format) or paths (legacy format):
{
"applinks": {
"details": [
{
"appIDs": ["TEAMID.com.example.app"],
"components": [
{ "/": "/products/*" },
{ "/": "/offers/*" },
{ "/": "/admin/*", "exclude": true }
]
}
]
}
}
Matching rules:
*matches any substring within a path segment.?matches a single character.- Patterns are evaluated in order; first match wins.
exclude: trueprevents a match, even if a previous pattern would match.- Matching is case-sensitive by default (set
caseSensitive: falseto change).
Example matches:
| URL | Pattern | Matches? |
|---|---|---|
/products/abc |
/products/* |
Yes |
/products/abc/reviews |
/products/* |
No (only one level) |
/products/abc/reviews |
/products/*/reviews |
Yes |
/admin/settings |
/admin/* (excluded) |
No (excluded) |
/offers/summer |
/offers/* |
Yes |
/unknown/path |
(no match) | No |
User Override
iOS allows users to override Universal Links:
- When a Universal Link is active, long-pressing the link shows "Open in Safari" and "Open in [App]."
- If the user chooses "Open in Safari," iOS remembers this preference for the domain.
- Subsequent taps on links from that domain open in Safari instead of the app.
- The user can reverse this by long-pressing again and choosing "Open in [App]."
This preference is stored per-domain and persists until the user changes it or the app is reinstalled.
In-App Browser Behavior
Universal Links do not activate inside WebViews (in-app browsers). This affects:
| App | Opens links in | Universal Links? |
|---|---|---|
| Safari | System browser | Yes |
| Chrome (iOS) | System browser | Yes |
| Messages | System browser | Yes |
| Depends on settings | Usually yes | |
| Gmail | In-app browser | No |
| In-app browser | No | |
| In-app browser | No | |
| Twitter/X | In-app browser | No |
| In-app browser | No |
For in-app browsers, use a bounce page strategy (see common deep linking challenges).
Android Link Resolution
Decision Flow
When a user taps an HTTPS link on Android:
User taps HTTPS link
↓
Is there an app with a verified App Link for this domain?
├─ Yes → Launch the app with the URL
└─ No ↓
Is there an app with an unverified intent filter for this URL?
├─ Yes → Show disambiguation dialog
│ "Open with: Chrome / [App]"
└─ No → Open in Chrome (or default browser)
Intent Filter Matching
Android matches URLs against Intent filters declared in the app's manifest:
<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="app.example.com"
android:pathPattern="/products/.*" />
</intent-filter>
Matching rules:
scheme,host, andpathPatternmust all match.pathPatternuses regex-like syntax:.*matches any characters.pathPrefixmatches the beginning of the path.pathmatches the exact path.- Multiple
<data>elements are OR-ed together.
Verification (autoVerify="true"):
- If present and verification succeeds, the app opens automatically (no dialog).
- If absent or verification fails, Android shows a disambiguation dialog.
- On Android 12+, unverified links may open in the browser by default instead of showing a dialog.
Verified vs Unverified
| State | autoVerify | assetlinks.json | Behavior |
|---|---|---|---|
| Verified | true | Valid | App opens automatically |
| Unverified | true | Invalid/missing | Browser opens (Android 12+) or dialog |
| No verification | false | N/A | Disambiguation dialog |
| No intent filter | N/A | N/A | Browser opens |
Multiple Apps
If multiple apps declare intent filters for the same URL:
- Both verified: Should not happen (only one app should be verified per domain).
- One verified, one not: Verified app wins.
- Neither verified: Disambiguation dialog shows both apps and the browser.
Default App Settings
Users can set default apps for specific links in Android Settings > Apps > Default apps > Opening links. If a user sets Chrome as the default for a domain, your app will not open even if it has a verified App Link.
Custom URL Scheme Resolution
iOS
User taps myapp://path
↓
Is "myapp" registered by any installed app?
├─ No → Error: "Safari cannot open the page"
└─ Yes ↓
Is there only one app registered for "myapp"?
├─ Yes → Launch that app
└─ No → Undefined behavior (one of them opens)
Android
User taps myapp://path
↓
Is there an intent filter for "myapp" scheme?
├─ No → Error: "No app found"
└─ Yes ↓
Is there only one app?
├─ Yes → Launch that app
└─ No → Disambiguation dialog
Custom URL schemes have no verification mechanism, so any app can claim any scheme. This is why Universal Links and App Links are preferred.
Resolution Priority
When multiple deep link mechanisms are available, they have a priority order:
iOS Priority
- Universal Links (highest priority when active)
- Custom URL schemes (if Universal Link not available)
- Web fallback (if neither works)
Android Priority
- Verified App Links (highest priority, no dialog)
- Unverified intent filters (disambiguation dialog)
- Custom URL schemes (if not an HTTPS link)
- Web fallback (default browser)
Edge Cases
Same-Domain Navigation
On iOS, Universal Links do not activate when navigating within the same domain. If the user is on https://app.example.com/page1 and taps a link to https://app.example.com/products/123, the navigation stays in the browser.
Universal Links only activate when the user taps a link from a different domain.
JavaScript Navigation
Programmatic navigation via JavaScript (window.location.href = '...') does not trigger Universal Links on iOS. Only user-initiated taps trigger them.
On Android, window.location.href to an App Link URL may or may not trigger the app, depending on the browser and timing. It is unreliable.
Redirect Chains
User taps: https://tracking.com/click?url=https://app.example.com/products/123
↓
Browser loads tracking.com (not your domain, Universal Links do not activate)
↓
tracking.com redirects to app.example.com/products/123
↓
iOS: Safari loads the page (too late for Universal Links)
Android: Chrome may activate App Links on the redirect target (sometimes)
This is the link wrapping problem. See link wrapping and redirects: impact on deep links for solutions.
App Clip / Instant App
On iOS 14+, if an App Clip is configured for the domain, it may open instead of the full app if the full app is not installed. On Android, Instant Apps can provide a similar experience.
Debugging Resolution
iOS
# Check AASA from Apple's CDN
curl -s "https://app-site-association.cdn-apple.com/a/v1/yourdomain.com" | python3 -m json.tool
# Check local AASA cache (on-device)
# Settings > Developer > Universal Links > Diagnostics
Android
# Check verification status
adb shell pm get-app-links com.example.app
# Status should be "verified"
# Force re-verification
adb shell pm verify-app-links --re-verify com.example.app
# Check which app handles a URL
adb shell pm resolve-activity -a android.intent.action.VIEW \
-d "https://app.example.com/products/123" -c android.intent.category.BROWSABLE
Tolinku for Link Resolution
Tolinku handles link resolution by hosting AASA and assetlinks.json files, managing domain verification, and providing fallback pages. See the Universal Links documentation for iOS resolution details.
For the lifecycle, see the deep linking lifecycle: from click to content. For the full guide, see the complete guide to deep linking in 2026.
Get deep linking tips in your inbox
One email per week. No spam.