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

  1. Publish the association file at https://your.domain/.well-known/apple-app-site-association — over HTTPS, with no redirect, served as application/json, and with no .json extension.
  2. Add the Associated Domains capability in Xcode and an entry applinks:your.domain for each domain.
  3. Handle the URL in the app: onOpenURL in SwiftUI, or scene(_:continue:) with userActivity.webpageURL in UIKit.
  4. Install a fresh build and test by tapping the link in Notes or Messages.
apple-app-site-association
{
  "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”

Android: App Links

  1. Publish the statement file at https://your.domain/.well-known/assetlinks.json, over HTTPS, as application/json, with no redirect.
  2. Add an intent filter with android:autoVerify="true" to the activity that should open, naming the scheme, host and paths.
  3. 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.
  4. Install and verify with adb, then tap the link.
assetlinks.json
[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.fitpulse",
      "sha256_cert_fingerprints": ["14:6D:E9:…:44:E5"]
    }
  }
]
AndroidManifest.xml
<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>
Terminal
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.

  1. 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.
  2. 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.
  3. Route on the path. The URL the app receives is the link itself, so /fitpulse tells 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.

Make your app link

One link and a QR code for every store. Free to start, no card required.

Create a link