# Universal Links & App Links

When your app is already installed, an `https` Detour link can open it directly
instead of loading a web page. iOS calls this a Universal Link, Android an App
Link, and both require a signed association file on the link's host.

Detour hosts those files for you. Supply the app identifiers in the dashboard,
that is Bundle ID and Team ID for iOS, package name and signing fingerprint for
Android, and Detour serves the matching payloads on your organization subdomain (for
example `your-org.godetour.link`):

- `/.well-known/apple-app-site-association` (iOS)
- `/.well-known/assetlinks.json` (Android)

Both are generated per request host from the configuration you saved.

## Deferred links vs Universal/App Links

|                  | Deferred links                                                                                                       | Universal Links and App Links                                                  |
| ---------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| Purpose          | Keep the link destination and campaign data when the app is not installed                                            | Open the app directly, without loading a web page                              |
| When it applies  | The app is not installed when the user clicks the link                                                               | The app is installed and the domain is verified on the device                  |
| What happens     | Detour records the click and redirects to the store. After install, the SDK asks Detour for the original link.       | The OS checks the domain association files and passes the URL to your app      |
| Matching         | After first open. Deterministic with the Play Install Referrer on Android, probabilistic otherwise.                   | —                                                                              |

```mermaid
flowchart TD
    A[User clicks an https link] --> B{App installed<br/>and domain verified?}
    B -->|Yes| C[Universal Link or App Link]
    C --> D[OS checks association files<br/>AASA or assetlinks.json]
    D --> E[App opens directly with the URL<br/>no browser]
    B -->|No| F[Deferred link]
    F --> G[Detour records the click<br/>and redirects to the store]
    G --> H[User installs and opens the app]
    H --> I[SDK asks Detour for the original link]
    I --> J{Matched?}
    J -->|Yes| K[App navigates to the intended content]
    J -->|No| L[App opens normally, organic install]
```

## Custom scheme links

Custom scheme links are URLs that your app defines, such as `myapp://promo/summer`.
The OS passes them to your app directly and does not check a domain association
file. You can use them as a fallback where Universal Links and App Links are not
available.

Custom schemes are optional. Detour can return the parsed link data, and your app
handles the navigation.

### Register the scheme

A custom scheme link opens your app only after you register the scheme in the
app. Registration differs between iOS and Android. Follow your SDK's installation
guide for the exact configuration.

:::note[Not part of the dashboard snippets]
The integration snippets from **App configuration** set up verified HTTPS links
only. Register the custom scheme separately.
:::

## What Detour hosts

Detour serves both association files under `/.well-known/` on the host your app
is assigned to. That is your organization subdomain, or your custom domain if you
attached one. Each file is built from your dashboard configuration when it is
requested.

| File                               | Contents                                                              |
| ---------------------------------- | --------------------------------------------------------------------- |
| `apple-app-site-association` (iOS) | Team ID, Bundle ID, the configured path pattern, and short link paths |
| `assetlinks.json` (Android)        | Package name and SHA-256 certificate fingerprints                     |

If a host has no configuration, the request returns `404`.

