universal links android
android app links
deep linking
android development
assetlinks.json

Mastering Universal Links Android: A Complete Guide

Mastering Universal Links Android: A Complete Guide

You're probably here because the Android side of deep linking looks deceptively simple. Add an intent filter. Drop an assetlinks.json file on your domain. Tap a link. Done.

Then reality hits. The link opens Chrome instead of your app. It works on one Pixel but not on a Samsung test device. A release build fails while debug worked yesterday. A marketing link opens inside an in-app browser and ignores everything you configured.

That gap between official docs and production behavior often results in significant time loss. If you're implementing universal links android, the first thing to know is that Android doesn't call them Universal Links. The secure HTTPS-based equivalent is Android App Links. The second thing to know is that a correct setup still needs a fallback plan, because the wild is messier than the spec.

Understanding Android App Links vs Universal Links

A real production bug often starts with a normal HTTPS link. Someone taps a product URL from email, ads, or chat, and Android opens Chrome instead of the app. The URL is correct. The intent filter looks correct. The app is installed. That is usually the moment teams realize Android App Links and Apple Universal Links solve the same business problem, but they do not behave the same way in practice.

On Android, the official term is App Links. On iOS, it is Universal Links. Both use standard HTTPS URLs and both tie app ownership to a domain-level verification file, which is what makes them safer than custom schemes. On Android, that trust relationship is handled through Digital Asset Links and an assetlinks.json file hosted at /.well-known/assetlinks.json on your domain. Google documents the verification model in the Android App Links overview.

Developers search for universal links android because they want one concept that works across both platforms. That search term is fine for discovery. It becomes a problem when teams copy iOS assumptions into Android implementation details, or expect Android verification and fallback behavior to be as predictable as Apple's.

Feature Android App Links iOS Universal Links
Platform term App Links Universal Links
Link format HTTPS URL HTTPS URL
Verification file assetlinks.json Apple association file
Trust model Digital Asset Links verification Domain association verification
Security goal Prevent unverified apps from claiming your URLs Prevent unverified apps from claiming your URLs
Fallback behavior Browser or web page if verification fails or the app is not selected Safari or web page if the app is not installed or the link is not handled
Typical setup location AndroidManifest.xml plus server file Xcode entitlements plus server file

The practical difference is reliability under messy conditions. Android App Links can work well, but they are more sensitive to device state, OEM behavior, browser context, and verification timing than many teams expect. If a link is opened from an in-app browser, a misconfigured host, or a device that has not completed verification yet, Android may send the user to the web even though your setup is technically close.

That is why I treat App Links as one layer in a routing system, not the whole system.

Verified HTTPS links still beat custom URI schemes for most production apps:

  • Security: Another app cannot acquire your public HTTPS URLs without domain verification.
  • Shared URLs: Product, support, marketing, and engineering can use one canonical link format.
  • Fallbacks: The same URL can still resolve to web content when app routing fails.
  • Cross-platform sanity: You can keep one public URL strategy even though Android and iOS wire it up differently.

Custom schemes like myapp://product/123 still have a place. They are useful in QA tools, internal app-to-app flows, and some third-party handoff scenarios. They are a poor primary strategy for user-facing links because they are easier to hijack and do not map as cleanly to web fallbacks.

If your app is cross-platform, this gets more interesting. A React Native codebase can share routing logic, but Android domain verification still lives in native config and server files. This React Native deep linking guide is a useful companion if your JavaScript navigation works but Android link ownership does not.

The hard-won rule is simple. Public links should be HTTPS. Your app should claim only the hosts it owns. Your website should always be able to handle the same URLs when Android decides not to open the app. That fallback is not a nice extra. It is part of a production-ready App Links setup.

Configuring Your App for Android App Links

Most App Links failures start in the manifest. Not because the XML is hard, but because tiny mismatches create silent fallbacks.

Start with the activity that should receive the incoming URL. In many apps that's MainActivity, but if your architecture uses a dedicated entry activity, put the filter there instead.

