Alla artiklar
6 oktober 2026

Universal links and deep linking: web-to-app handoff done right

Universal links on iOS and App Links on Android open your app from a tapped https link, with a safe fallback when the app is not installed.

Node graph in the shape of the Kallos Labs kappa mark on a dotted canvas
Illustration: Kallos Labs.

Universal links are Apple's mechanism for letting a tapped https link open your iOS app directly, instead of Safari. Android's equivalent is called App Links. Both fall back to the browser automatically when the app isn't installed. And both depend on a signed ownership file hosted on your own domain: apple-app-site-association for iOS, assetlinks.json for Android. Since iOS 14, Apple's file format uses a "components" key instead of the older flat "paths" list, which changes how you write path matches.

Before you start

Universal links solve a problem custom URL schemes can't. A scheme like myapp:// can be registered by more than one app, and neither iOS nor Android has a way to confirm which app actually owns it. A universal link or Android App Link ties the handoff to a real https:// URL on a domain you control, proven with a signed file the operating system fetches and verifies. That's also why Smart App Banners and universal links get described as complementary rather than redundant. A banner is a discovery prompt that still costs the visitor a tap; a correctly configured universal link removes that tap entirely.

Before touching any code, confirm four things are in place: a domain you control end to end, HTTPS on that domain with no exceptions, an Apple Developer Team ID and App ID for the iOS side, and the Android app's release signing certificate fingerprint for the Android side. Other entitlement-gated iOS capabilities follow this same pattern. Push notification setup and WidgetKit and Live Activities both need a server-side file or certificate right before the app-side entitlement does anything at all.

Step-by-step: host and shape the AASA file

The file must be named exactly apple-app-site-association, with no file extension, hosted at the root of your web server and conventionally mirrored at /.well-known/. It must be served over HTTPS. Apple's own troubleshooting guide is explicit that it must be served without any redirects: a 3xx response at that path breaks validation silently, with no error message pointing you back to the cause.

Since iOS 14, the file's applinks section uses a components array instead of the older paths key. Each entry in that array is a dictionary that can match against the URL's path, query and fragment. Only the path component actually gets compared for matching purposes. Query strings and fragments are ignored, so a query parameter present on one link and absent on another makes no difference to whether the link opens the app. An exclude property inside a components entry lets you carve out a specific path, a logout link or a legacy redirect, say, that should stay inside Safari instead of opening the app. In Xcode, add the Associated Domains capability with an entry like applinks:yourdomain.com, matching the exact domain that hosts the AASA file.

Step-by-step: mirror it on Android

Android's App Links use a Digital Asset Links file, assetlinks.json, published at https://yourdomain.com/.well-known/assetlinks.json. Google's documentation sets the same baseline rules as Apple's: HTTPS, content type application/json, reachable without redirects. The app's intent filter needs android:autoVerify="true" on the relevant <intent-filter> block. When the app installs, Android fetches assetlinks.json and checks the sha256_cert_fingerprints value against the app's actual signing certificate, before granting it the right to open matching links automatically and skip the app-picker dialog.

As of Android 15, Dynamic App Links let you update which paths are claimed by editing assetlinks.json on the server, with no new app build required. That's a meaningful operational difference from iOS, where a path change still ships inside an app update in most cases.

Test both platforms the same way: on a device with the app already installed, and on a clean device or a fresh simulator image with it removed. A link that opens correctly on a device you've tested a dozen times can still fail for a new user if the AASA or assetlinks.json file changed after that device last fetched it. Both platforms cache the verification result rather than checking on every tap.

Verify before you ship

Apple publishes an App Search API Validation Tool that checks a domain's AASA file and reports whether the Universal Links section passes. Run it against the production domain, not a staging subdomain, since the Associated Domains entitlement has to match the exact host the file lives on. On Android, adb shell pm get-app-links <package> reports which domains the installed app has successfully verified. It's the fastest way to confirm the assetlinks.json handshake actually completed, rather than silently falling back to a disambiguation dialog.

Keep a record of the last time each file changed. Both operating systems cache verification, so a fix to the AASA file or assetlinks.json doesn't take effect for a user until the OS re-fetches it, typically on the next app install or update rather than immediately.

Design the fallback page

A universal link that resolves to a web page when the app isn't installed needs that page to do something useful, not just load a dead end. Google's deep-linking guidance describes the standard pattern: own the landing URL, have that page attempt a handoff to the app, and if the app doesn't respond, keep the visitor on that same page with a clear path to install it. Don't redirect a second time or show a generic 404. The visitor already took the action that was supposed to work. The fallback page is where you catch them.

A studio that ships iOS apps for a living, Kallos Labs included, treats this fallback page and the AASA file as part of the same pre-submission checklist, not something bolted on after the app is in the store.

Common mistakes

Most universal link failures come down to one of five causes, all documented in Apple's troubleshooting guide:

  • A redirect on the AASA or assetlinks.json response. Any 3xx in that chain breaks validation on both platforms.
  • A domain mismatch. The Associated Domains entitlement in the app must reference the exact domain that hosts the AASA file, not a subdomain or alias of it.
  • An expired certificate. If the AASA file is signed, an expired signing or intermediate certificate invalidates it even when the JSON content is correct.
  • An appID or path mismatch. The appID in the AASA file must match the app's real App ID, and at least one path entry must match the tapped URL's path component.
  • A rejecting delegate. The app's own UIApplicationDelegate method can return false and refuse to handle a link that otherwise validated correctly.

A mismatched identifier, an expired credential, a configuration the review process doesn't surface until submission: these same categories show up again and again in App Store rejection reasons generally. Testing the handoff on a real device, with the app both installed and freshly deleted, before submission catches most of this. Teams planning a native app that needs a clean handoff from the web can see how this fits into a broader iOS build at Kallos Labs's iOS development services.

Frequently asked questions

Yes. A tapped universal link falls back to opening the same URL in Safari, or Chrome for Android App Links, when the app is not on the device, which is what makes the mechanism safe to ship without creating a broken-link risk.

A valid AASA file is necessary but not sufficient. Apple's own troubleshooting guide lists an expired signing certificate, an appID in the file that does not match the app's real App ID, no declared path matching the tapped URL, and the app's delegate method returning false and rejecting the link as the most common causes once validation itself passes.

Do I need both an AASA file and an assetlinks.json file?

Yes, if the product ships on both platforms. Apple reads apple-app-site-association for universal links, and Android reads assetlinks.json for App Links. They are separate files with separate schemas, hosted at separate well-known paths, and each must independently pass HTTPS-with-no-redirects hosting rules.

A custom URL scheme such as myapp:// can be registered by more than one app and has no ownership proof, so neither iOS nor Android can verify which app should handle it. A universal link or Android App Link uses a real https:// URL tied to a domain the business proves it owns through the AASA or assetlinks.json file, so the operating system can safely open the app without a disambiguation prompt.

Should the fallback web page redirect immediately to the app, or show something first?

Own the landing URL and attempt the handoff first. If the app does not respond, keep the visitor on that same page with a clear install prompt rather than redirecting them again or showing a dead link, which is the pattern Google's own deep-linking documentation describes.