:::note[One host per app]
An app is assigned to one host at a time. Attaching a custom domain moves both
files to it, and the previous host stops serving them. See
[Moving an app between hosts](https://detour.swmansion.com/docs/platform/advanced/custom-domain#moving-an-app-between-hosts).
:::

### Short link paths

A short link is a single path segment on your Detour host. The format changed on
**27 July 2026**:

| Created | Path                                   | Example             |
| ------- | -------------------------------------- | ------------------- |
| Since   | the app hash, then the short link hash | `/afcsvjRbhL051ksk` |
| Before  | the short link hash on its own         | `/051ksk`           |

Both forms still work, and the association files Detour serves cover both.

The prefix matters on Android, where App Link intent filters match a path prefix.
A short link that starts with the app hash is already covered by the filter in
your app's manifest, so you can add short links without releasing a new version
of the app. Older links do not start with the app hash and need a filter of their
own, and the dashboard includes them in the Android snippet it generates for you.

:::note[Association file changes take about a week]
Apple's CDN refetches the file within 24 hours, and devices refresh roughly
weekly after install. There is no way to force a refresh, so plan configuration
changes with that delay in mind.

To see what the CDN currently holds for a host, open
`https://app-site-association.cdn-apple.com/a/v1/<your-host>`. It returns the
cached file, or `Not Found` if Apple has not fetched one for that host yet.
:::

## Dashboard configuration

Add this data to your **App configuration** so Detour can generate correct association files.

### iOS configuration

- **Bundle ID** — for example `com.example.myapp`.
- **Apple Team ID** — from your Apple Developer account. It becomes the `TEAMID.bundleId` entries in the AASA `appIDs` array.

### Android configuration

- **Package name** — for example `com.example.android`.
- **SHA-256 signing certificate fingerprints** — production, debug, or both. At least one is required. See [Finding your Android SHA-256 fingerprint](#finding-your-android-sha-256-fingerprint).

## App-side setup

The association files hosted by Detour are only one side of the setup. Your app
must also register the generated host and paths with the operating system and
deliver cold-start and runtime links to the SDK.

The exact files and callbacks depend on your framework. Follow the complete
guide for your SDK:

[React Native](https://detour.swmansion.com/docs/sdk/react-native/sdk-installation/#native-link-configuration)
  [iOS](https://detour.swmansion.com/docs/sdk/ios/sdk-installation/#link-capabilities)
  [Android](https://detour.swmansion.com/docs/sdk/android/sdk-installation/#manifest-integration)
  [Flutter](https://detour.swmansion.com/docs/sdk/flutter/sdk-installation/#platform-integration)
The Dashboard generates native configuration for verified HTTPS links. Custom
schemes are optional and must be registered separately in the app. Any change to
entitlements, Expo app config, or `AndroidManifest.xml` requires a new native
build.

## Verification

Once Detour hosts the files, devices verify them automatically:

- **iOS** fetches the AASA file and verifies `appIDs` and `paths`. Once verified, Universal Links for matching paths open the app.
- **Android** fetches `assetlinks.json` and verifies the `package_name` + `sha256_cert_fingerprints`. If verified, intent filters with `autoVerify="true"` cause App Links to open the app.

:::caution[Only list hosts the dashboard generates]
When Android
[verifies App Links](https://developer.android.com/training/app-links/verify-android-applinks),
it checks every host in your manifest. On Android 11 and lower: if any one of
those hosts does not serve a valid `assetlinks.json`, verification fails for all
of them, and your links open in the browser instead of the app.

Every host you list has to serve that file. Detour serves it for the one host an
app is assigned to, so a manifest with both the Detour subdomain and a custom
domain always contains one host that answers `404`. List only the host the app is
assigned to.

Android 12 and higher verifies each host on its own, so there only the unserved
host fails.
:::

:::note[Testing verified links]
Typing a verified link into the browser address bar does not open the app. Safari
only shows a banner. Tap the link from a messaging or notes app instead.
:::

## Finding your Android SHA-256 fingerprint

The fingerprint saved in the dashboard must belong to the key that signed **the APK installed on the device**. Any other key fails verification and the link opens in the browser.

First find which key signed the build. Each key links to the section that shows
how to read its fingerprint.

| How the app is installed                                  | Signed with                                                                          |
| --------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| Google Play, any track                                    | [Play App Signing key](#play-app-signing-key)                                        |
| Side-loaded copy of the artifact you uploaded to Play     | [Upload key](#play-app-signing-key)                                                  |
| `eas build`, any profile, including `development`         | [Keystore in your EAS account](#read-it-from-eas)                                    |
| `eas build` with `"withoutCredentials": true`             | [Project debug keystore](#read-it-from-the-keystore)<br />`android/app/debug.keystore` |
| `npx expo run:android` or `npx react-native run-android`  | [Project debug keystore](#read-it-from-the-keystore)<br />`android/app/debug.keystore` |
| Flutter and native Android debug builds                   | [Home debug keystore](#read-it-from-the-keystore)<br />`~/.android/debug.keystore`     |
| Release build signed with your own keystore               | [Your release keystore](#read-it-from-the-keystore)                                  |

:::caution[A local release build is usually debug-signed]
The React Native, Expo and Flutter templates point the `release` build type at
the **debug** signing config until you add your own release keystore.
:::

### Read it from the keystore

When you know which file signed the build, `keytool` returns the value directly, already uppercase and colon-separated, which is the format the dashboard expects:

```bash
keytool -list -v -keystore android/app/debug.keystore -storepass android | grep SHA256
```

```txt frame="none"
	 SHA256: FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:66:75:91:03:3B:9C
```

Flutter and native Android debug builds use the debug keystore in your home
directory:

```bash
keytool -list -v -keystore ~/.android/debug.keystore -storepass android | grep SHA256
```

For your own release keystore, use its path. `keytool` asks for the keystore
password:

```bash
keytool -list -v -keystore <path-to-your.keystore> | grep SHA256
```

Remove `| grep SHA256` to see the full certificate. A keystore holding several keys prints one block per alias. Pick the one whose `Alias name` matches the `keyAlias` in your signing config.

If you are unsure which keystore a variant uses, let Gradle resolve it:

```bash
cd android && ./gradlew signingReport   # native Android projects: run in the project root
```

One block per variant: match `Variant:` to the build you install, then read `SHA-256:`, which is hyphenated here, unlike `keytool`. `Store:` names the file Gradle resolved.

<details>
<summary>Sample <code>signingReport</code> output</summary>

```txt frame="none" mark={8, 17}
> Task :app:signingReport
Variant: debug
Config: debug
Store: /Users/you/project/android/app/debug.keystore
Alias: androiddebugkey
MD5: 20:F4:61:48:B7:2D:8E:5E:5C:A2:3D:37:A4:F4:14:90
SHA1: 5E:8F:16:06:2E:A3:CD:2C:4A:0D:54:78:76:BA:A6:F3:8C:AB:F6:25
SHA-256: FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:66:75:91:03:3B:9C
Valid until: Wednesday, 1 May 2052
----------
Variant: release
Config: debug
Store: /Users/you/project/android/app/debug.keystore
Alias: androiddebugkey
MD5: 20:F4:61:48:B7:2D:8E:5E:5C:A2:3D:37:A4:F4:14:90
SHA1: 5E:8F:16:06:2E:A3:CD:2C:4A:0D:54:78:76:BA:A6:F3:8C:AB:F6:25
SHA-256: FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:66:75:91:03:3B:9C
Valid until: Wednesday, 1 May 2052
```

Here `release` reports `Config: debug`, the template default described above.

</details>

`signingReport` reads local Gradle config only, so it cannot show the keystore EAS holds or the Play App Signing key.

### Read it from EAS

Builds from `eas build` are signed with the keystore stored in your EAS account:

```bash
eas credentials -p android
```

Select the build profile, then **Keystore**, and copy `SHA256 Fingerprint`.

### Read it from the artifact

When you did not build the binary yourself, or want to be certain what a device is running, inspect the signature of the file:

```bash
apksigner verify --print-certs app.apk               # build-tools; use $ANDROID_HOME/build-tools/*/apksigner if not on PATH
keytool -printcert -jarfile app.aab | grep SHA256    # .aab and other JAR-signed artifacts
```

`apksigner` prints lowercase hex without separators, while `assetlinks.json` and the dashboard expect uppercase colon-separated:

```bash
apksigner verify --print-certs app.apk \
  | awk -F'digest: ' '/SHA-256 digest/ { print $2 }' \
  | tr -d ' ' | tr 'a-f' 'A-F' | sed 's/../&:/g; s/:$//'
```

For an app already on a device: `adb shell pm path com.example.app`, then `adb pull <returned path>`.

### Play App Signing key

Play re-signs everything it delivers, internal testing included. The **App signing** page can be opened two ways:

- **Protected with Play** → **Play Store protection** card → **Manage Play app signing**.
- **Test and release** → **App integrity** → **Go to Protected with Play** → then as above.

Copy the **SHA-256 certificate fingerprint** under **App signing key certificate**. The **Upload key certificate** on the same page is a different key. It signs what you send to Google, so use it only for artifacts you side-load yourself.

### Verify

```bash
curl -s https://your-org.godetour.link/.well-known/assetlinks.json   # what Detour publishes
adb shell pm verify-app-links --re-verify com.example.app           # Android 12+
adb shell pm get-app-links com.example.app                          # expect: verified
```

## Handling links in your app

You can handle Universal/App Links and custom scheme links yourself with your preferred navigation and linking library. Detour also resolves them for you, returning a route the same way it does for deferred links. For details, see <SdkUsageLinks />.

## Related pages

[Click Handling & Redirect Flow](https://detour.swmansion.com/docs/platform/architecture/click-handling/)
  [Custom Domain](https://detour.swmansion.com/docs/platform/advanced/custom-domain/)
  [Getting Started with Detour](https://detour.swmansion.com/docs/platform/fundamentals/getting-started/)