A hand drawing a schematic diagram of Android manifest files and universal links on a smartphone screen.

A sane manifest baseline

<activity
    android:name=".MainActivity"
    android:exported="true"
    android:launchMode="singleTask">

    <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>
</activity>

A few details matter more than they look:

  • android:autoVerify="true" tells Android to attempt domain verification.
  • BROWSABLE is required for links launched outside your app.
  • DEFAULT lets the activity resolve standard intents correctly.
  • Separate <data> entries are fine for each host you support.

If you need to match only part of your site, narrow the 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"
        android:pathPrefix="/products" />
</intent-filter>

What to include and what to avoid

Don't start by matching every URL on your domain unless you mean it. Broad filters look convenient, but they create routing problems later when your website grows.

Use this review checklist:

  • Single host first: Begin with one production domain, then add www or regional hosts only if you serve them.
  • Specific path prefixes: If only /products and /reset-password should open the app, declare those intentionally.
  • One clear owner activity: Don't scatter overlapping filters across multiple activities unless you enjoy debugging ambiguous intent resolution.
  • Release reality: If this app ships signed differently in staging and production, plan for each environment up front.

A lot of React Native teams hit the same Android-specific issues because the native config looks minor until it isn't. If your app straddles native and JS navigation, this guide on React Native deep linking is a good companion.

Handle the URL after launch

Getting the app to open is only half the job. The activity still needs to read the URI and route correctly.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    handleIntent(intent)
}

override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    if (intent != null) {
        handleIntent(intent)
    }
}

private fun handleIntent(intent: Intent) {
    val data: Uri? = intent.data
    if (data != null) {
        // Parse path, query params, and route to the right screen
    }
}

If your app uses singleTask or a similar launch mode, onNewIntent() becomes important. Otherwise you'll test a cold start, think everything works, and later discover warm launches don't proceed as intended.

If an App Link opens your app but lands on the home screen, your verification is only partially done. Routing is still broken.

Hosting and Validating Your assetlinks.json File

Teams usually get stuck here after the manifest looks right and the app still opens Chrome. In practice, assetlinks.json is the part that fails most often because Android depends on your web stack being exact, and web stacks love redirects, caching, and environment drift.

Android App Links only verify when three details match the same build and the same domain: your manifest declaration, your app's SHA-256 signing certificate fingerprint, and a valid assetlinks.json served over HTTPS. If one of those is off, Android often drops back to the browser with very little explanation.

A hand-drawn illustration showing a server with an assetlinks.json file being inspected with a magnifying glass.

The file has to be precise

A minimal assetlinks.json looks like this:

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.app",
      "sha256_cert_fingerprints": [
        "YOUR_SHA256_FINGERPRINT"
      ]
    }
  }
]

Serve it at:

https://example.com/.well-known/assetlinks.json

The path must be exact. A different directory, a redirect to another host, or an HTML fallback page is enough to break verification.

