How to set up universal links and Android App Links
A link that opens your app when it is installed, and the store or a web page when it is not. Both platforms do this without an SDK — with one file on your domain and one declaration in your app. This guide covers both sides and how to test them.
How it works
The phone has to trust that your app may open links on a domain. So the domain publishes a file that names the app, and the app names the domain. When both sides agree, a tap on the link opens the app. If the app is not installed — or the check failed — the link opens in the browser like any other, which is where it can go on to the store.
iOS: universal links
- Publish the association file at
https://your.domain/.well-known/apple-app-site-association— over HTTPS, with no redirect, served asapplication/json, and with no.jsonextension. - Add the Associated Domains capability in Xcode and an entry
applinks:your.domainfor each domain. - Handle the URL in the app:
onOpenURLin SwiftUI, orscene(_:continue:)withuserActivity.webpageURLin UIKit. - Install a fresh build and test by tapping the link in Notes or Messages.
{
"applinks": {
"details": [
{
"appIDs": ["ABCDE12345.com.example.fitpulse"],
"components": [{ "/": "/fitpulse" }, { "/": "/invite/*" }]
}
]
}
} The app ID is your Team ID (ten characters, under Membership in the developer account) and your bundle ID, joined by a dot. components lists the paths the app may open; everything else on the domain stays in the browser.
iOS does not fetch the file from your server directly. Apple’s CDN fetches it and caches it, and the device asks the CDN when the app is installed or updated. A changed file can take a day or more to reach phones. While developing, add ?mode=developer to the entitlement entry and enable Associated Domains Development on the device to skip the cache.
Why universal links “do not work”
- The link was typed or pasted into Safari’s address bar. Universal links open the app only from a tap.
- The link points to the same domain as the page it is on. Apple treats that as navigation within the site.
- The user once chose “Open in Safari”. iOS remembers that per domain until they long-press a link and pick the app again.
- An in-app browser. Instagram, TikTok and others often open links inside themselves without asking iOS.
- The file is redirected, served as HTML or behind authentication. Check it with the app links validator.
Android: App Links
- Publish the statement file at
https://your.domain/.well-known/assetlinks.json, over HTTPS, asapplication/json, with no redirect. - Add an intent filter with
android:autoVerify="true"to the activity that should open, naming the scheme, host and paths. - Use the right fingerprint. With Play App Signing, Google signs the app you ship: take the SHA-256 of the app signing key from Play Console → Test and release → App integrity, not the one of your upload key. Add the debug key’s fingerprint too if you test debug builds.
- Install and verify with
adb, then tap the link.
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.fitpulse",
"sha256_cert_fingerprints": ["14:6D:E9:…:44:E5"]
}
}
] <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="go.example.com" android:path="/fitpulse" />
</intent-filter> adb shell pm verify-app-links --re-verify com.example.fitpulse
adb shell pm get-app-links com.example.fitpulse get-app-links should list the domain as verified. Anything else — legacy_failure, 1024 — means Android could not fetch or match the file. Google’s Statement List API shows what it sees: https://digitalassetlinks.googleapis.com/v1/statements:list?source.web.site=https://go.example.com&relation=delegate_permission/common.handle_all_urls.
Doing it with SetLinks links
If your app’s links are SetLinks links, you do not host the two files: SetLinks publishes them for your links on the link’s domain, and keeps them in step when you add or remove a link. You do the app side.
- Turn on open in app for the link and enter your Team ID and bundle ID, and for Android the package name and SHA-256 fingerprints. It is on plans that include it, listed on the pricing page.
- Add the domain to the app:
applinks:setlinks.io, or your own domain, in Associated Domains; the intent filter above on Android. The dashboard shows the exact lines for each link. - Route on the path. The URL the app receives is the link itself, so
/fitpulsetells it which link was tapped.
On setlinks.io only iPhone opens the app. Android verifies a whole domain, and on Android 14 and older an app’s own manifest decides which paths it claims — on a shared domain one app could take other customers’ links. So SetLinks publishes Android App Links only on a team’s own domain, where every path belongs to that team.
When the app is not installed, the same link does what it always did: sends the phone to the right store. What it does not do is carry the link through the install to a screen in the app — that is deferred deep linking, and it needs an SDK.
Questions
Does apple-app-site-association need a .json extension?
No. It must be at /.well-known/apple-app-site-association exactly, served with an application/json content type and without a redirect.
Which SHA-256 fingerprint goes into assetlinks.json?
The one of the key that signs the app on users’ phones. With Play App Signing that is the app signing key shown in Play Console, not your upload key.
Do universal links need an SDK?
No. They are a platform feature: a file on the domain and an entitlement in the app. An SDK is needed only for deferred deep links.