The response also needs normal server hygiene. Use HTTPS, serve valid JSON, and avoid edge rules that rewrite /.well-known/* requests. If your team controls the domain and hosting, this guide on choosing a web hosting provider for reliability and predictable asset delivery is a useful check before you spend hours blaming Android for a server problem.

The fingerprint mistake almost everyone makes

Debug and release builds are different identities as far as App Links are concerned. Verifying with a debug key proves nothing about production.

Use keytool against the keystore that signs the build you’re testing:

keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android

For release, use your release keystore and alias:

keytool -list -v -keystore your-release-key.keystore -alias your-alias

Copy the SHA-256 fingerprint. Then paste it into assetlinks.json exactly as generated.

If you use Play App Signing, check which certificate the installed build is signed with. That trips up plenty of teams on their first rollout. The local release keystore and the Play signing certificate are not always the same thing.

Server checks that save hours

Before touching the app again, verify the hosted file like a backend issue, not a mobile issue:

  1. Open the exact URL in a browser Confirm it returns raw JSON. If you see a branded 404 page, a login wall, or an index fallback, Android will fail too.

  2. Test with curl Check headers and redirects directly:

    curl -I https://example.com/.well-known/assetlinks.json
    curl https://example.com/.well-known/assetlinks.json
    

    You want a successful HTTPS response and the actual JSON body.

  3. Match the package name The package_name in assetlinks.json must match the installed app variant. Staging and production package names often get crossed.

  4. Match the signing certificate Use the certificate for the build on the device. This matters more than the certificate you expected to use.

  5. Check caching rules CDNs and reverse proxies can keep serving stale JSON after you fix the file. Purge caches if needed, then request the URL again from a real device.

A few failure modes show up repeatedly in production work: CDN cache serving an old fingerprint, staging builds pointed at the production domain, and copy-pasted fingerprints damaged by chat tools or docs formatting.

Add multiple certificate entries only when you intentionally support multiple valid signing identities, such as separate debug and release setups for controlled testing. Dumping every certificate your team has ever used into one file makes verification harder to reason about later.

Rigorous Testing and Debugging on Real Devices

Don't trust one successful tap. App Links need repeatable verification.

A proper test loop uses ADB, real devices, and Logcat. Emulators can help, but they won't expose every browser, OEM, or account-state quirk you'll see in production.

Use ADB to inspect domain state

Start by asking Android what it thinks about your package's link associations.

adb shell pm get-app-links com.example.app

That output tells you whether the OS considers the domain verified, denied, or unresolved. If verification state looks stale after changes, force a re-check on supported Android versions:

adb shell pm verify-app-links --re-verify com.example.app

Then trigger an actual link open:

adb shell am start -W -a android.intent.action.VIEW -d "https://example.com/products/123"

What you want is boring behavior. The app opens directly and routes to the intended screen. If you get a browser instead, verification likely failed. If you get the app but the wrong screen, routing is broken inside your app, not at the OS level.

Read the logs like a detective

Logcat is where Android stops being mysterious. Filter around intent verification and domain handling so you can spot fetch errors, signature mismatches, and verification outcomes.

Look for signs of:

  • JSON fetch failure: The device couldn't retrieve assetlinks.json successfully.
  • Signature mismatch: The domain file was found, but the signing cert doesn't match the installed app.
  • No matching intent filter: The URL path or host doesn't line up with your manifest.
  • Fallback behavior: Android resolves the URL but sends it to the browser.

Manual testing matters too. Test these paths on actual hardware:

  • Cold start: App not running.
  • Warm start: App in background.
  • Logged-out flow: Especially for password reset and invite links.
  • Links from external apps: Email, chat, note apps, and browsers.
  • Copied link paste into browser address bar: Some flows behave differently from tap events.

If your QA process is still informal, tighten it. This mobile app testing checklist is a useful baseline for making link validation part of release discipline instead of a last-minute scramble.

Interpret the result correctly

There are only a few real outcomes, and each means something different.

Result What it usually means
App opens directly Verification and intent routing are working
Browser opens Verification failed, host/path mismatch, or unsupported context
Chooser dialog appears Link may not be treated as a verified App Link for that case
App opens wrong screen App-side URI parsing or navigation logic is wrong

Don't stop after testing Chrome. The link that works perfectly from Chrome may still fail from an email client or embedded browser.

Mitigating Common Pitfalls and Real-World Failures

This is the part most clean tutorials skip. App Links are useful, but they are not uniformly reliable across Android devices and traffic sources.

The gap shows up in adoption and runtime behavior. Only about 20-30% of top apps fully support Android App Links as of 2024 surveys, compared with over 80% for iOS Universal Links, and pure App Links fail in 35% of in-app browser scenarios, according to this Branch explainer on App Links, Universal Links, and URI schemes. That should reset expectations immediately.

A conceptual drawing of a broken chain link with three figures representing conflict and troubleshooting.

Why official setups still fail in practice

The spec assumes a reasonably cooperative environment. Real users don't live in that environment.

Some links open from:

  • social apps with embedded browsers
  • OEM-customized Android builds
  • email clients that rewrite tracking URLs
  • ad platforms that wrap destination links
  • apps that don't hand off verified links cleanly

That means a perfect manifest and a valid assetlinks.json file still won't guarantee app opens everywhere.

Fallbacks that actually help

If you build universal links android behavior as if App Links are the only path, you'll ship a brittle system. The better pattern is layered handling.

Use a fallback stack like this:

  1. Verified App Link first This is still the preferred route when Android honors it.

  2. Mobile web page that understands intent If the app doesn't open, the page should still render the target content or a clear next action.

  3. Chrome Custom Tabs for controlled handoff When you need a web fallback inside the app, Custom Tabs usually produce a cleaner experience than raw WebViews.

  4. Optional custom scheme backup Use sparingly and only when you control the context, because custom schemes trade security for reach.

The biggest UX mistake is sending users to a generic home page as fallback. If the original URL was /products/123, your fallback page should preserve that destination and explain what happened in plain language.

Build your fallback for the failure case first. The success path is easy.

Protect the link payload

Wrapped links often lose context or mutate query parameters. Keep your routing logic tolerant.

A few hard-won habits help:

  • Prefer durable path-based routes: /invite/abc123 survives wrappers better than heavily nested query strings.
  • Treat query params as optional: They may disappear or arrive reordered.
  • Make destination pages useful on web too: That way browser fallback still gets the user somewhere relevant.
  • Log failed parses in-app: If a URL reaches the app but your parser rejects it, you need evidence.

For teams building acquisition flows, product invites, referral links, or auth links, resilience matters more than elegance. The cleanest App Links setup on paper still needs an escape hatch.

Strategic Choices Migration, Analytics, and Third-Party Routing

A migration to Android App Links usually looks simple in a planning doc. Then marketing asks for install attribution, product asks for invite links to survive app store hops, and support starts seeing cases where the right app opened but the user still landed on the wrong screen.

That is the real decision point. The question is not whether your team can configure App Links. The question is which parts of link routing, attribution, and recovery your team is prepared to own.

Pure Android App Links solve one problem well. They verify that a web URL can open your installed Android app. They do not replace a deferred deep linking system, and they do not give you the same attribution model you get from a dedicated routing platform. Teams migrating off Firebase Dynamic Links often discover that gap late, after the Android side is already shipped.

Keep the trade-off plain. If your requirement is "open the app from our domain when the app is installed," self-managed App Links are often enough. If your requirement includes campaign attribution, post-install routing, QR flows, email clients, social in-app browsers, and recovery when context gets stripped, you are building more than App Links.

AppsFlyer describes the same split in its analysis of Universal Links and App Links. Verified links and attribution are related, but they are not the same system.

A practical decision framework

Use this table before you commit engineering time:

Situation Best fit
You need secure verified links into installed Android app content Pure App Links
You need predictable behavior across browser wrappers and partner apps App Links plus a well-designed web fallback
You need deferred deep linking and campaign attribution continuity Third-party routing platform
You have limited Android time and no appetite for maintaining edge-case logic Buy instead of build

The analytics plan should stay boring and specific. Instrument the handoff points you control. Log whether Android received the URL, whether your parser accepted it, which screen opened, and whether the web fallback rendered for the same route. On the server side, keep a route ID or campaign token that can be observed on both web and app paths. Without that, debugging turns into guesswork.

One more hard-earned point. Do not let a vendor migration become a big-bang rewrite. Run old and new links in parallel for a while, keep redirects stable, and compare outcomes route by route. App Links failures are rarely dramatic. They show up as small drops in opens, attribution mismatches, and support tickets that look unrelated until someone traces the full path.

The clean design choice is to separate concerns. App Links handle verified opening into the installed app. Your web layer handles graceful fallback. Attribution and deferred routing need their own decision, whether that is in-house infrastructure or a third-party platform.


If your team needs help implementing Android App Links, validating edge cases, or building a fallback strategy that survives real devices and real campaigns, Nerdify's mobile development team can help.