This is the full developer documentation for Detour # Detour Documentation > Detour is a deep linking library built for developers. **Detour** is a deep linking library built for developers. It is lightweight, developer-first, and free to start. Detour handles deep linking and deferred deep linking for mobile apps. Navigation context and campaign data are preserved through the store install, so the user opens the in-app content the link pointed at. Marketing teams create links and track campaigns in the Dashboard. Developers integrate the SDK with a small amount of code. Detour provides: * Dedicated SDKs for React Native, iOS, Android, and Flutter. * A Dashboard for links, app configuration, and privacy-aware analytics. * Deterministic and probabilistic matching, tunable per app. ## Choose your SDK [Section titled “Choose your SDK”](#choose-your-sdk) [ React Native Expo & bare React Native SDK, with navigation setup. ](/docs/sdk/react-native/sdk-installation/)[ iOS Native Swift SDK for deferred, universal & scheme links. ](/docs/sdk/ios/sdk-installation/)[ Android Kotlin SDK for deferred links, App Links & scheme links. ](/docs/sdk/android/sdk-installation/)[ Flutter Flutter plugin bridging native Detour on iOS & Android.](/docs/sdk/flutter/sdk-installation/) Flutter and the related logo are trademarks of Google LLC. We are not endorsed by or affiliated with Google LLC. ## Start here [Section titled “Start here”](#start-here) [Platform fundamentals ](/docs/platform/fundamentals/introduction/)Learn how deferred deep linking, link matching, and organizations work, then pick the SDK for your stack above. # Platform API > Authenticate with a secret key to manage resources and retrieve analytics through the Detour Platform API. The Platform API lets your own backend manage Detour resources and pull analytics without going through the Dashboard. It covers three areas: * short links and the parameters they carry, * the app’s link settings, * analytics exports in JSON, CSV, or XLSX. Every endpoint is authenticated with a secret API key. The base URL is `https://api.godetour.link`. ## Authentication [Section titled “Authentication”](#authentication) Each request carries two headers: a secret API key as a Bearer token, and the ID of the app it applies to. A request missing either one is rejected with `401`. ### API key types [Section titled “API key types”](#api-key-types) Detour provides two types of API keys: | Key Type | Purpose | Use Case | | :---------- | :---------------------------------------- | :-------------------------------- | | Publishable | Identify your app, used in SDKs | Client-side SDK implementations | | Secret | Full platform management & administration | Server-side API calls, automation | ### Generating a secret key [Section titled “Generating a secret key”](#generating-a-secret-key) Secret keys are issued per app, from the **API configuration** tab of the app you want to manage: 1. Open the app in the Dashboard and go to **API configuration**. 2. In the **Secret API Key** card, click **Generate secret key**. 3. Copy the key from the dialog and store it somewhere safe. The secret key is shown once The key is shown **once**, in that dialog only. Detour stores only a hash of it, so it cannot be retrieved or revealed later. If you lose it, generate a new one. The card afterwards shows only whether a key is active and when it was generated. To replace a key, use **Regenerate secret key**: the previous key is revoked the moment the new one is issued, so any server-side integration still using the old key stops working immediately. Plan the rollout before you regenerate. Generating and regenerating keys requires the **Owner** or **Admin** role. ### Authentication headers [Section titled “Authentication headers”](#authentication-headers) Include both headers in every request: ```http Authorization: Bearer X-App-ID: ``` * `Authorization` — the app’s secret key, as a Bearer token. * `X-App-ID` — the app’s ID (a UUID), from the same **API configuration** tab. A value that is not a UUID is rejected with `401`. Example request ```bash curl -X GET https://api.godetour.link/api/manage/short-links \ -H "Authorization: Bearer 1234567890abc..." \ -H "X-App-ID: your-app-id" ``` Secret keys stay on the server Secret keys must be stored securely on your server. Never expose them in client-side code or commit them to version control. *** ## Short links [Section titled “Short links”](#short-links) Short links wrap the app’s main link together with a set of custom parameters, which the SDK returns to your app after a deferred or direct open. A custom domain changes the returned `url` The `url` field returned by these endpoints is built from your organization’s link host. The examples below use the default `.godetour.link` subdomain, but if your organization has a [custom domain](/docs/platform/advanced/custom-domain) configured, short link URLs are returned on that domain instead. ### List short links [Section titled “List short links”](#list-short-links) ```http GET /api/manage/short-links ``` Returns every short link belonging to the app, newest first. The endpoint takes no query parameters. **Status codes** — `200`, `401`, `429`, `500` Example request ```bash curl -X GET https://api.godetour.link/api/manage/short-links \ -H "Authorization: Bearer a1b2c3d4e5f6..." \ -H "X-App-ID: my-app" ``` Example response ```json [ { "id": "short_link_uuid", "hash": "abc123", "url": "https://acme-corp.godetour.link/abc123", "parameters": "{\"campaign\":\"summer_sale\"}", "name": "Summer Sale Campaign", "link_id": "link_uuid", "created_at": "2026-06-16T10:30:00.000Z" } ] ``` *** ### Create a short link [Section titled “Create a short link”](#create-a-short-link) ```http POST /api/manage/short-links ``` Creates a short link on the app’s main link and returns it, including the public URL. **Request body** ```json { "parameters": "{\"campaign\":\"summer_sale\",\"utm_source\":\"email\"}", "name": "Summer Sale Email Campaign" } ``` **Required fields** * `parameters` required * Type `string` * JSON string of custom parameters (cannot be empty) **Optional fields** * `name` * Type `string` * User-friendly name for the short link. **Status codes** — `201`, `400`, `401`, `402`, `404`, `429`, `500` * `402` — the Free plan limit of 50 short links per app is reached. Paid plans have no limit. * `404` — the app has no link yet. Example request ```bash curl -X POST https://api.godetour.link/api/manage/short-links \ -H "Authorization: Bearer a1b2c3d4e5f6..." \ -H "X-App-ID: my-app" \ -H "Content-Type: application/json" \ -d '{ "parameters": "{\"campaign\":\"summer_sale\"}", "name": "Summer Sale" }' ``` Example response ```json { "id": "short_link_uuid", "hash": "abc123", "url": "https://acme-corp.godetour.link/abc123", "parameters": "{\"campaign\":\"summer_sale\",\"utm_source\":\"email\"}", "name": "Summer Sale Email Campaign", "link_id": "link_uuid", "created_at": "2026-06-16T10:35:00.000Z" } ``` *** ### Update a short link [Section titled “Update a short link”](#update-a-short-link) ```http PATCH /api/manage/short-links ``` Replaces the parameters, and optionally the name, of an existing short link. The hash and URL stay the same, so links already distributed keep working. **Request body** ```json { "id": "short_link_uuid", "parameters": "{\"campaign\":\"fall_sale\"}", "name": "Fall Sale Campaign" } ``` **Required fields** * `id` required * Type `string` * UUID of the short link to update. * `parameters` required * Type `string` * Updated JSON string of custom parameters. **Optional fields** * `name` * Type `string` * Updated user-friendly name. **Status codes** — `200`, `400`, `401`, `404`, `429`, `500` Example request ```bash curl -X PATCH https://api.godetour.link/api/manage/short-links \ -H "Authorization: Bearer a1b2c3d4e5f6..." \ -H "X-App-ID: my-app" \ -H "Content-Type: application/json" \ -d '{ "id": "short_link_uuid", "parameters": "{\"campaign\":\"fall_sale\"}", "name": "Fall Sale Campaign" }' ``` Example response ```json { "id": "short_link_uuid", "hash": "abc123", "url": "https://acme-corp.godetour.link/abc123", "parameters": "{\"campaign\":\"fall_sale\"}", "name": "Fall Sale Campaign", "link_id": "link_uuid", "created_at": "2026-06-16T10:35:00.000Z" } ``` *** ## Link settings [Section titled “Link settings”](#link-settings) Every app has one main link. Its settings decide where a click is redirected and which parameters are forwarded. ### Get link settings [Section titled “Get link settings”](#get-link-settings) ```http GET /api/manage/links ``` Returns the app’s main link: its destination, its fallback, and how query parameters are passed through. The endpoint takes no query parameters. **Response fields** * `id` * Type `string` * UUID of the link. * `url` * Type `string` * Primary redirect URL. * `redirect_url` * Type `string` * Fallback redirect URL. * `params_pass_strategy` * Type `string` * How parameters are passed: `"all"`, `"none"`, or `"specific"` * `specific_params` * Type `array | null` * If set, only these parameters are passed through. * `created_at` * Type `string` * Link creation timestamp (ISO 8601) * `app_hash` * Type `string` * Hash identifier used in short link URLs. **Status codes** — `200`, `401`, `404`, `429`, `500` * `404` — the app has no link. Example request ```bash curl -X GET https://api.godetour.link/api/manage/links \ -H "Authorization: Bearer a1b2c3d4e5f6..." \ -H "X-App-ID: my-app" ``` Example response ```json { "id": "link_uuid", "url": "https://example.com", "redirect_url": "https://fallback.example.com", "params_pass_strategy": "all", "specific_params": null, "created_at": "2026-06-16T10:00:00.000Z", "app_hash": "x9y8z7" } ``` *** ## Analytics [Section titled “Analytics”](#analytics) ### Export analytics [Section titled “Export analytics”](#export-analytics) ```http GET /api/analytics/stats ``` Exports the same figures the Dashboard’s Analytics section shows, over a date range you choose. The response is always sent as a file download, in the format given by `format`. **Query parameters** * `type` required * Type `string` * Data type: `"overview"`, `"links"`, or `"events"` * `startDate` required * Type `string` * Start of date range (ISO 8601, for example `2026-06-01T00:00:00Z`) * `endDate` required * Type `string` * End of date range (ISO 8601, for example `2026-06-30T23:59:59Z`) * `format` * Type `string` Default `json` * Export format: `"json"`, `"csv"` or `"xlsx"` * `platform` * Type `string` Default `all` * Filter by platform: `"all"`, `"ios"`, or `"android"` * `linkId` * Type `string` * UUID of a specific link to scope the query. Defaults to the main app link. Relevant for `"links"` and `"overview"` types, ignored for `"events"` * `limit` * Type `number | "all"` Default `1000` * Rows per page, `1`–`1000`, or `"all"` for the complete set in one response. `type=links` and `type=events` only. * `offset` * Type `number` Default `0` * Row offset for paging. Not accepted with `limit=all`. `type=links` and `type=events` only. * `envelope` * Type `boolean` Default `false` * `format=json` only. Wraps the array as `{ meta, rows }` instead of returning a bare array. * `paramKeys` * Type `string` * Comma-separated parameter names to keep in the breakdown. `type=links` only. * `excludeParams` * Type `string` * Comma-separated parameter names to hide from the breakdown. Their clicks still count. `type=links` only. Click-level filters are listed separately under [Filtering link analytics](#filtering-link-analytics). **Plan requirements** | `type` | Required Plan | | :--------- | :------------ | | `overview` | Free | | `links` | Paid | | `events` | Paid | **Response** — one row per day, with the columns for the requested `type`: | `type` | Columns | | :--------- | :------------------------------------------------------------------------------------------------------------------------------------------------- | | `overview` | `date`, `total_clicks`, `already_installed_opens`, `fallback_redirects`, `organic_installs`, `non_organic_installs`, `top_event`, `retention_rate` | | `links` | `date`, `short_link`, `path`, `param_key`, `param_value`, `clicks`, `matched_installs`, `fallbacks`, `already_installed_opens` | | `events` | `date`, `dimensions` (`event_name`, `platform`, `is_retention`), `metrics` (`event_count`, `unique_devices`) | CSV and XLSX exports flatten `dimensions` and `metrics` into plain columns. #### Reading `links` rows [Section titled “Reading links rows”](#reading-links-rows) A `links` export mixes two kinds of row: | Row | How to recognize it | What it holds | | :------------ | :------------------------------------- | :---------------------------------------- | | Path row | `param_key` and `param_value` are `""` | Totals for one date, short link, and path | | Parameter row | `param_key` is set | One parameter value inside that path | Sum path rows only A URL can carry several parameters, so parameter rows add up to more clicks than their path row. Use path rows for totals. Path rows and parameter rows both carry `already_installed_opens`, the taps that opened an installed app directly through [Universal Links or App Links](/docs/platform/fundamentals/universal-app-links). These opens are included in `clicks`: * `clicks` — all opens * `clicks - already_installed_opens` — web opens only An open is split into parameter rows the same way a web click is, and filters apply to it. Its `short_link` is empty when the tap was on a plain deep link, or when the open was recorded before Detour began attributing these opens. Summed this way, `type=links` matches `type=overview` and the Dashboard for the same app, period, and platform. #### Status codes [Section titled “Status codes”](#status-codes) `200`, `400`, `401`, `402`, `403`, `404`, `429`, `500`, `503` * `400` — `type` is missing or invalid, the date range is longer than **120 days**, or a filter or pagination parameter is out of range or does not apply to `type`. * `402` — `links` and `events` require a paid plan. * `403` — `linkId` does not belong to this app. * `404` — the app was not found. Checked for `links` and `events` only. * `503` — the analytics query timed out. Narrow the date range, or drop high-cardinality parameters with `excludeParams`. Error bodies on this endpoint carry a machine-readable `code` alongside `error`, and sometimes a `hint`: ```json { "error": "Analytics query timed out", "code": "query_timeout", "hint": "Narrow the date range, or use paramKeys/excludeParams to drop high-cardinality parameters such as fbclid." } ``` A date range spans at most 120 days The range from `startDate` to `endDate` can be at most **120 days**. A longer range is rejected with `400`. The Dashboard date picker has the same limit. #### Pagination [Section titled “Pagination”](#pagination) `type=links` and `type=events` are paged. The default page is 1,000 rows, and the response always reports whether more rows exist, so a truncated result is always signalled. Every format carries the same metadata in response headers, because CSV and XLSX cannot carry a response envelope: | Header | Meaning | | :---------------- | :---------------------------------------------------------- | | `X-Total-Rows` | Rows in the full result set. Absent when the page is empty. | | `X-Returned-Rows` | Rows in this response | | `X-Row-Offset` | Offset this page started at | | `X-Row-Limit` | Page size used. Absent for `limit=all` | | `X-Truncated` | `true` when more rows follow | | `Link` | `; rel="next"`, when more rows follow | All of them are listed in `Access-Control-Expose-Headers`, so a browser-based consumer reading the API cross-origin can see them. With `envelope=true` and `format=json` the same values also come back in the body: ```json { "meta": { "total_rows": 4821, "returned_rows": 1000, "limit": 1000, "offset": 0, "truncated": true, "next_offset": 1000 }, "rows": [...] } ``` Follow `next_offset`, or the `Link` header, until `truncated` is `false`. `limit=all` returns the complete set in a single response instead. It is opt-in because a large result takes longer to answer than a page, and it rejects `offset`. #### Filtering link analytics [Section titled “Filtering link analytics”](#filtering-link-analytics) Two independent filter layers apply to `type=links`, and they compose. Both are rejected with `400` on `type=overview` and `type=events` rather than silently ignored. A dropped filter would read as “no clicks matched”, which is indistinguishable from a real answer. **Row filters: `paramKeys` and `excludeParams`.** These decide which parameter rows come back. `paramKeys` keeps only the listed parameters, and `excludeParams` hides the listed ones. Neither removes a click. Path rows always come back and still count every click, so a path row is not the sum of the parameter rows you kept. Use row filters to keep a high-cardinality parameter such as `fbclid` from filling the whole page: ```bash curl -X GET "https://api.godetour.link/api/analytics/stats?type=links&startDate=2026-06-01T00:00:00Z&endDate=2026-06-30T23:59:59Z&excludeParams=fbclid,gclid" \ -H "Authorization: Bearer a1b2c3d4e5f6..." \ -H "X-App-ID: my-app" ``` **Click filters.** These decide which clicks are counted, so they change every number in the response, path rows included. They are the same filters as the filter bar in Dashboard → **Analytics** → **Links**. There are four dimensions, `path`, `param_name`, `param_kv`, and `param_value`, each usable with the operators `is`, `is_not`, `contains`, and `starts_with`. The filter bar labels them **is**, **is not**, **contains**, and **starts with**. Shorthand parameters avoid URL-encoding JSON: | Parameter | Operator parameter | Dimension | Dashboard filter | | :------------------ | :------------------- | :------------ | :-------------------- | | `filterPath` | `filterPathOp` | `path` | **Path** | | `filterParamName` | `filterParamNameOp` | `param_name` | **Param name** | | `filterParamValue` | `filterParamValueOp` | `param_value` | **Param value** | | `filterParam.` | `filterParamOp` | `param_kv` | **Param key = value** | Comma-separated values inside one filter are combined with OR. Separate filter parameters are combined with AND. The operator defaults to `is`. `filterParamOp` applies to every `filterParam.` in the request. To use different operators on two `filterParam.` filters, use the JSON form below. For the same dimension and values, `is_not` keeps exactly the clicks that `is` drops, so the two counts add up to the unfiltered total. With several values, it keeps the clicks that match none of them. A click that does not carry the parameter is kept. For example, `filterParam.utm_source=facebook` with `filterParamOp=is_not` also keeps the clicks that have no `utm_source`. The exception is an [imported install](/docs/platform/fundamentals/analytics#imports) without a link path, which every active click filter drops, `is_not` included. ```bash curl -X GET "https://api.godetour.link/api/analytics/stats?type=links&startDate=2026-06-01T00:00:00Z&endDate=2026-06-30T23:59:59Z&filterPath=/checkout&filterPathOp=starts_with&filterParam.utm_source=facebook" \ -H "Authorization: Bearer a1b2c3d4e5f6..." \ -H "X-App-ID: my-app" ``` The same filters can be sent as JSON in a single `filters` parameter, as an array of `{ "dimension": { "key": … }, "operator": …, "values": [ … ], "paramKey": … }` objects. `operator` takes the same four values. Use that form when a value contains a comma, which the shorthand cannot express. At most **20 filters** and **200 filter values** may be combined in one request. The table below compares row filters and click filters on four clicks on `/shop`: ```txt /shop?color=red&id=111 /shop?color=red&id=222 /shop?color=blue&id=333 /shop ``` | Row | No filter | `paramKeys=color` | `excludeParams=id` | `filterParamName=id` | | :----------------- | --------: | ----------------: | -----------------: | -------------------: | | `/shop` (path row) | 4 | 4 | 4 | 3 | | `color=red` | 2 | 2 | 2 | 2 | | `color=blue` | 1 | 1 | 1 | 1 | | `id=111` | 1 | — | — | 1 | | `id=222` | 1 | — | — | 1 | | `id=333` | 1 | — | — | 1 | `paramKeys` and `excludeParams` hide parameter rows only, so the path row still counts all four clicks. `filterParamName=id` removes the fourth click before counting, so the path row counts 3. More example requests Get overview analytics: ```bash curl -X GET "https://api.godetour.link/api/analytics/stats?type=overview&startDate=2026-06-01T00:00:00Z&endDate=2026-06-30T23:59:59Z" \ -H "Authorization: Bearer a1b2c3d4e5f6..." \ -H "X-App-ID: my-app" ``` Export link analytics as CSV (paid): ```bash curl -X GET "https://api.godetour.link/api/analytics/stats?type=links&format=csv&startDate=2026-06-01T00:00:00Z&endDate=2026-06-30T23:59:59Z" \ -H "Authorization: Bearer a1b2c3d4e5f6..." \ -H "X-App-ID: my-app" \ -o analytics.csv ``` Filter by platform: ```bash curl -X GET "https://api.godetour.link/api/analytics/stats?type=events&platform=ios&startDate=2026-06-01T00:00:00Z&endDate=2026-06-30T23:59:59Z" \ -H "Authorization: Bearer a1b2c3d4e5f6..." \ -H "X-App-ID: my-app" ``` *** ## Rate limits [Section titled “Rate limits”](#rate-limits) Detour limits requests per app and per minute, with a separate limit for each endpoint. A `429` response carries `Retry-After` (seconds), `X-RateLimit-Remaining` and `X-RateLimit-Reset`. Wait for `Retry-After` before retrying. *** ## Error handling [Section titled “Error handling”](#error-handling) All errors return a JSON response with an `error` field. Each endpoint above lists the codes it returns and explains the ones specific to it. | Status | Meaning | | :----- | :-------------------------------------- | | `400` | Invalid request body | | `401` | Missing or invalid API key / App ID | | `402` | Plan limit reached, upgrade required | | `403` | Resource belongs to another app | | `404` | Resource not found | | `429` | Rate limit exceeded, back off and retry | | `500` | Server error | | `503` | Analytics query timed out | *** ## Support [Section titled “Support”](#support) Need help? Support is available on the [Detour Discord server](https://discord.gg/tj7uFuymne) and through the [contact form](https://detour.swmansion.com/#contact). # Custom Domain > Serve Detour links from your own domain. Links can run on your own host, for example `links.yourdomain.com` instead of `your-org.godetour.link`. Point a DNS record at Detour. Detour then serves TLS certificates, the `.well-known` association files, and per-app routing. ## Direct domain integration [Section titled “Direct domain integration”](#direct-domain-integration) In this mode, your users stay on your custom domain throughout their session. Detour handles domain hosting, SSL certificates, and the Universal / App Link verification files, with no manual server configuration needed. Scale plan and above Direct domain integration is part of the **Scale** plan. Free and Starter organizations serve links from their `.godetour.link` subdomain. See [Billing](/docs/platform/fundamentals/billing#available-plans) for what each plan includes. ### 1. Add the domain [Section titled “1. Add the domain”](#1-add-the-domain) 1. Go to the **Detour Dashboard** and select your organization. 2. Navigate to **Organization Settings** and open the **Custom Domain** tab. 3. Enter your custom domain name and click **Add Domain**. ### 2. Update DNS records [Section titled “2. Update DNS records”](#2-update-dns-records) After adding your domain, the Dashboard displays the **exact DNS record** you need to configure at your DNS provider. The values are generated per-domain and unique to your setup, so always copy them directly from the Dashboard. Copy the values from the Dashboard The Dashboard shows a **Type**, **Name**, and **Value** table with a copy button next to each value. Use those exact values at your DNS provider. The record type depends on your domain: * **Subdomains** (for example `links.yourdomain.com`) → a **CNAME** record, named after the subdomain label (`links`) * **Apex/Root Domains** (for example `yourdomain.com`) → an **A** record, named `@` If the Dashboard shows additional records (for example a `TXT` record), add all of them. Extra records appear when your domain requires extra ownership verification, typically because it is already in use on Vercel elsewhere. ### 3. Verify and activate [Section titled “3. Verify and activate”](#3-verify-and-activate) With the records in place, click **Verify DNS** in the Dashboard. Propagation usually takes a few minutes, occasionally up to 48 hours. The domain carries a **Pending DNS** badge until the record is detected, then switches to **Verified** and starts serving links. Detour re-checks DNS daily If your DNS finishes propagating later, you do not need to come back and click **Verify DNS** again. Detour re-checks every custom domain daily and also syncs the status when you open the Dashboard. ### 4. Assign apps [Section titled “4. Assign apps”](#4-assign-apps) An active domain can be assigned to one or more apps, from the **Assign to apps** checklist on the same settings page. Once an app is assigned: * Its short links and deferred links use your custom domain, for example `https://links.yourdomain.com/`. * Detour serves that app’s `/.well-known/apple-app-site-association` and `/.well-known/assetlinks.json` on the custom domain. Unassigning an app, or removing the domain, reverts both back to your `.godetour.link` subdomain. An organization can hold **several custom domains** at once and assign different apps to different domains. An app is served from one domain at a time. Changing the host breaks old links Assigning or unassigning a domain replaces the host the app is served from instead of adding one, and the links already shared on the old host stop working. Read the section below before you do this on a live app. ## Moving an app between hosts [Section titled “Moving an app between hosts”](#moving-an-app-between-hosts) An app is served from one host at a time. Three operations replace that host, and they all behave the same way: * assigning a custom domain * unassigning it * losing it when a paid plan is canceled ### What changes [Section titled “What changes”](#what-changes) * **The app’s links move.** `https:////…` stops resolving, so a link already shared on the old host returns a 404. * **The association files move with it**, so only the current host opens the app directly. * **Short links are the exception.** They are looked up by their hash rather than by host, so they keep resolving on either one. On the old host they open in the browser instead of the app. ### Planning the switch [Section titled “Planning the switch”](#planning-the-switch) Change the host only when you are ready to publish links on the new one. Do not change it while links on the old host are still being distributed. There is no redirect from the old host, so anything already printed, scheduled or embedded on it has to be reissued. A host change also needs a new native build. The host goes into `associatedDomains` on iOS and into the App Link `` on Android, and both are compiled into the app. Assign the domain first, then copy the snippets the Dashboard generates for it. They are written for the host the app is assigned to at the time you copy them. The installation guide for your SDK shows where each one goes. Keep the DNS record for as long as links published on that host should keep resolving. Removing it also removes the host from Detour, and no Dashboard setting restores those links. *** ## Legacy redirect-based integration [Section titled “Legacy redirect-based integration”](#legacy-redirect-based-integration) Deprecated The redirect-based strategies below are **deprecated**. Migrate to [Direct domain integration](#direct-domain-integration) for automatic SSL, `.well-known` hosting, and no extra redirect hops. ### Redirect target [Section titled “Redirect target”](#redirect-target) Construct your redirect target using your **Organization Subdomain** and **App Hash**. | Component | Variable | Example | | :--------------- | :----------- | :---------- | | **Organization** | `` | `acme-corp` | | **App Hash** | `` | `x9y8z7` | ### Strategy A: path-based redirect [Section titled “Strategy A: path-based redirect”](#strategy-a-path-based-redirect) **Best for** — providers with Layer 7 capabilities (Cloudflare Page Rules, AWS ALB, Vercel Edge, Netlify). This method resolves directly to the canonical resource location, with no intermediate redirect. * **Source** — `client-domain.com/*` * **Destination** — `https://.godetour.link//$1` * **Status** — `301 Permanent Redirect` **Example configuration (pseudo-code)** ```nginx # Nginx / Reverse Proxy Example server { server_name client-domain.com; location / { return 301 https://acme-corp.godetour.link/x9y8z7$request_uri } } ``` ### Strategy B: host-based redirect [Section titled “Strategy B: host-based redirect”](#strategy-b-host-based-redirect) **Best for** — basic DNS registrars (Namecheap, GoDaddy) or limited forwarding services that do not support path appending or complex routing rules. If your provider validates the destination field as a strict hostname (disallowing slashes/paths), use the underscore-delimited format. The Detour edge layer detects this format and redirects the client to the canonical path. * **Source** — `client-domain.com` * **Destination** — `https://_.godetour.link` * **Status** — `301 Permanent Redirect` **Flow behavior** 1. The client requests `client-domain.com`. 2. Your provider redirects to `acme-corp_x9y8z7.godetour.link`. 3. Detour redirects to the canonical `acme-corp.godetour.link/x9y8z7`. ### Checklist [Section titled “Checklist”](#checklist) * **SSL/TLS** — your redirect service must terminate SSL for `client-domain.com`, or users see a certificate warning before the redirect happens. * **Path preservation** — carry query parameters and sub-paths through the redirect where you can, so `.../dashboard` becomes `...//dashboard`. * **DNS propagation** — verify resolution with `curl`. ```bash # Verification curl -I https://client-domain.com # Expected Output HTTP/2 301 location: https://acme-corp.godetour.link/x9y8z7/ ``` ### Universal and App Links [Section titled “Universal and App Links”](#universal-and-app-links) iOS and Android verify app ownership by fetching a file from the URL the user clicked: * **iOS** — `/.well-known/apple-app-site-association` * **Android** — `/.well-known/assetlinks.json` Both **abort verification** if that request answers with a 301 or 302, which is exactly what a redirect-based strategy does. Your web server needs a hybrid configuration: 1. **Serve the verification files locally.** The `.well-known` paths must return your app’s JSON with `200 OK`, never a redirect. 2. **Redirect everything else** to the Detour platform. **Example Nginx configuration** ```nginx # 1. Serve your app's deep link files locally (NO REDIRECT) location /.well-known/ { root /var/www/html; } # 2. Redirect everything else to Detour location / { return 301 https://orgname.godetour.link/apphash$request_uri; } ``` The Detour side of this setup is described in [Universal and App Links](/docs/platform/fundamentals/universal-app-links). Apple documents the iOS requirements in [Supporting Associated Domains](https://developer.apple.com/documentation/xcode/supporting-associated-domains), and Google documents the Android requirements in [Android App Links](https://developer.android.com/training/app-links/about). ## Support [Section titled “Support”](#support) Need help? Support is available on the [Detour Discord server](https://discord.gg/tj7uFuymne) and through the [contact form](https://detour.swmansion.com/#contact). # Custom HTML Redirects > Customize the redirect, iOS copy-link, and app preview pages users see when they open your Detour links, using a template or your own HTML. Between the tap and the App Store, users see a Detour page. You can replace it with your own HTML, with your logo, your loading state, and your wording, instead of a generic interstitial. Each app has three such pages, each on its own tab: * **Redirect page** — shown during the immediate redirect and fingerprinting. * **Copy link page** — the background of the iOS clipboard flow. * **App preview** — an optional interstitial shown before the store redirect, which the visitor continues from with a tap. *** ## Where to configure [Section titled “Where to configure”](#where-to-configure) To customize your app’s redirect pages: 1. Log in to the **Detour Dashboard**. 2. Select your mobile **App** from the sidebar. 3. Open the **Redirect page** settings, and pick the **Redirect Page**, **Copy Link Page** or **App Preview** tab. *** ## Custom redirect page [Section titled “Custom redirect page”](#custom-redirect-page) The redirect page renders in the user’s browser while the platform detects the device OS, records the click, and gathers fingerprint data. * **Best for** — loading spinners, your app logo, or a short line like *“Opening the app…”* * **How it is served** — inside a sandboxed iframe. **JavaScript is disabled**, so only HTML markup and inline CSS are allowed. *** ## Custom copy link page [Section titled “Custom copy link page”](#custom-copy-link-page) The copy link page appears only for iOS users, and only when the **Copy Link** flow is enabled for the app. Recent iOS versions block pasteboard access from a direct page redirect. Detour works around that by asking the user to copy the link before installing, which gives probabilistic matching a strong extra signal. ### Injected dialog [Section titled “Injected dialog”](#injected-dialog) When you customize the HTML for the Copy Link page: * Your HTML serves as the **background layout** for the page. * Detour automatically injects and displays the interactive **Copy Link Dialog Box** on top of your HTML. * The dialog box contains buttons to copy the link, install the app, and open the app store. ### Dialog settings [Section titled “Dialog settings”](#dialog-settings) Because the dialog is injected dynamically, you configure its appearance in the Dashboard rather than in your markup: * **Banner Theme** — Switch between **Light** and **Dark** themes to match your background. * **Banner Language** — Choose from 11 supported languages (English, Polski, Español, Français, Deutsch, Italiano, Português, Nederlands, 日本語, 한국어, 中文) to automatically localize the copy-link prompt. *** ## App preview page [Section titled “App preview page”](#app-preview-page) The app preview is an optional interstitial: instead of sending a mobile visitor straight to the store, Detour shows a page you control, and the visitor continues with a tap on a **Continue** button. Use it to say what the app is before asking someone to install it. It is off by default. Turn on **Show App Preview** on the **App Preview** tab. * **Who sees it** — mobile visitors only. Desktop and web visitors always go straight to your fallback URL, preview or not. * **Configured but off** — nothing is shown and visitors are redirected immediately. The Dashboard flags this rather than failing silently. Copy Link takes precedence on iOS The copy link screen and the app preview are alternatives, and the copy link one is shown first. With **Copy Link** enabled, only Android visitors see the app preview. Turn Copy Link off to show it on iOS too. ### Built from a template [Section titled “Built from a template”](#built-from-a-template) The default source is a form, so no markup is needed: | Field | Limit | | ----------------------------------- | --------------- | | Headline | 80 characters | | Body | 280 characters | | App icon URL | 2048 characters | | Button text | 64 characters | | Background, text, and button colors | hex | ### Built from custom HTML [Section titled “Built from custom HTML”](#built-from-custom-html) On **Scale and Enterprise** the page can be raw HTML instead, with the same [limits](#limits-and-security) as the other two pages. The Continue button is always injected by Detour, and you only choose where it goes: | Marker | Result | | ----------------- | --------------------------------------------------------------------------- | | `{{CTA}}` | The button is rendered in its place and flows with your layout. | | `id="detour-cta"` | The element is replaced whole by the button. | | neither | The button is appended as a fixed overlay pinned to the bottom of the page. | Its color and label come from the same **Button text** and **Button** color fields as the template, so they stay configurable without editing markup. Plan downgrades A plan that no longer includes custom HTML keeps the saved markup untouched but stops serving it. Visitors are served the template instead, and the HTML starts rendering again after an upgrade. With no template filled in, no app preview is shown at all. *** ## Limits and security [Section titled “Limits and security”](#limits-and-security) All three pages are subject to the same rules, which limit their size and the resources they may load: * **Size** — the HTML must not exceed **50 KB** (51,200 characters). * **Scripts** — ` ``` Two properties of the snippet: * **It carries the app hash, not a link URL.** The link base, the layouts and the targeting rules all arrive from Detour on each page view, so changing a banner in the Dashboard takes effect without changing your site again. * **It loads from your organization’s own link subdomain**, not from a shared host used by every Detour customer. A shared marketing-script host is the kind of host privacy blocklists tend to cover, and a per-organization one is not. The script is `async` and every part of it is guarded. If anything fails, no banner is shown and your page is unaffected. Smart banners and custom domains A banner tap opens the app’s ordinary Detour link, so it uses whichever host the app currently serves links from, a [custom domain](/docs/platform/advanced/custom-domain) included. The script tag is the exception: it always loads from your organization’s `.godetour.link` subdomain, and stays there when a custom domain is attached, unassigned or reverted. The snippet therefore does not need to be changed. ### Restrict where it may run [Section titled “Restrict where it may run”](#restrict-where-it-may-run) By default the banner renders **anywhere the snippet is pasted**, including sites you did not paste it on yourself. Narrow that with the **Websites allowed to show it** list on the same page. Subdomains count as the same website, so listing `example.com` also covers `www.example.com`. The check runs in the browser before anything is rendered, and again on the server before an impression is recorded. *** ## Build a banner [Section titled “Build a banner”](#build-a-banner) Each banner is built in a four-step wizard, and an app can have several. Statuses are **Draft**, **Live**, **Paused**, plus **Scheduled** and **Ended**, which are derived from a live banner’s date window. ### 1. Who sees it [Section titled “1. Who sees it”](#1-who-sees-it) | Setting | Options | | ---------------- | ------------------------------------------------ | | Pages | Every page, or only pages that match your rules | | Device | The platforms the banner can show on | | Language | Any, English, Polish, German, Spanish, or French | | Country | Worldwide, EU, or US | | Desktop behavior | Nothing, a link, or a QR code | **Page rules** check the full address, path, and query, with one of the operators **contains**, **does not contain**, **is exactly** or **starts with**. **Language** is read from the browser. The region subtag is ignored. **Desktop behavior** applies because a computer cannot install the app. A link can point to a URL of your own or to the store’s web page. A QR banner has no tap target, because the visitor continues on their phone. ### 2. Where it goes [Section titled “2. Where it goes”](#2-where-it-goes) Choose the screen a tap opens: | Destination | What opens | | -------------------- | ------------------------------------------- | | Same page in the app | The app screen mapped from the website path | | A fixed screen | The same screen for every tap | For **Same page in the app**, each `*` in a website path captures a part, and the captures fill the `*` in the app screen in order: ```txt Rule: /products/* → /product/* Result: /products/shoes → /product/shoes ``` Set an **unmatched screen** for addresses that no rule covers. You can also set: * **Campaign** — your own label, sent as `utm_campaign`. * **Extra link parameters** — added to the link. Every banner link is your app’s link plus the screen, with three parameters that Detour adds: | Parameter | Value | | -------------- | ----------------------------------------------------- | | `utm_source` | Always `smart_banner` | | `utm_campaign` | The campaign field, or the banner name if it is empty | | `dtb` | The banner ID, used to count taps | These three parameters are reserved Extra link parameters cannot overwrite them, and a screen that sets one loses that value. Taps are counted by `dtb`, not `utm_campaign`, because a campaign label can be renamed and reused across banners. ### 3. How it looks [Section titled “3. How it looks”](#3-how-it-looks) Four ready-made layouts, **top**, **bottom**, **floating**, and **interstitial**, each with headline, subline, button text, app icon, colors, corner radius, text size and shadow. The editor preview renders the banner exactly as visitors see it. The banner mounts into a shadow root on your page: your stylesheet does not apply inside it, and its styles do not apply to your site. The interstitial layout locks page scrolling while it is open and always shows a visible close control. #### Custom HTML [Section titled “Custom HTML”](#custom-html) On **Scale and Enterprise**, a banner can be written as raw HTML instead. Two markers connect your markup to Detour: | Marker | Meaning | | -------------------------- | --------------------------------------------------------------------------- | | `{{LINK}}` | Replaced with the resolved link. Every occurrence, so put it in `href="…"`. | | `id="detour-banner-close"` | Any element carrying this id becomes the dismiss control. | The same limits as the [custom redirect pages](/docs/platform/advanced/custom-html-redirects#limits-and-security) apply: **50 KB** maximum, no scripts, and external assets served over HTTPS from your own host. If the plan later drops below Scale, the saved markup is kept but not served. The banner falls back to its template, and starts rendering again on an upgrade. A custom-HTML banner with empty markup is skipped rather than shown as an empty box. ### 4. When it runs [Section titled “4. When it runs”](#4-when-it-runs) * **Schedule** — always, or between two dates. Outside the window a live banner reports as **Scheduled** or **Ended** and is not served. * **Dismissal period** — how long a dismissal lasts, either the session, an hour, a day, a week, or forever. * **Frequency cap** — show it at most once, three times, five times, or without a cap. Both are remembered in the visitor’s own browser storage. In a browser that blocks storage the banner is shown again. Nothing else is affected. *** ## How one banner is chosen [Section titled “How one banner is chosen”](#how-one-banner-is-chosen) Banners are evaluated in the order you arrange them and **the first match wins**. A visitor sees one banner or none, never two. A banner is skipped when its rules do not match, when the visitor dismissed it and the dismissal has not expired, or when the frequency cap is reached. Country targeting fails closed If the country lookup is unavailable, a banner narrowed to a region is **not** shown. Showing it worldwide instead would deliver the banner outside the configured region, with no signal in the Dashboard. On a single-page app, call `window.DetourBanner.refresh()` after a route change so page rules and the destination are re-evaluated for the new address. *** ## Impressions, taps, and billing [Section titled “Impressions, taps, and billing”](#impressions-taps-and-billing) The **Seen · 30 days** column in the banner list shows each banner’s impressions over the last 30 days, and the percentage of them that were tapped. For a date range you choose, and for installs, see the [Smart banners analytics view](/docs/platform/fundamentals/analytics#smart-banners). * **Impressions are free.** They are reported as a daily aggregate and are not metered. * **A tap costs a click**, through the ordinary redirect flow, the same click your plan limit and analytics already count. See [Click Handling & Redirect Flow](/docs/platform/architecture/click-handling). Taps are counted from the `dtb` parameter on the click, so they also appear in your link analytics alongside every other parameter breakdown. ## Related pages [Section titled “Related pages”](#related-pages) [Custom HTML Redirects ](/docs/platform/advanced/custom-html-redirects/)Restyle the pages between the tap and the store. [Custom Domain ](/docs/platform/advanced/custom-domain/)Serve the links a banner tap opens from your own domain. [Analytics ](/docs/platform/fundamentals/analytics/)Where banner taps show up in link analytics. [Billing & Payments ](/docs/platform/fundamentals/billing/)Which plans include smart banners and custom HTML. # Limitations & Known Issues > Known limitations and edge cases of deferred deep linking with Detour, and how to work around them. Deferred linking depends on the signals the platforms expose, and those signals can be masked, stripped or absent. The sections below describe the edge cases seen in real traffic, their cause, and their effect on attribution. *** ## Private Relay and VPNs [Section titled “Private Relay and VPNs”](#private-relay-and-vpns) Several kinds of service change the client’s IP address before it reaches Detour: * iCloud Private Relay * consumer VPNs * corporate proxies Private Relay is the most common. It masks the real IP and presents a relay IP instead, sometimes with a coarse region attached. Impact * IP equality is one of the strongest signals in probabilistic matching. A missing or altered IP lowers the total score and produces **false negatives**, that is matches that should have happened and did not. * When the click carried one IP and the first open carries a relay IP, the equality check fails outright. Combined with otherwise weak signals, the candidate drops below the threshold. ### How Private Relay works [Section titled “How Private Relay works”](#how-private-relay-works) **iCloud Private Relay** is an opt-in iCloud+ service that proxies and encrypts traffic from Apple devices, so that no single party sees both who the user is and what sites they visit. Traffic takes two hops, an Apple-operated ingress relay and then a third-party egress relay, which masks the real IP address and DNS lookups while preserving the user’s approximate region. * **Who has it** — iCloud+ subscribers who turn it on in iCloud settings. * **What it covers** — web browsing in Safari, plus other unencrypted app traffic the system can proxy. It runs in the OS networking stack and is not visible to the user once enabled. Improve iOS match rates Enable the copy-link feature for iOS users. The clipboard signal is not affected by Private Relay and raises the chance of a successful match. *** ## iOS pasteboard permission [Section titled “iOS pasteboard permission”](#ios-pasteboard-permission) Clipboard contents are a strong probabilistic signal, but iOS restricts access to them. Reading the clipboard triggers a system prompt in some contexts, and users can decline it. In other flows the prompt never appears at all. Impact * Without paste access, the `pastedLink` signal is missing and the score drops accordingly. * The copy-to-install flow depends on the user actually copying the link. Users who skip that step gain nothing from it. *** ## Redirect chains and referrer loss [Section titled “Redirect chains and referrer loss”](#redirect-chains-and-referrer-loss) Any service between the tap and Detour can rewrite the URL. Ad networks, trackers, and link shorteners routinely strip query parameters or referrer information, and on Android `click_id` is carried in those parameters. Impact * A missing `click_id` at install time rules out deterministic matching for that click. Detour falls back to probabilistic matching, which is less reliable. * A redirect can also change the landing domain, in which case `assetlinks` verification runs against a different host. *** ## Play Store referrer missing [Section titled “Play Store referrer missing”](#play-store-referrer-missing) The Android Install Referrer usually delivers `referrer` data. It does not in these cases: * The device has an enterprise account configured. Referrer data is reset automatically, even when a non-enterprise account is currently selected. This is a known Android platform limitation, tracked in the [Google Issue Tracker](https://issuetracker.google.com/issues/445826632). * The link opened the Play Store website rather than the Play Store app. * The install did not come from a normal store flow: pre-installs, sideloads, some Play Store variants, and internal or external test tracks. * The Play Store and the app were not interacted with in the expected order, which can delay or lose the referrer. Impact Deterministic matching fails when the referrer is missing or malformed, and the install falls back to probabilistic matching. *** ## In-app browsers block the App Store [Section titled “In-app browsers block the App Store”](#in-app-browsers-block-the-app-store) Instagram, Threads, Facebook, and TikTok open links in their own webview on iOS, and that webview refuses App Store URLs. A link placed in a bio or a caption is opened there by default, so a deferred link that redirects to the App Store stops at that point. The Meta webviews end on a blank page. TikTok shows an error toast. Only iOS App Store destinations are affected. Android in-app browsers follow Play Store URLs normally, and a link whose destination is a web page loads in all of these webviews. Detour treats these apps differently: * **Instagram and Threads** — Detour opens the App Store URL outside the webview, through another browser or the App Store app. If no hand-off goes through, the visitor stays on the page with a button to continue manually. * **Facebook** — Detour first tries to open the installed app, through Safari for a link in a post or an ad, and through an **Open app** button for a link in a bio. Without the app, the visitor then reaches the App Store. If the visitor declines the Safari prompt, a tap on the **App Store** button opens the App Store. * **Messenger** — the automatic redirect does not open the App Store. Detour shows the **App Store** button, and a tap opens the App Store. * **TikTok** — no hand-off works. Detour skips the attempts, **App Preview**, and the iOS copy-link page, and tells the visitor to open the page in their own browser from the `•••` menu. Impact * A visitor who stays inside the webview never reaches the store, so that install does not happen. * In Instagram, Threads, Messenger, and TikTok, the click is recorded before the redirect runs. Click counts therefore do not fall when this happens, and only the install count does. * In Facebook, and for a link with the open-app flag, Detour records no click on page load. Your app records the click, or the page that sends the visitor to the store does. A visitor who leaves without opening the app or tapping a button is not counted. * A visitor who leaves the webview and installs later can still be matched, because the recorded click stays available for the match window. For the hand-off in each app, see [Links in Social Apps](/docs/platform/architecture/social-app-links). *** ## Frozen OS version on iOS 26 [Section titled “Frozen OS version on iOS 26”](#frozen-os-version-on-ios-26) Safari on iOS 26 reports `CPU iPhone OS 18_7` in the user agent regardless of the real iOS version. The device signal on iOS is exactly that comparison: the OS version from the click’s user agent against the `systemVersion` the SDK reports. A frozen token makes it fail. Detour reads the true version from Safari’s `Version/` token instead. Impact A browser that sends the frozen token and no `Version/` token scores **0** for the device signal, and iOS has no second device check to fall back on (see [Scoring weights](/docs/platform/architecture/matching#scoring-weights)). The candidate has to clear the threshold on its remaining signals alone. Workaround Enable the copy-link feature. On such browsers the clipboard signal is usually what raises the score above the threshold. *** ## False positives and false negatives [Section titled “False positives and false negatives”](#false-positives-and-false-negatives) Probabilistic matching is a heuristic. It combines signals and accepts anything above a threshold, so there are two kinds of error: * **False negative** — a correct match is missed, because the score fell below the threshold. * **False positive** — the wrong click is matched, because its score cleared the threshold. Impact * A false negative loses the deferred attribution, and the user does not reach the content the link pointed to. * A false positive corrupts your analytics, and the user reaches the wrong content. *** ## Shared devices and shared IPs [Section titled “Shared devices and shared IPs”](#shared-devices-and-shared-ips) Shared households, corporate devices, and NATs all produce similar fingerprints and identical IPs for several distinct users. Impact * Two users clicking different links on the same device or network within a short window raise the risk of a false positive. * Attribution on shared devices is hard to interpret, even when it is technically correct. # Click Handling & Redirect Flow > The Detour click and redirect flow, from link click through store redirect to in-app delivery. When a user opens a Detour link, the platform determines which app the link belongs to, records the click, and redirects the user. This page describes that flow and what you can configure. *** ## High-level flow [Section titled “High-level flow”](#high-level-flow) 1. The user opens a Detour link in a browser. The link has the form `https:////...`. 2. Detour records the click. Either Detour collects the device fingerprint right away, or the user sees the iOS copy-link page and decides whether to copy the link to the clipboard. You choose which one in the iOS configuration in the Dashboard. With `#detour_open_app=true` in the Meta in-app browsers that support it, Detour tries to open the installed app first and does not record the click on page load. Facebook gets this without the flag. See [Links in Social Apps](/docs/platform/architecture/social-app-links). 3. If **App Preview** is on, the user sees the preview page. The user taps to continue instead of being redirected right away. 4. Detour redirects the user. Mobile users go to the App Store or Google Play. Desktop users go to the fallback URL. 5. The user installs and opens the app. On first open, the SDK calls the match endpoint. If the click is matched, the SDK receives the link data. You can replace each page in this flow with your own HTML. See [Custom HTML Redirects](/docs/platform/advanced/custom-html-redirects). ## Click handling [Section titled “Click handling”](#click-handling) Detour recognizes deferred links on platform hosts such as `*.godetour.link`, as well as on any configured custom domain. It looks up the app configuration attached to the link and redirects the user according to device OS and runtime context. Only the base segment, the platform-generated hash, is canonical for that lookup. Anything you append to it is free-form: * extra path segments * URL search parameters Neither affects deferred-link resolution, and both are delivered to your app in the returned link data. One app can therefore serve many campaign and content routes under a single base hash. ## Reserved link parameters [Section titled “Reserved link parameters”](#reserved-link-parameters) Most search parameters on a link are free-form and are passed to your app. A few names are read by the platform itself: | Parameter | Read by | Effect | | --------------------------------------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------- | | `ppid` | the iOS store redirect | Sends the visitor to a specific App Store **custom product page** instead of the default one. | | `pt`, `ct`, `mt` | the iOS store redirect | Forwarded to the App Store URL for campaign reports in App Store Connect. | | `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content` | the Android store redirect | Forwarded inside the `referrer` of the Play Store URL for campaign reports in Play Console. | | `dtb` | [Smart Banners](/docs/platform/advanced/smart-banners) | Identifies which banner a tap came from, for banner statistics. | ### App Store custom product pages (`ppid`) [Section titled “App Store custom product pages (ppid)”](#app-store-custom-product-pages-ppid) Append `?ppid=` to a link and iOS installs from it open that custom product page, with different screenshots and promotional text per campaign, without a separate app or a separate link base: ```plaintext https://acme-corp.godetour.link/x9y8z7/summer?ppid=1a2b3c4d-5e6f-7890-abcd-ef1234567890 ``` The id is the UUID App Store Connect assigns to the product page. Constraints: * **iOS only.** Play Store custom store listings are selected by a different mechanism, so the Android URL never carries it. * **A malformed id is dropped**, not passed through. The App Store would silently serve the default page for an invalid id, which looks like a working campaign showing the wrong screenshots. * It is an ordinary query parameter, so it also appears in the per-parameter click breakdown in [Analytics](/docs/platform/fundamentals/analytics). ### Store campaign parameters [Section titled “Store campaign parameters”](#store-campaign-parameters) Only App Store Connect and Play Console report what happens on the store page, such as product page views and downloads. To see those numbers per campaign, add the campaign parameters of App Store Connect and Google Play to the link: ```txt https://your-org.godetour.link//summer?pt=118032&ct=summer-email&utm_source=newsletter&utm_campaign=summer-email ``` * **iOS** — Detour forwards `pt`, `ct`, and `mt` to the App Store URL. Copy the values from an App Store Connect campaign link. * **Android** — Detour forwards the UTM keys inside the `referrer` parameter of the Play Store URL. Add them to the link as ordinary parameters, not inside a `referrer` of your own. Detour puts them in `referrer` next to its own `click_id`. * Each store URL carries only its own keys, and never a key with an empty value. * Your app receives these parameters in the link data, and they appear in the per-parameter click breakdown in [Analytics](/docs/platform/fundamentals/analytics). Detour counts every click on the link, while App Store Connect and Play Console count store page views and downloads by their own rules. Compare campaigns by their ratios, not by the absolute counts. ## Runtime safeguards [Section titled “Runtime safeguards”](#runtime-safeguards) Detour includes safeguards against false attribution and redirect loops: * Automated and low-quality traffic is filtered out from attribution. * Repeated redirect attempts are guarded to avoid redirect loops. * If the redirect target cannot be resolved, the user sees a fallback failure view. ### Return to the web destination [Section titled “Return to the web destination”](#return-to-the-web-destination) After a store redirect, Detour detects the visitor returning to the page, which happens when the store was dismissed, the app was not installed, or the browser restored the page from its back/forward cache. Instead of leaving the visitor on a redirect page that does nothing, Detour sends them to the web destination. ### In-app browsers on iOS [Section titled “In-app browsers on iOS”](#in-app-browsers-on-ios) Instagram, Threads, Facebook, and TikTok in-app browsers on iOS stopped loading App Store URLs, so a link that redirects to the store stops there. The Meta browsers end on a blank page with no error. TikTok shows an error toast. For those browsers, and only for App Store destinations, Detour changes what it does: * **Instagram and Threads** — Detour opens the App Store URL outside the webview, through another browser or the App Store app. If the hand-off does not go through, the visitor stays on the page with a button to continue manually. * **Facebook** — Detour first tries to open the installed app, through Safari for a link in a post or an ad, and through an **Open app** button for a link in a bio. Without the app, the visitor then reaches the App Store. * **Messenger** — the automatic redirect does not open the App Store, so Detour shows the **App Store** button. A tap opens the App Store without a hand-off. * **TikTok** — no hand-off works, so Detour does not attempt one and does not show **App Preview** or the iOS copy-link page. The page tells the visitor to open it in their own browser from the `•••` menu. For what this costs in attribution, see [In-app browsers block the App Store](/docs/platform/architecture/architecture-limitations#in-app-browsers-block-the-app-store). For what each app does, and for opening the installed app from these browsers, see [Links in Social Apps](/docs/platform/architecture/social-app-links). Nothing else changes: * Safari, Chrome, desktop, and every Android in-app browser keep the instant redirect. The exceptions are a link with the `#detour_open_app=true` flag in a Meta in-app browser and every link in Facebook on Android. See [Links in Social Apps](/docs/platform/architecture/social-app-links). * Android in-app browsers open Play Store URLs, so they need no hand-off. A hand-off would also drop the install referrer, which carries `click_id`. For host-specific behavior and `/.well-known` implications on custom domains, see [Custom Domain](/docs/platform/advanced/custom-domain). ## Forwarding query parameters [Section titled “Forwarding query parameters”](#forwarding-query-parameters) An app has one desktop redirect URL, shared by all of its deferred links. The search parameters on the clicked link can still be forwarded to that URL. Pick a strategy in **Link settings**: * **Pass all parameters** — every search param on the deferred link is forwarded to the desktop fallback. * **Pass specific parameters** — only the params you name are passed. * **Don’t pass parameters** — the fallback URL is used as-is. For matching details after first app open, see [Matching](/docs/platform/architecture/matching). For platform-level setup context, see [Dashboard Walkthrough](/docs/platform/fundamentals/dashboard). # Matching > How Detour matches a first app open to a previously recorded click, and how scoring and thresholds affect attribution. Attribution answers one question: does this first app open belong to a click Detour recorded a few minutes ago? The answer is either an exact match or a score. This page describes how the server decides and which settings you can change. ## How matching starts [Section titled “How matching starts”](#how-matching-starts) On first app open the SDK calls the Detour match API. The server picks one of two matching modes based on what the payload contains. ``` flowchart TD A[First app open] --> B{clickId present?} B -->|Yes| C[Deterministic lookup] B -->|No| E[Probabilistic scoring] C --> D{Click exists
and unmatched?} E --> F[Load unmatched clicks
in the time window] F --> G[Score candidates,
keep the highest] G --> H{Score above
the threshold?} D -->|Yes| M1[Return link URL] D -->|No| N1[Return 404] H -->|Yes| M2[Return link URL] H -->|No| N2[Return 404] ``` A returned link URL marks the install as non-organic. A 404 stores it as organic. The default score threshold is 850. ## Matching modes [Section titled “Matching modes”](#matching-modes) * **Deterministic (preferred)** — an exact `clickId` is present, from the Android install referrer. The server looks it up and returns the link if that click exists and is still unmatched. The lookup is exact. * **Probabilistic (fallback)** — no `clickId`, which is the norm on iOS. Detour scores the device fingerprint sent by the SDK against recent unmatched click beacons. ## Probabilistic payload [Section titled “Probabilistic payload”](#probabilistic-payload) The SDK sends these fields when there is no `clickId`: | Field | Type | Meaning | | ---------------------------------------------------- | --------------------------- | ---------------------------------------------------------------------- | | `platform`, `model`, `manufacturer`, `systemVersion` | `string` | Device signature | | `screenWidth`, `screenHeight`, `scale` | `number` | Screen size and pixel ratio | | `locale` | `{ languageTag: string }[]` | Device languages. `languageTag` is scored. | | `timezone` | `string \| null` | Device timezone | | `userAgent` | `string` | Raw user agent | | `timestamp` | `number` | Capture time in Unix milliseconds, checked against the match window | | `pastedLink` | `string`, optional | iOS only. Clipboard content, compared with the click URL and its token | Clipboard is an iOS-only signal Every SDK sends `pastedLink` as `null` on Android. Reading the clipboard there raises a system toast and is restricted to foreground apps, so the Android and Flutter SDKs never attempt it and the React Native SDK gates the read on `Platform.OS === "ios"`. An Android probabilistic match is therefore scored without the 350-point pasteboard signal, so the Play Install Referrer is the main deterministic signal on Android. ## Scoring weights [Section titled “Scoring weights”](#scoring-weights) The server scores each candidate click using separate scoring functions. The weights used by Detour today: | Signal | Weight | Scored on | | --------------------------------- | -----: | ------------- | | IP exact | 500 | iOS + Android | | Model + system version exact | 450 | Android \* | | iOS system version match | 350 | iOS | | User-agent device signature match | 350 | Android | | Pasteboard URL/token match | 350 | iOS | | Timezone | 200 | iOS + Android | | Screen match | 200 | iOS + Android | | Language match | 100 | iOS + Android | \*The model and system version come from high-entropy client hints. The check itself is platform-agnostic, but WebKit does not implement those hints, so only Android browsers supply the values. The highest weights go to the signals that are hardest to spoof or to match by coincidence: IP, device signature, and clipboard. Weaker signals such as language add only a small score. Rules that change the totals at runtime: * Only one device signal is scored per candidate. `Model + system version` is used when the browser supplies both. On iOS it never does, so the system version parsed from the click’s user agent is scored instead. The user-agent device signature is the fallback everywhere else. The highest reachable total is therefore **1700 on iOS** and **1450 on Android**. * Screen comparison allows a tolerance of `±1` on width and height, `±0.01` on scale. * The iOS pasteboard score has two tiers: * `350` when the token matches and the pasted URL starts with the click URL. * `175` when only the URL prefix matches. ## Threshold and time window [Section titled “Threshold and time window”](#threshold-and-time-window) Both values are app-level settings from `Link settings → Matching configuration`: * **Matching threshold** — default **850**, allowed range `700..1200` * **Matching time window** — default **15 minutes**, allowed range `5..180` minutes Customizing the threshold The default of **850** balances accuracy and recall. Change it when you need a different trade-off: * **Lower** (for example 700) finds more matches but attributes more installs to the wrong link. Usually acceptable for general marketing links. * **Higher** (for example 1000) is stricter and misses some legitimate matches. Prefer it when links point at sensitive content. Clicks outside the window are ignored. The window is measured between the click timestamp and the fingerprint timestamp. The default of **15 minutes** covers a normal install and limits the number of false positives. Customizing the time window The default of **15 minutes** is configurable. * **Shorter** (5–10 minutes) raises confidence, because the click and the first open are close in time. Slow downloads then fall outside the window and are not matched. * **Longer** (30–60 minutes) matches users who were interrupted during the install, but it enlarges the candidate pool and raises the false-positive rate. ## Candidate selection [Section titled “Candidate selection”](#candidate-selection) In probabilistic mode the server: 1. Loads the app’s unmatched clicks inside the configured time window. 2. Scores every candidate. 3. Keeps the highest score. 4. Breaks ties in favor of the newer click. 5. Accepts the highest-scoring candidate only if its score is `>= threshold`. ## After a match [Section titled “After a match”](#after-a-match) A successful match does three things: * marks the click as matched, * stores the install as non-organic in the analytics install stream, * returns the resolved link URL. With no match, the install is recorded as organic and the API returns `404`. ## Example scores [Section titled “Example scores”](#example-scores) * **iOS — IP + exact pasteboard token and URL** — `500 + 350 = 850`. Accepted at the default threshold. * **Android — IP + user-agent device signature** — `500 + 350 = 850`. Accepted at the default threshold. * **Timezone + language + screen** — `200 + 100 + 200 = 500`. Rejected. ## Tuning [Section titled “Tuning”](#tuning) Start with the defaults and change them only after you have seen real traffic in analytics. If too many matches are missed, lower the threshold or extend the time window. If you see cross-attribution in dense traffic, raise the threshold or shorten the window. For practical edge cases that affect tuning decisions, see [Limitations & Known Issues](/docs/platform/architecture/architecture-limitations). ## Related pages [Section titled “Related pages”](#related-pages) [Click Handling & Redirect Flow ](/docs/platform/architecture/click-handling/)What happens when a user clicks a link. [Limitations & Known Issues ](/docs/platform/architecture/architecture-limitations/)Platform constraints and known issues. [Analytics ](/docs/platform/fundamentals/analytics/)Privacy-aware link and event analytics. [Getting Started with Detour ](/docs/platform/fundamentals/getting-started/)Set up your first app from account to first link. # Links in Social Apps > How Detour links behave in the in-app browsers of Instagram, Threads, Facebook, Messenger, and TikTok, and how to open the installed app from them. Instagram, Threads, Facebook, Messenger, and TikTok open links in their own in-app browser. There, a link does not always open your installed app or the store. This page describes what Detour does in each of these apps and how the `#detour_open_app=true` flag opens the installed app. ## The open-app flag [Section titled “The open-app flag”](#the-open-app-flag) In some in-app browsers, a Universal Link or an App Link does not open your installed app, and the visitor goes to the store instead. Add `#detour_open_app=true` to the end of a link to make Detour try to open the installed app first: ```txt https://your-org.godetour.link//promo#detour_open_app=true ``` Instagram changes the link in a bio Instagram on iOS saves the flag in a bio as `%23detour_open_app%3Dtrue` and adds its own parameters after it. Detour restores the flag, so the link works as you pasted it. The flag works in these apps: * **Instagram and Threads** — on iOS and Android. * **Messenger** — on Android. In every other app and browser, Detour ignores the flag, so you can use the same link everywhere. Facebook, and Messenger on iOS, open the installed app without the flag. Your app does not receive the flag in the link data. In the apps where the flag works, a link with it shows the Detour redirect page with a store button instead of **App Preview**, the iOS copy-link page, or a [custom HTML](/docs/platform/advanced/custom-html-redirects) page. A visitor without your app either taps the store button there or goes to the browser, which shows these pages as you configured them. The sections below say which one each app does. ## Instagram and Threads [Section titled “Instagram and Threads”](#instagram-and-threads) Instagram and Threads behave the same. * **iOS, app installed** — without the flag, the visitor goes to the App Store. With the flag, the app opens the link outside its in-app browser, and iOS opens your app. * **iOS, app not installed** — Detour opens the App Store outside the app. Instagram and Threads do not open App Store links themselves. Detour opens the App Store from the redirect page, from the iOS copy-link page, and from **Continue** on **App Preview**. With the flag, the link opens in the browser, which opens the App Store. * **Android, app installed** — without the flag, Detour redirects to Google Play. With the flag, the app asks the visitor to leave it, then your app opens. * **Android, app not installed** — Detour redirects to Google Play. With the flag, the visitor stays on the page and taps **Google Play**. Meta can change this behavior Meta does not document how its apps open other apps, and can change it without notice. If Detour cannot open the App Store, the visitor stays on the page with an **App Store** button. ## Facebook [Section titled “Facebook”](#facebook) Facebook needs no flag. Detour tries to open your app from every link in Facebook. Facebook removes the flag from a link, so a link with the flag works there too. * **iOS, link in a post or an ad** — Facebook asks the visitor to open the link in Safari. Safari opens your app, or the App Store when the app is not installed. If the visitor declines, the page shows an **App Store** button. * **iOS, link in a profile bio** — the page shows an **Open app** button with this line: ```txt Tap the button below to open the app. ``` A tap opens your app, or the App Store when the app is not installed. * **Android** — Facebook asks the visitor to leave the app. Then your app opens. When the app is not installed, the link opens in the default browser, which redirects to Google Play. If the visitor declines, the page shows a **Google Play** button. Facebook lowercases a link in a bio Facebook saves a link in a profile bio in lowercase. Detour restores the case of the app hash, so the link opens. The rest of the path and the parameters reach your app in lowercase, so match them without regard to case. Facebook on iOS loads the link page even when your app is installed. With **App Preview** or the iOS copy-link page turned on, the visitor sees that page first. A tap on **Continue** or on a copy-link button opens your app, or the App Store when the app is not installed. Facebook on Android does not show **App Preview** or a [custom HTML](/docs/platform/advanced/custom-html-redirects) page. The visitor sees the Detour redirect page with a **Google Play** button. ## Messenger [Section titled “Messenger”](#messenger) * **iOS, app installed** — your app opens. * **iOS, app not installed** — Messenger does not open the App Store on its own. After a few seconds, the page shows an **App Store** button, and a tap opens the App Store. * **Android, app installed** — without the flag, Detour redirects to Google Play. With the flag, Messenger asks the visitor to leave the app, then your app opens. * **Android, app not installed** — Detour redirects to Google Play. With the flag, the link opens in the default browser, which redirects to Google Play. On iOS, Messenger needs no flag, and a link with the flag works the same. ## TikTok [Section titled “TikTok”](#tiktok) * **iOS** — TikTok does not open the App Store from a link in a bio. After **3 seconds**, the redirect page shows these lines above the **App Store** button: ```txt If the App Store didn't open, this app is blocking it. Tap ••• in the top right, then open this page in your browser. ``` The text follows the browser language: `en`, `pl`, `es`, `fr`, `de`, `it`, `pt`, `nl`, `ja`, `ko`, or `zh`. Other languages get English. From a direct message, the App Store opens. * **Android** — Detour redirects to Google Play. TikTok on iOS does not show **App Preview** or the iOS copy-link page. The visitor sees the redirect page instead, also for a link from a direct message. In the browser, these pages show as you configured them. ## Behavior by app [Section titled “Behavior by app”](#behavior-by-app) The tables below compare the apps for a link that leads to the store. Outside in-app browsers, a Universal Link or an App Link opens your installed app, and a visitor without the app goes to the store. ### Your app is installed [Section titled “Your app is installed”](#your-app-is-installed) * iOS | App | Default | With the flag | | ------------------ | ------------------------------------------------------------------------------------------------------------------------ | -------------------- | | Instagram, Threads | The app does not open. Detour opens the App Store. | The app opens. | | Facebook1 | From a post or an ad, Facebook asks to open Safari, then the app opens. From a bio, a tap on **Open app** opens the app. | Same as the default. | | Messenger | The app opens. | Same as the default. | 1 With **App Preview** or the iOS copy-link page, the visitor taps **Continue** or a copy-link button instead. See [Facebook](#facebook). * Android | App | Default | With the flag | | ----------------------------- | ------------------------------------------------------- | ---------------------------------------------- | | Facebook | Facebook asks to leave the app, then the app opens. | Same as the default. | | Instagram, Threads, Messenger | The app does not open. Detour redirects to Google Play. | The app asks to leave it, then your app opens. | ### Your app is not installed [Section titled “Your app is not installed”](#your-app-is-not-installed) * iOS | App | Default | With the flag | | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | | Instagram, Threads | Detour opens the App Store. | The link opens in the browser, which opens the App Store. | | Facebook1 | From a post or an ad, Facebook asks to open Safari, then the App Store opens. From a bio, a tap on **Open app** opens the App Store. | Same as the default. | | Messenger | A tap on **App Store** opens the App Store. | Same as the default. | | TikTok | From a bio, the App Store does not open, and Detour asks the visitor to open the page in a browser. From a direct message, the App Store opens. | Same as the default. | 1 With **App Preview** or the iOS copy-link page, the visitor taps **Continue** or a copy-link button instead. See [Facebook](#facebook). * Android | App | Default | With the flag | | ------------------ | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | | Facebook | Facebook asks to leave the app, then the default browser opens the link and redirects to Google Play. | Same as the default. | | Instagram, Threads | Detour redirects to Google Play. | A tap on **Google Play** opens the store. | | Messenger | Detour redirects to Google Play. | The link opens in the default browser, which redirects to Google Play. | | TikTok | Detour redirects to Google Play. | Same as the default. | As the tables show, the flag sometimes adds one automatic step in which the link opens in the browser. In return, a Universal Link or an App Link opens your app when it is installed. ## App Preview and the iOS copy-link page [Section titled “App Preview and the iOS copy-link page”](#app-preview-and-the-ios-copy-link-page) **App Preview** and the iOS copy-link page are pages that Detour shows before the store. You turn them on in [Redirect page](/docs/platform/fundamentals/dashboard#redirect-page). Outside in-app browsers, they show only when your app is not installed, because a Universal Link or an App Link opens the installed app first. In the in-app browsers, the result is: * iOS The page is App Preview or the copy-link page, whichever is on. With both on, iOS shows the copy-link page. | App | App installed | App not installed | | --------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------ | | Outside in-app browsers | Your app opens. Neither page shows. | The page shows. Its button opens the App Store. | | Instagram, Threads | The page shows. Its button opens the App Store. | The page shows. Its button opens the App Store. | | Instagram, Threads, with the flag | Your app opens. Neither page shows. | The link opens in the browser, which shows the page. | | Facebook | The page shows. Its button opens your app. | The page shows. Its button opens the App Store. | | Messenger | Your app opens. Neither page shows. | The page shows. Its button opens the App Store. | | TikTok | Neither page shows. | Neither page shows. Detour asks the visitor to open the link in a browser, which shows the page. | * Android Android has no copy-link page. | App | App installed | App not installed | | --------------------------------- | -------------------------------------------------- | -------------------------------------------------------------------- | | Outside in-app browsers | Your app opens. App Preview does not show. | App Preview shows. **Continue** opens Google Play. | | Instagram, Threads, Messenger | App Preview shows. **Continue** opens Google Play. | App Preview shows. **Continue** opens Google Play. | | Instagram, Threads, with the flag | After a prompt to leave the app, your app opens. | App Preview does not show. A tap on **Google Play** opens the store. | | Messenger, with the flag | After a prompt to leave the app, your app opens. | The link opens in the default browser, which shows App Preview. | | Facebook | After a prompt to leave the app, your app opens. | The link opens in the default browser, which shows App Preview. | ## Parameters added by social apps [Section titled “Parameters added by social apps”](#parameters-added-by-social-apps) Social apps can add their own parameters to a link. For example, Instagram adds `utm_*` parameters to a link in a bio, and Facebook adds `fbclid` to a link in a post or an ad. Detour treats them as ordinary link parameters: * Your app receives them in the link data. * They appear in [Analytics](/docs/platform/fundamentals/analytics). * On Android, Detour forwards the `utm_*` parameters to Google Play. It does not forward `fbclid`. A short link keeps only the parameters saved with it. Parameters that a social app adds to a short link do not reach your app, **Analytics**, or Google Play. See [Store campaign parameters](/docs/platform/architecture/click-handling#store-campaign-parameters). ## Related pages [Section titled “Related pages”](#related-pages) [Click Handling & Redirect Flow ](/docs/platform/architecture/click-handling/)What happens when a user clicks a link. [Limitations & Known Issues ](/docs/platform/architecture/architecture-limitations/)What in-app browsers cost in attribution. [Universal Links & App Links ](/docs/platform/fundamentals/universal-app-links/)The setup that opening the installed app depends on. [Custom HTML Redirects ](/docs/platform/advanced/custom-html-redirects/)The pages the open-app flag replaces in in-app browsers. # Analytics > Detour's analytics automatically track clicks, installs, and events across the Overview, Links, Events, and Smart banners Dashboard views, and can include history imported from AppsFlyer. Installing the SDK is enough to start collecting link data. Clicks, installs, and attribution are tracked without any extra calls. You read the results in the Dashboard views **Overview**, **Links**, **Events**, and **Smart banners**, and you can add history from another attribution provider in **Imports**. Links, Events, and Smart banners need a paid plan **Overview** and **Imports** are available on every plan. **Links**, **Events**, and **Smart banners** are available on Starter and above. See [Billing & Payments](/docs/platform/fundamentals/billing). ## Automatic tracking [Section titled “Automatic tracking”](#automatic-tracking) The click and install flow produces these metrics on its own: * **Clicks** — counted from recorded click beacons. * **Installs via link** — installs attributed to a matched click. * **Fallbacks** — clicks that ended on non-mobile fallback flow. * **Organic vs Non-Organic installs** — based on whether install was matched to a click. ## Dashboard views [Section titled “Dashboard views”](#dashboard-views) **Overview**, **Links**, and **Events** share the same filters, that is selected app, platform, and date range, so numbers stay comparable when you move between them. **Smart banners** shares the app and the date range. **Imports** lists import jobs, filtered by app and status. Analytics can also be delivered to your own endpoint on a schedule. See [Webhooks](/docs/platform/fundamentals/webhooks). ### Overview [Section titled “Overview”](#overview) Overview summarizes performance on one page. It shows four things: * link trend * organic vs non-organic install attribution * top event distribution * retention comparison For an app with imported installs, **Overview** also shows an **Imported installs** card. See [Imports](#imports). For cohort and N-day methodology, see [Retention](#retention). ### Links [Section titled “Links”](#links) Link-level diagnostics: day-by-day link stats, plus a breakdown by path and query parameter. Use it to compare campaign routes and see where clicks and matches come from. A filter bar narrows which clicks are counted, by **Path**, **Param name**, **Param value**, or **Param key = value**. Each filter uses **is**, **is not**, **contains**, or **starts with**. With **is not**, the matching clicks are dropped, and clicks that do not carry the parameter are kept. For the matching rules, see [Filtering link analytics](/docs/platform/advanced/api-management#filtering-link-analytics). [Smart banner](/docs/platform/advanced/smart-banners) taps are recorded like any other click. They carry `utm_source=smart_banner`, a `utm_campaign` of your choosing, and a `dtb` parameter identifying the banner, so all three appear in the parameter breakdown. The same figures are available over HTTP, with pagination and filtering, through [Export Analytics](/docs/platform/advanced/api-management#export-analytics). For the redirect and matching behavior behind these metrics, see [Click Handling & Redirect Flow](/docs/platform/architecture/click-handling). ### Events [Section titled “Events”](#events) SDK event activity in the selected window: * total event count * timeline trends per event name * drill-down tables for event totals and individual occurrences ### Smart banners [Section titled “Smart banners”](#smart-banners) Results of your [smart banners](/docs/platform/advanced/smart-banners) for the selected app and date range: * **Smart banner funnel** — **Impressions**, **Taps**, **Installs from banner**, and **Already installed opens**, with the tap and install rates, and a funnel from **Banner shown** to **Tapped** to **Opened the app**. * **Performance by configuration** — impressions, taps, and tap rate by **Source**, **Layout**, **Country**, **Language**, or **Desktop mode**. * **Impressions & taps over time** — daily impressions and taps. * **Per banner** — impressions, taps, tap rate, and installs for each banner. The view has no platform filter, because impressions are recorded per banner and day without a platform. It counts whole UTC days, from the start of the first day to the end of the last, so its taps can exceed the banner clicks on **Links** for the same range. Deleted banners are not counted. Available on Starter and above. ### Imports [Section titled “Imports”](#imports) **Imports** adds install and in-app event history exported from AppsFlyer to an app’s analytics. It carries a **Beta** badge and is available on every plan. Only an **Owner** or **Admin** can open it, and the sidebar does not show it to a **Member**. 1. Click **New import**, then pick the app and the report, **Installs** or **In-app events**. 2. Confirm the timezone the file was exported in. AppsFlyer puts it in the export filename. Detour reads the filename and warns when it does not match the report or the timezone you chose. A wrong timezone shifts every timestamp. 3. Choose the CSV file and click **Create import & start upload**. Split a large export into shorter date ranges. Rows that are already imported are dropped as duplicates, so the ranges can overlap. 4. Review the rows and click **Approve & import**. The review shows how many rows are imported, left out, or skipped, and the **Errors CSV** lists the rows left out or skipped. Nothing reaches analytics before you approve. **Cancel import** discards the job instead. **Undo import** on a finished import removes exactly the rows it added. An undo is final. To import the file again, upload it again. Imported data appears in the other views: * **Installs** — **Overview** shows an **Imported installs** card. The card breaks the installs in the date range down by **Network**, **Campaign**, **Country**, and other attribution fields. It also splits them into **After a click**, **After an impression**, and **Organic**. Only an install after a click also counts in **Clicks** and **Installs via link**, on the **Link stats** card, on **Links**, and in link exports. Every imported install counts in organic vs non-organic installs, where an install after an impression is non-organic. An imported install without a link path drops out of **Links** while a filter is active. * **In-app events** — the event views and event exports include them. Each imported event name starts with `af_`, so it stays separate from the events the SDK logs. Imported data never counts toward your monthly click limit. Detour does not match imported devices to the devices that the SDK reports, so a device that appears in both for the same period is counted twice in device counts. ## Organic vs non-organic [Section titled “Organic vs non-organic”](#organic-vs-non-organic) Every install is labeled by where it came from, which is what makes campaign effectiveness measurable: * **Non-organic** — the install was attributed to a specific click on a Detour link. * **Organic** — no prior link interaction was detected. ## Manual event tracking [Section titled “Manual event tracking”](#manual-event-tracking) Automatic tracking covers the link funnel. To track later actions, for example sign-ups or purchases, log events yourself through the SDK’s analytics API. If you are still setting up the integration, start with [Getting Started with Detour](/docs/platform/fundamentals/getting-started). * React Native ```ts import { DetourAnalytics, DetourEventNames } from "@swmansion/react-native-detour"; DetourAnalytics.logEvent(DetourEventNames.Login); DetourAnalytics.logEvent(DetourEventNames.Purchase, { value: 9.99, currency: "USD" }); DetourAnalytics.logRetention("day_7_return"); ``` * iOS ```swift import Detour DetourAnalytics.logEvent(.addToCart, data: ["sku": "abc"]) DetourAnalytics.logEvent("purchase", data: ["value": 9.99, "currency": "USD"]) DetourAnalytics.logRetention("day_7_return") ``` * Android ```kotlin import com.swmansion.detour.analytics.DetourAnalytics import com.swmansion.detour.analytics.DetourEventNames DetourAnalytics.logEvent(DetourEventNames.Login) DetourAnalytics.logEvent(DetourEventNames.Purchase, mapOf("value" to 9.99)) DetourAnalytics.logRetention("day_7_return") ``` * Flutter ```dart import 'package:detour_flutter_plugin/detour_flutter_plugin.dart'; final detour = DetourService(); await detour.start( const DetourConfig( apiKey: "", appID: "", ), ); await detour.logEvent(DetourEventName.login); await detour.logEvent(DetourEventName.purchase, data: {"value": 9.99}); await detour.logRetention("day_7_return"); ``` ## Retention [Section titled “Retention”](#retention) Detour retention is **event-based**. It is calculated from SDK calls to `logRetention("")`, grouped by retention event name. ### Data model [Section titled “Data model”](#data-model) * Retention events are stored in analytics as a dedicated retention stream. * They are tied to event name, device, time, and platform. * Retention data is not mixed into regular event charts like Top Events and Events timeline. ### Cohorts and N-day logic [Section titled “Cohorts and N-day logic”](#cohorts-and-n-day-logic) * A cohort is the group of devices that triggered a given retention event for the first time in the selected date range. * `Day 0` is the first occurrence day, and later points (`Day 1`, `Day 2`, …) show return behavior on following days. * Retention is tracked up to **30 days** (`Day 0` to `Day 30`). * Future dates are excluded to avoid artificial 0% drops on incomplete days. * Platform filtering works the same way as in other analytics views. ### Metrics [Section titled “Metrics”](#metrics) Retention reporting shows: * how many cohort devices returned on a given day, * how large the original cohort was, * and the resulting retention percentage. When multiple cohorts are combined on one chart line, Detour uses weighted aggregation so larger cohorts have proportionally higher impact. ## Related pages [Section titled “Related pages”](#related-pages) [Dashboard Walkthrough ](/docs/platform/fundamentals/Dashboard/)The Dashboard sections and app details. [Webhooks ](/docs/platform/fundamentals/webhooks/)Receive event notifications over HTTP. [Getting Started with Detour ](/docs/platform/fundamentals/getting-started/)Set up your first app from account to first link. [Click Handling & Redirect Flow ](/docs/platform/architecture/click-handling/)What happens when a user clicks a link. [Universal Links & App Links ](/docs/platform/fundamentals/universal-app-links/)Runtime deep links on iOS and Android. # Billing & Payments > Detour billing and payments: plans, usage limits, and how metered link resolution is charged. Every Detour organization is on a plan. The plan sets how many clicks a month you get, how many apps, short links and webhooks you can create, and which features are available. Payments, invoices and card details are handled by Paddle. ## Available plans [Section titled “Available plans”](#available-plans) Free needs no payment details. Starter and Scale are bought in the Dashboard, with monthly or discounted annual billing. Enterprise limits, pricing and features are agreed individually. Write to ****. | Pricing & quotas | Free | Starter | Scale | Enterprise | | ---------------------- | ------- | ------------- | -------------- | ---------- | | Price per month | $0 | $19 | $79 | Custom | | Clicks per month | 1,500 | 70,000 | 350,000 | Custom | | Clicks past that limit | Blocked | $0.03 / 1,000 | $0.015 / 1,000 | Custom | | Apps | 1 | Unlimited | Unlimited | Unlimited | | Short links per app | 50 | Unlimited | Unlimited | Unlimited | | Active webhooks | 0 | 3 | 10 | Custom | The Overview analytics view is on every plan. The rest depends on the plan: | Capability | Free | Starter | Scale | Enterprise | | -------------------------------------------- | ---- | ------- | -------- | ---------- | | Links and Events analytics | No | Yes | Yes | Yes | | Pay-As-You-Go | No | Yes | Yes | Custom | | Smart banners | No | Yes | Yes | Yes | | Custom HTML for smart banners | No | No | Yes | Yes | | Custom HTML for the app preview page | No | No | Yes | Yes | | Custom HTML for redirect and copy link pages | Yes | Yes | Yes | Yes | | Custom domain | No | No | Yes | Yes | | Annual billing | No | Yes | Yes | Custom | | Free trial | No | 30 days | No | No | | Support | — | Basic | Priority | Dedicated | | SSO | No | No | No | Yes | A smart banner impression is free. A banner tap counts as a click toward your monthly limit. ## Free trial [Section titled “Free trial”](#free-trial) Starter includes a **30-day free trial**, once per organization. A trial cannot be reused after it ends or after the subscription is canceled. During the trial the subscription status is `trialing` and every Starter feature is available. If you do not cancel before the trial ends, the subscription continues on the paid plan and you are charged. ## Pay-As-You-Go (PAYG) [Section titled “Pay-As-You-Go (PAYG)”](#pay-as-you-go-payg) Without PAYG, clicks past your monthly limit are blocked. With PAYG, they are processed and billed at the end of the billing period at your plan’s rate from the table above. You set a **spending limit** in USD, which caps the PAYG charge per month. A limit of **$0** turns PAYG off. You are asked for the limit when you first subscribe to Starter or Scale, and you can change it at any time on the **PAYG** card in the **Billing** section. ```txt overage_clicks = total_clicks_used − base_plan_limit amount_due = (overage_clicks / 1,000) × rate_per_1k final_charge = min(amount_due, spending_limit) ``` ### When you reach the click limit [Section titled “When you reach the click limit”](#when-you-reach-the-click-limit) * **Without PAYG** — Deep link matching, for Universal Links and deferred deep links, stops. Standard redirects keep working. You get an email at 80% and at 100% of the limit. * **With PAYG** — Clicks are processed in full and charged. Once the spending limit is used up, matching stops and standard redirects keep working. Click counters reset every month. ## Plan changes [Section titled “Plan changes”](#plan-changes) | Change | Takes effect | What you pay | | ---------------- | ------------------------- | ---------------------------------------------------------------------------------------------- | | Free → paid | Immediately | A new Paddle checkout. The trial may apply if you are eligible and the selected price has one. | | Trial → paid | Immediately, trial ends | The full monthly or annual price, with no proration | | Upgrade | Immediately | The prorated difference for the rest of the billing period. A pending downgrade is canceled. | | Downgrade | End of the billing period | The lower price from the next renewal. You keep the current features until then. | | Monthly → annual | Immediately | The annual total, minus the unused part of the current month | | Annual → monthly | Immediately | Nothing until the remaining annual credit is used up by monthly invoices | | Resubscribe | Immediately | Regular pricing in a new Paddle checkout, because a trial cannot be reused | You can cancel a scheduled downgrade at any time before the period ends. ### What a downgrade or cancellation turns off [Section titled “What a downgrade or cancellation turns off”](#what-a-downgrade-or-cancellation-turns-off) * **Webhooks** — The oldest active webhooks are deactivated until the new limit is met. Free allows none. See [Webhooks](/docs/platform/fundamentals/webhooks). * **Smart banners** — Below Starter, banners are no longer served and live banners are paused. Upgrading restores every saved banner, but you turn each one back on yourself. * **Custom HTML** — Below Scale, saved HTML for the app preview page and smart banners is kept but no longer served, and the template version is shown. Upgrading serves the HTML again. * **Custom domain** — When a paid plan is canceled, custom domains stop serving the organization’s links, and its apps move back to their `.godetour.link` subdomain. Shared custom-domain links stop working Links already shared on a custom domain stop working when the subscription ends. Plan the switch before you cancel. See [Moving an app between hosts](/docs/platform/advanced/custom-domain#moving-an-app-between-hosts). ## Cancellation [Section titled “Cancellation”](#cancellation) Click **Cancel subscription** at the bottom of the **Billing** section. Cancellation takes effect at the end of the billing period, and you keep access until then. Any outstanding PAYG balance is charged first. The organization then moves to **Free**, and a new monthly click window begins. You can resubscribe at any time. ## Payments [Section titled “Payments”](#payments) Checkout, invoices, and card details are handled by Paddle. Paddle accepts major cards, such as Visa, Mastercard, and American Express, and local payment methods depending on your region. The billing currency follows the Paddle price you selected, and PAYG charges use the same currency. * **Card** — The **Billing** section shows the saved card. Click **Update card** to replace it in a Paddle form. There is no separate option to remove a card. Canceling the subscription stops all future charges. * **Invoices** — **Transaction History** in the **Billing** section lists every charge. Click **Download invoice** for the PDF. Paddle also emails a receipt after every successful charge. ### Failed payments [Section titled “Failed payments”](#failed-payments) If a payment fails, for example because the card expired, Paddle retries it over the following days and emails you. The subscription status changes to `past_due`, and your features stay available. To recover, click **Update card** in the **Billing** section before the retries end. If every retry fails, the subscription is canceled and the organization moves to Free. ## Refunds [Section titled “Refunds”](#refunds) Detour’s general policy is **no refunds**. Limited exceptions apply: | Scenario | Eligibility | | ---------------------------------------------------------------- | ----------------------------------------------- | | Accidental purchase by a friend or family member on your account | May be eligible | | Unauthorized charge reported within 120 days | May be eligible | | Cancellation during an active billing period | Not eligible, access continues until period end | To request a refund, write to **** with your account details and the reason. The team responds within **14 business days**. Sending more requests for the same transaction does not speed this up. See the [Refund Terms](https://godetour.dev/refund-terms). ## Related pages [Section titled “Related pages”](#related-pages) [Dashboard Walkthrough ](/docs/platform/fundamentals/Dashboard/)The Dashboard sections and app details. [Analytics ](/docs/platform/fundamentals/analytics/)Privacy-aware link and event analytics. [Webhooks ](/docs/platform/fundamentals/webhooks/)Receive event notifications over HTTP. [Custom Domain ](/docs/platform/advanced/custom-domain/)Serve links from your own domain. # Dashboard Walkthrough > A walkthrough of the Detour Dashboard: apps, links, analytics views, and API configuration. In the Dashboard you create apps, configure link behavior, generate API keys, and read analytics. Every app belongs to an organization. The sections below describe each area, in the order you use them during setup. ## Organizations [Section titled “Organizations”](#organizations) An **organization** is the top-level workspace in Detour. Each organization has its own: * subdomain * members * apps and links * analytics context The subdomain is assigned at creation and becomes the host of every generated base link, unless you attach a [custom domain](/docs/platform/advanced/custom-domain). The switcher in the sidebar controls which organization you are working in. ### Roles [Section titled “Roles”](#roles) Permissions are role-based. * **Owner** — the broadest scope. Manages apps, short links, API keys, and team membership, and can assign every role (`owner`, `admin`, `member`). Only an Owner can delete the organization. * **Admin** — full day-to-day control. Covers app setup, link configuration, short links, and API key rotation. Can invite and manage Admin and Member users, but not Owners, and cannot assign the Owner role or delete the organization. * **Member** — read-only. Can inspect app setup and analytics, but cannot create or edit apps, manage short links, invite users, or perform any organization-level action. If you are the sole owner of an organization, you cannot leave it until another owner is assigned. If that organization has other members, you also cannot delete your account until then. ## Apps [Section titled “Apps”](#apps) The **Apps** view is the main Dashboard view. It lists every app in the selected organization. Owners and Admins can create, rename, and delete apps. Every member can copy an app’s link. The number of apps you can create depends on your plan, and the Free plan allows one. See [Billing & Payments](/docs/platform/fundamentals/billing). ### Base link [Section titled “Base link”](#base-link) When you create an app, Detour generates its App ID, its publishable API key and its base link: ```txt https://.godetour.link/ ``` You cannot change the base link. For campaign variants, create short links or add URL parameters to it. Create one app per environment To keep staging and production apart, create a separate app for each. Each app has its own credentials. ## App details [Section titled “App details”](#app-details) An app opens in a view with four tabs. A setup progress widget stays visible until the app has at least one mobile platform configured and a Redirect URL for web fallback. ### Link settings [Section titled “Link settings”](#link-settings) * **Your app link** — the read-only base URL, with a QR code and editable link metadata (title and description, Open Graph, Twitter/X). * **Redirect URL for web fallback** — the URL desktop and non-app traffic is redirected to. Query parameters can be forwarded in full, selectively, or not at all. * **Short links** — campaign and channel variants, with QR codes. * **Matching configuration** — threshold and time window. See [Matching](/docs/platform/architecture/matching). ### App configuration [Section titled “App configuration”](#app-configuration) The iOS and Android cards each have an edit mode and a generated integration snippet. For the fields and where to find their values, see [Getting Started](/docs/platform/fundamentals/getting-started#app-configuration). Saving regenerates the snippets with the current link host and app-specific paths. If your organization uses a [custom domain](/docs/platform/advanced/custom-domain), the snippets carry that host instead. ### Redirect page [Section titled “Redirect page”](#redirect-page) Three tabs, one per screen a user can see while a link resolves: * **Redirect Page** — shown during the immediate redirect. * **Copy Link Page** — the background of the iOS copy-link flow. * **App Preview** — an optional interstitial shown to mobile visitors before the store redirect, which they continue from with a tap. Off by default, and built either from a template or, on Scale and above, from your own HTML. Custom HTML supports HTML and inline CSS only, and scripts are disabled. Detour injects the copy-link dialog and the App Preview Continue button on top of your markup, so their appearance is set here rather than in the HTML. See [Custom HTML Redirects](/docs/platform/advanced/custom-html-redirects). ### API configuration [Section titled “API configuration”](#api-configuration) Each app has two keys. * **Publishable API Key** (with the **App ID**) — used by the SDK. **Generate new key** deactivates the previous one and creates a replacement. * **Secret API Key** — used for server-to-server calls to the [Platform API](/docs/platform/advanced/api-management). It is shown only once at generation time, because Detour stores a hash rather than the key. Regenerating revokes the previous secret immediately. Treat both as app credentials. Rotate at the first suspicion of exposure and update every client. Never ship the secret key in client-side code or a mobile app. ## Analytics [Section titled “Analytics”](#analytics) **Overview**, **Links**, **Events**, and **Smart banners** are app-scoped and share the app and date range filters. All of them except **Smart banners** also filter by platform (`all`, `ios`, `android`). **Imports** lists the organization’s import jobs. **Overview** and **Imports** are available on all plans, and only an **Owner** or **Admin** can open **Imports**. **Links**, **Events**, and **Smart banners** require a paid plan. See [Billing & Payments](/docs/platform/fundamentals/billing). For metrics, event logging, and the retention model, see [Analytics](/docs/platform/fundamentals/analytics). ## Smart banners [Section titled “Smart banners”](#smart-banners) In **Smart Banners** you configure the app banner shown on your own website and copy the script tag that adds it to the site. Available on Starter and above. See [Smart Banners](/docs/platform/advanced/smart-banners). ## Webhooks [Section titled “Webhooks”](#webhooks) **Webhooks** deliver an app’s analytics data to an HTTPS endpoint on a recurring schedule (daily, weekly, or monthly). Webhooks are a paid feature and the number of active webhooks depends on your plan. See [Webhooks](/docs/platform/fundamentals/webhooks) for setup, payload format, and signature verification. ## Organization settings [Section titled “Organization settings”](#organization-settings) Invite members, change roles, and remove users, following the [role rules](#roles) above. ## Billing [Section titled “Billing”](#billing) The organization’s plan, usage, payment method, and invoices. For plans, limits, pay-as-you-go, and the full payment lifecycle, see [Billing & Payments](/docs/platform/fundamentals/billing). ## Account settings [Section titled “Account settings”](#account-settings) Change your password or email, or delete your account. ## Related pages [Section titled “Related pages”](#related-pages) [Getting Started with Detour ](/docs/platform/fundamentals/getting-started/)Set up your first app from account to first link. [Analytics ](/docs/platform/fundamentals/analytics/)Privacy-aware link and event analytics. [Billing & Payments ](/docs/platform/fundamentals/billing/)Plans, usage limits, and payments. [Webhooks ](/docs/platform/fundamentals/webhooks/)Receive event notifications over HTTP. [Matching ](/docs/platform/architecture/matching/)How clicks are matched to app installs. [Universal Links & App Links ](/docs/platform/fundamentals/universal-app-links/)Runtime deep links on iOS and Android. # Getting Started with Detour > Set up your first Detour app: create an organization, configure the Dashboard, and generate your first deferred link. This guide describes how to set up Detour for your app. You create an organization, configure an app in the Dashboard, integrate the SDK, and test a deferred link on a real device. ## 1. Create an account [Section titled “1. Create an account”](#1-create-an-account) Sign up at the [Detour Dashboard](https://godetour.dev/auth/signup) and confirm your email. ## 2. Create or join an organization [Section titled “2. Create or join an organization”](#2-create-or-join-an-organization) Detour is multi-tenant. An **Organization** groups apps, links, and users under one unique subdomain. * You choose that subdomain while creating the organization: `yourorg.godetour.link`. * Invited members manage apps and links according to their role: **Owner**, **Admin**, or **Member**. Subdomain and links The organization subdomain is the base for every generated deferred link. To use your own host instead, see [Custom domain](/docs/platform/advanced/custom-domain). ## 3. Create your app [Section titled “3. Create your app”](#3-create-your-app) In the Dashboard, create a new app by providing an **App Name**. Detour then generates: * **App ID** — the unique app identifier used by the SDK configuration. * **Publishable API Key** — the active key used by the SDK configuration. * **Base deferred link** — `https://.godetour.link/`, or your custom domain if one is attached. Both values are required by the SDK The **App ID** and **Publishable API Key** are both required by the SDK configuration. ## 4. Configure the app [Section titled “4. Configure the app”](#4-configure-the-app) After you create an app, its details view opens automatically. You can also open it later from the **Apps** list. Complete the three tabs below. ### Link settings [Section titled “Link settings”](#link-settings) * **Your app link** is generated automatically and is read-only. You can copy it, open its QR code, and manage its metadata, but the base hash segment must stay unchanged. * Set the **Redirect URL for web fallback**. * Pick a query-forwarding strategy for that fallback URL: **Don’t pass parameters**, **Pass all parameters**, or **Pass specific parameters**. * Use **Short links** for campaign variants with their own path and query parameters. * Optionally tune **Matching configuration**. See [Matching](/docs/platform/architecture/matching). ### App configuration [Section titled “App configuration”](#app-configuration) Configure **iOS** and **Android** separately. #### iOS [Section titled “iOS”](#ios) | Field | Format | Where to find it | | ------------------------ | ------------------------------- | --------------------------------------------------------------------- | | Bundle ID | For example `com.example.app` | Xcode: target settings → **General** → **Bundle Identifier** | | Team ID | 10 uppercase letters and digits | Apple Developer account membership details, or Xcode signing settings | | App Store ID | Numeric | App Store Connect, or your app’s App Store URL (`.../id1234567890`) | | Enable copy link feature | Optional | — | #### Android [Section titled “Android”](#android) | Field | Format | Where to find it | | ------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | Package name | For example `com.example.app` | `applicationId` in `build.gradle`, or `android.package` in an Expo config | | SHA-256 certificate fingerprint | At least one | [Finding your Android SHA-256 fingerprint](/docs/platform/fundamentals/universal-app-links#finding-your-android-sha-256-fingerprint) | The SHA-256 fingerprint must match the key that signed the build you install. Otherwise links open in the browser instead of the app. Which key that is depends on how you build. On save, Detour generates the integration snippets for the app-side setup, filled in with your real host and path values. ### API configuration [Section titled “API configuration”](#api-configuration) * Copy the **Publishable API Key** and **App ID**. * **Generate new key** rotates the key and revokes the previously active one. ## 5. Integrate the SDK and your app [Section titled “5. Integrate the SDK and your app”](#5-integrate-the-sdk-and-your-app) Start with the [**React Native installation**](/docs/sdk/react-native/sdk-installation/) guide. Other SDKs: [iOS](/docs/sdk/ios/sdk-installation/), [Android](/docs/sdk/android/sdk-installation/), [Flutter](/docs/sdk/flutter/sdk-installation/). After you install and initialize the SDK: 1. **Register the Detour host** Add the snippets from **App configuration**. On iOS that is an Associated Domain, on Android an App Link intent filter. Without them, Detour links do not open your app when it is already installed. Deferred links work without them. 2. **Handle navigation** Open the screen for the returned link, as described in the SDK usage guide. Custom-scheme links need a separate registration. See [Universal Links & App Links](/docs/platform/fundamentals/universal-app-links#app-side-setup). ## 6. Test the deferred-link flow [Section titled “6. Test the deferred-link flow”](#6-test-the-deferred-link-flow) Which steps apply depends on whether your app is already in the store. ### Before your app is published [Section titled “Before your app is published”](#before-your-app-is-published) This is the usual case at this point, because the build that carries the SDK is not in a store yet. Only the install step changes, so a simulator or an emulator works here. 1. Remove any earlier install from the device, simulator, or emulator. 2. Open a Detour link in a mobile browser there. 3. Install your development build there and open it. Detour records the click before the redirect runs, so the redirect ending on the web fallback or on a listing that does not exist yet does not affect the test. ### After your app is published [Section titled “After your app is published”](#after-your-app-is-published) This needs a real device. 1. Remove any earlier install from the device. 2. Open a Detour link in a mobile browser there. 3. Follow the redirect into the App Store or Google Play, install the app from there, and open it. In both cases the SDK returns the link you opened, on the first launch after the install. For the full steps, see [Testing Deferred Links](/docs/platform/fundamentals/testing). ## Related pages [Section titled “Related pages”](#related-pages) [Dashboard Walkthrough ](/docs/platform/fundamentals/Dashboard/)The Dashboard sections and app details. [Universal Links & App Links ](/docs/platform/fundamentals/universal-app-links/)Runtime deep links on iOS and Android. [Click Handling & Redirect Flow ](/docs/platform/architecture/click-handling/)What happens when a user clicks a link. # Introduction to Deferred Linking > How deferred deep linking works: recording a click before install and delivering the original destination into the app after first open. A standard deep link opens in-app content only when the app is already installed. A deferred deep link also works when the app is not installed. Detour records the click, redirects the user to the App Store or Google Play, and delivers the original destination when the app opens for the first time. Navigation context and campaign data are preserved through the store install. You configure and monitor it all from the [Detour Dashboard](https://godetour.dev/auth/signup). ## The deferred flow [Section titled “The deferred flow”](#the-deferred-flow) 1. A user clicks a Detour link, for example `https://your-org.godetour.link//campaign-x`. 2. Detour saves the click and details about the device, such as screen size and system version. 3. Detour redirects the user to the App Store or Google Play. On Android, the Google Play link also contains a click ID. 4. The user installs the app and opens it for the first time. 5. The SDK sends the same device details to Detour, plus the click ID on Android. 6. Detour matches the install to the saved click and returns the link’s destination. 7. Your app opens the screen for that destination. *** ## Benefits [Section titled “Benefits”](#benefits) * Marketing context is preserved through the store install, so users see the promotion or content you intended. * Installs from link-based campaigns are attributed. * Onboarding can pre-fill flows or show contextual content on first open. *** ## Matching modes [Section titled “Matching modes”](#matching-modes) Detour matches an install to a click in one of two ways. ### Deterministic matching [Section titled “Deterministic matching”](#deterministic-matching) Deterministic matching uses an explicit identifier created at click time, carried through the store and read on first open. On Android that identifier is `click_id` from the Play Install Referrer. It is exact, and Detour prefers it whenever it is available. ### Probabilistic matching [Section titled “Probabilistic matching”](#probabilistic-matching) Probabilistic matching is the fallback. Detour collects signals at click time and again on first open, then scores how similar the two are. Signals include: * user agent * screen size * system version * locale and timezone * IP address * clipboard contents, on iOS only, and only when the user allows the paste Detour picks the click with the highest score. It returns that click’s link only if the score is above a strict minimum. *** ## Limitations [Section titled “Limitations”](#limitations) * **Timing** — probabilistic matching only considers clicks inside a configured time window. * **Cross-device** — a click on device A never matches an install on device B. * **Platform constraints** — iOS has no install-referrer equivalent. Clipboard heuristics help, but they need the user’s paste permission and are not guaranteed. * **Accuracy** — probabilistic matching is best-effort. Do not route sensitive data through deferred links. *** ## Related pages [Section titled “Related pages”](#related-pages) [Getting Started with Detour ](/docs/platform/fundamentals/getting-started/)Set up your first app from account to first link. [Click Handling & Redirect Flow ](/docs/platform/architecture/click-handling/)What happens when a user clicks a link. [Matching ](/docs/platform/architecture/matching/)How clicks are matched to app installs. [Limitations & Known Issues ](/docs/platform/architecture/architecture-limitations/)Platform constraints and known issues. # Detour Skills > The two Detour AI coding-agent skills for setting up Detour or migrating to it from Branch or AppsFlyer. Detour provides two AI skills that help your coding agent set up Detour or migrate to it. Both use the open [Agent Skills](https://agentskills.io) format, so any agent that supports it can use them. They are open source and maintained by Software Mansion in the [software-mansion-labs/skills](https://github.com/software-mansion-labs/skills) repository. ## Available skills [Section titled “Available skills”](#available-skills) [Detour onboarding ](#detour-onboarding)For an app with no deep linking yet. Sets up the Dashboard, Universal Links and App Links, the SDK, and analytics. [Migrate to Detour ](#migrate-to-detour)For an app that uses Branch or AppsFlyer. Moves the links, the SDK callbacks, and the analytics events to Detour. Both skills cover Android, iOS, React Native, and Flutter. ## Installation [Section titled “Installation”](#installation) * Claude Code plugin Run in Claude Code: ```txt /plugin marketplace add software-mansion-labs/skills /plugin install skills@swmansion /reload-plugins ``` To update, run `/plugin marketplace update swmansion`. * npx (any agent) The [`skills` CLI](https://www.npmjs.com/package/skills) works with any supported agent, Claude Code included: ```bash npx skills add software-mansion-labs/skills ``` When prompted, select `detour-onboarding`, `migrate-to-detour`, or both. The CLI installs the skills into every supported agent it finds. To choose the agents, pass `--agent` once per agent, for example `--agent claude-code --agent cursor`. To update, run `npx skills update`. ## Detour onboarding [Section titled “Detour onboarding”](#detour-onboarding) **Skill name** — `detour-onboarding` Use it when your app has no deep linking yet. The skill guides your agent through: * Dashboard setup, including the organization subdomain, App ID, and API key * Universal Links and App Links configuration * SDK integration for iOS, Android, React Native, and Flutter * Analytics tracking with type-safe enums [View the skill source](https://github.com/software-mansion-labs/skills/tree/main/skills/detour/detour-onboarding) ## Migrate to Detour [Section titled “Migrate to Detour”](#migrate-to-detour) **Skill name** — `migrate-to-detour` Use it when your app already uses Branch or AppsFlyer. The skill guides your agent through: * Replacing the old link domain, for example `yourapp.app.link` or `yourapp.onelink.me`, with your Detour subdomain * Replacing the Branch or AppsFlyer callbacks with the single Detour `result.type` callback * Mapping analytics events, for example `af_purchase` or `BranchEvent.Purchase`, to Detour events It works through the platforms in this order: Android, iOS, React Native, Flutter. Some features have no Detour equivalent The skill flags in-app purchase validation, ATT, SKAdNetwork, and SDK-side link generation. Plan a replacement for these before you migrate. [View the skill source](https://github.com/software-mansion-labs/skills/tree/main/skills/detour/migrate-to-detour) ## Testing [Section titled “Testing”](#testing) Both skills end with a deferred-link test on a real device. See [Testing Deferred Links](/docs/platform/fundamentals/testing). ## Related pages [Section titled “Related pages”](#related-pages) [Getting Started with Detour ](/docs/platform/fundamentals/getting-started/)Set up your first app from account to first link. [Universal Links & App Links ](/docs/platform/fundamentals/universal-app-links/)Runtime deep links on iOS and Android. [Analytics ](/docs/platform/fundamentals/analytics/)Privacy-aware link and event analytics. # Testing Deferred Links > How to test deferred links and Universal Links or App Links on a real device, and why every deferred-link test needs a new click. An install from the App Store or Google Play needs a real device. A deferred test that skips the store also runs on a simulator or an emulator. Before you test, confirm that at least one platform, **iOS** or **Android**, is configured in the Dashboard, and that **Redirect URL for web fallback** is set. ## Test a deferred link before the app is published [Section titled “Test a deferred link before the app is published”](#test-a-deferred-link-before-the-app-is-published) Use this while you are integrating the SDK, before any build that contains it reaches a store. Only the install step differs, so a simulator or an emulator works here. 1. Uninstall the app from the test device, simulator, or emulator. 2. Open a Detour link in a mobile browser there. Detour records the click first, then redirects. 3. Expect the redirect to end on the web fallback, or on a store listing that does not exist yet. The click is recorded either way. 4. Install a development build there and open it. 5. Check the link that the SDK returns. The match is probabilistic here. A build you install yourself carries no Play install referrer, so Android has no `click_id` for the deterministic lookup. See [Play Store referrer missing](/docs/platform/architecture/architecture-limitations#play-store-referrer-missing). ## Test a deferred link after the app is published [Section titled “Test a deferred link after the app is published”](#test-a-deferred-link-after-the-app-is-published) 1. Uninstall the app from the test device. 2. Open a Detour link in a mobile browser on the device. Detour records the click and redirects to the App Store or Google Play. 3. Install a build of your app and open it. 4. Check the link that the SDK returns. On first open, the SDK requests a match, and a matched click returns the original link. Each test needs a new click Repeat every step for every test, including the uninstall. ### Why a click works only once [Section titled “Why a click works only once”](#why-a-click-works-only-once) Detour does not keep a list of devices. Two separate mechanisms make each test one-shot: * **The click is used up** — Detour matches only clicks that are still unmatched, so a matched click cannot match again. Only clicks inside the match window count, and that window is 15 minutes by default. * **The SDK asks once** — On the first launch, the SDK saves a flag to storage and skips the deferred lookup on every later launch. This is why step 1 uninstalls the app. See [Matching](/docs/platform/architecture/matching). ## Test a link with the app installed [Section titled “Test a link with the app installed”](#test-a-link-with-the-app-installed) Universal Links and App Links open an installed app directly, so they use a different test: 1. Install a build that contains the Associated Domains or App Link intent-filter configuration. 2. Tap the Detour link from another app, such as Messages or Notes. 3. Check that the operating system opens your app directly and the SDK returns the link. Typing the URL into a browser address bar is not a reliable test. Custom scheme links need their own test after you register the scheme. See [Custom scheme links](/docs/platform/fundamentals/universal-app-links#custom-scheme-links). ## Platform-specific tests [Section titled “Platform-specific tests”](#platform-specific-tests) See the [**Usage**](/docs/sdk/react-native/sdk-usage/) and [**Testing & Troubleshooting**](/docs/sdk/react-native/testing/) pages for specific test instructions. ## Related pages [Section titled “Related pages”](#related-pages) [Getting Started with Detour ](/docs/platform/fundamentals/getting-started/)Set up your first app from account to first link. [Matching ](/docs/platform/architecture/matching/)How clicks are matched to app installs. [Universal Links & App Links ](/docs/platform/fundamentals/universal-app-links/)Runtime deep links on iOS and Android. # Universal Links & App Links > Configure Universal Links (iOS) and App Links (Android) so verified https links open directly in your app. 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 [Section titled “Deferred links vs Universal/App Links”](#deferred-links-vs-universalapp-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. | — | ``` flowchart TD A[User clicks an https link] --> B{App installed
and domain verified?} B -->|Yes| C[Universal Link or App Link] C --> D[OS checks association files
AASA or assetlinks.json] D --> E[App opens directly with the URL
no browser] B -->|No| F[Deferred link] F --> G[Detour records the click
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 [Section titled “Custom scheme links”](#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 [Section titled “Register the scheme”](#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. 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 [Section titled “What Detour hosts”](#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`. 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](/docs/platform/advanced/custom-domain#moving-an-app-between-hosts). ### Short link paths [Section titled “Short link paths”](#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. 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/`. It returns the cached file, or `Not Found` if Apple has not fetched one for that host yet. ## Dashboard configuration [Section titled “Dashboard configuration”](#dashboard-configuration) Add this data to your **App configuration** so Detour can generate correct association files. ### iOS configuration [Section titled “iOS configuration”](#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 [Section titled “Android configuration”](#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 [Section titled “App-side setup”](#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 ](/docs/sdk/react-native/sdk-installation/#native-link-configuration)Configure Expo or bare React Native on iOS and Android. [iOS ](/docs/sdk/ios/sdk-installation/#link-capabilities)Add Associated Domains and wire AppDelegate or SceneDelegate. [Android ](/docs/sdk/android/sdk-installation/#manifest-integration)Add verified App Links and activity callbacks. [Flutter ](/docs/sdk/flutter/sdk-installation/#platform-integration)Configure both native targets and Flutter delegate forwarding. 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 [Section titled “Verification”](#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. 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. 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 [Section titled “Finding your Android SHA-256 fingerprint”](#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) `android/app/debug.keystore` | | `npx expo run:android` or `npx react-native run-android` | [Project debug keystore](#read-it-from-the-keystore) `android/app/debug.keystore` | | Flutter and native Android debug builds | [Home debug keystore](#read-it-from-the-keystore) `~/.android/debug.keystore` | | Release build signed with your own keystore | [Your release keystore](#read-it-from-the-keystore) | 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 [Section titled “Read it from the 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 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 | 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. Sample `signingReport` output ```txt > 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. `signingReport` reads local Gradle config only, so it cannot show the keystore EAS holds or the Play App Signing key. ### Read it from EAS [Section titled “Read it from EAS”](#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 [Section titled “Read it from the artifact”](#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 `. ### Play App Signing key [Section titled “Play App Signing key”](#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 [Section titled “Verify”](#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 [Section titled “Handling links in your app”](#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 the SDK [**Usage**](/docs/sdk/react-native/sdk-usage/) page . ## Related pages [Section titled “Related pages”](#related-pages) [Click Handling & Redirect Flow ](/docs/platform/architecture/click-handling/)What happens when a user clicks a link. [Custom Domain ](/docs/platform/advanced/custom-domain/)Serve links from your own domain. [Getting Started with Detour ](/docs/platform/fundamentals/getting-started/)Set up your first app from account to first link. # Webhooks > Receive scheduled analytics exports on an HTTPS endpoint, with payload format and signature verification. A webhook sends an app’s analytics data to your HTTPS endpoint on a recurring schedule. On each run, Detour sends a signed JSON payload. Each webhook belongs to one app and one export type. You manage an organization’s webhooks in the **Webhooks** section of the Dashboard sidebar. *** ## Availability [Section titled “Availability”](#availability) Webhooks are a paid feature. The number of **active** webhooks you can run depends on your plan: | Plan | Active webhooks | | ---------- | --------------- | | Free | 0 (unavailable) | | Starter | 3 | | Scale | 10 | | Enterprise | Custom | For full plan details, see [Billing & Payments](/docs/platform/fundamentals/billing). * If you try to create a webhook beyond your active limit, it is saved but created **paused** (inactive). Activate it later after deactivating another webhook. * If you **downgrade** to a plan with a lower limit, the oldest active webhooks are automatically paused to fit the new limit. *** ## Creating a webhook [Section titled “Creating a webhook”](#creating-a-webhook) When you create a webhook, you set these fields: * `App` required The app whose analytics data is exported. * `Endpoint URL` required * Type `URL` * Where Detour sends the payload. It must be a public HTTPS URL on port 443. Detour rejects endpoints that resolve to private or internal addresses. * `Export type` required * Type `Overview | Links | Events` * The dataset to send. * `Overview` — aggregated performance, including link stats, organic and non-organic installs, retention, and top events * `Links` — a breakdown by link path and query parameter * `Events` — SDK event activity * `Schedule` required * Type `Daily | Weekly | Monthly` * How often Detour sends the payload. See [Delivery schedule](#delivery-schedule). * `Platform` required * Type `all | ios | android` * The platform whose data is included. * `Filters` Optional filters that narrow the exported data. The signing secret is shown once Store the signing secret when you create the webhook, because you cannot view it again. If you lose it, rotate it. See [Managing webhooks](#managing-webhooks). *** ## Delivery schedule [Section titled “Delivery schedule”](#delivery-schedule) Deliveries run on a fixed daily cycle (around **08:00 UTC**). Each delivery covers the period that has closed: | Schedule | Runs on | Period covered | | --------- | --------------------- | --------------------------- | | `Daily` | Every day | The previous day | | `Weekly` | Mondays | The previous 7 days | | `Monthly` | The 1st of each month | The previous calendar month | All period boundaries are in UTC. *** ## Payload format [Section titled “Payload format”](#payload-format) Each delivery is an HTTP `POST` with a JSON body: ```json { "webhook_id": "", "type": "overview | links | events", "app_id": "", "period": { "start": "2026-05-01T00:00:00.000Z", "end": "2026-06-01T00:00:00.000Z" }, "delivered_at": "2026-06-01T08:00:00.000Z", "meta": { "total_rows": 4821, "returned_rows": 4821, "truncated": false, "row_limit": 10000 }, "data": [ /* rows for the selected export type */ ] } ``` | Field | Meaning | | -------------- | ---------------------------------------------------------- | | `webhook_id` | The webhook subscription this delivery came from | | `type` | The export type, `overview`, `links`, or `events` | | `app_id` | The app the exported data belongs to | | `period` | The window the data covers, as two ISO 8601 UTC timestamps | | `delivered_at` | When Detour built this delivery, ISO 8601 UTC | | `meta` | Row counts for this delivery, described below | | `data` | The rows, in the shape that matches `type` | The rows in `data` are the same rows a manual export of that view returns. Both period bounds are inclusive A row is in the delivery when its timestamp is at or after `period.start` and at or before `period.end`. `end` is the midnight UTC boundary between the reported period and the current one, so a row timestamped exactly then reaches this delivery and the next one. Aggregate deliveries by `period.start` to avoid counting that instant twice. ### `meta` [Section titled “meta”](#meta) | Field | Meaning | | --------------- | ---------------------------------------------------------- | | `total_rows` | Rows the export matched, before any limit | | `returned_rows` | Rows present in `data` | | `truncated` | `true` when `data` holds only the first part of the result | | `row_limit` | The per-delivery row cap in force | `meta` is additive. `data` keeps its existing shape and position, so an integration that reads only `data` needs no change. *** ## Row limits [Section titled “Row limits”](#row-limits) A single delivery carries at most **10,000 rows**. The limit applies to the HTTP body Detour sends, not to your data, so you can still get the full result in other ways. Check `meta.truncated` on every delivery. When it is `true`, `data` holds the first `row_limit` rows of a larger result, and `meta.total_rows` says how many rows there were in total. When deliveries are truncated, you have three options: * **Narrow the webhook with filters** — fewer clicks are then in scope. * **Switch to a shorter schedule** — `Daily` rather than `Weekly` or `Monthly`, so each delivery covers a smaller window. * **Pull the full dataset instead** — the [Platform API](/docs/platform/advanced/api-management) supports paging and parameter-level filtering. *** ## Reading `links` rows [Section titled “Reading links rows”](#reading-links-rows) A `links` delivery carries two kinds of row, distinguished by `param_key`: * **Path rows** (`param_key` is `""`) — the totals for one date, short link and path. * **Parameter rows** — the breakdown of a single query parameter value inside that path. ### Already-installed opens [Section titled “Already-installed opens”](#already-installed-opens) Path rows and parameter rows both carry `already_installed_opens`, the taps that opened an installed app directly through [Universal Links or App Links](/docs/platform/fundamentals/universal-app-links). These opens are included in `clicks`: * `clicks` — all opens * `clicks - already_installed_opens` — web opens only An open is split into parameter rows the same way a web click is. Its `short_link` is empty when the tap was on a plain deep link, or when the open was recorded before Detour began attributing these opens. *** ## Verifying the signature [Section titled “Verifying the signature”](#verifying-the-signature) Every request includes an `X-Webhook-Signature` header so you can confirm the payload came from Detour and wasn’t tampered with: ```plaintext X-Webhook-Signature: sha256= ``` The digest is an **HMAC-SHA256** of the **raw request body**, keyed with your webhook’s signing secret. Compute the same HMAC on your side and compare: ```js import { createHmac, timingSafeEqual } from "crypto"; function isValidSignature(rawBody, header, signingSecret) { const expected = createHmac("sha256", signingSecret) .update(rawBody) .digest("hex"); const received = (header ?? "").replace(/^sha256=/, ""); const a = Buffer.from(expected); const b = Buffer.from(received); return a.length === b.length && timingSafeEqual(a, b); } ``` Use the raw body Always compute the HMAC over the **raw, unparsed** request body. Re-serializing the JSON can change bytes (key order, whitespace) and break the comparison. *** ## Delivery behavior [Section titled “Delivery behavior”](#delivery-behavior) * Detour posts only to the configured URL. **Redirects are not followed**. * The endpoint must respond with a **2xx** status within **10 seconds** for the delivery to count as successful. * **Failed deliveries are not retried** within the same cycle. The next attempt is the next scheduled run. Make sure your endpoint is reachable at delivery time. * The Dashboard shows the **last successful delivery** time for each webhook. *** ## Managing webhooks [Section titled “Managing webhooks”](#managing-webhooks) In the **Webhooks** section you can: * **Pause or resume** — Resuming counts toward your plan’s active-webhook limit. At the limit, pause another webhook first. * **Rotate the signing secret** — The new secret is shown once, and the previous one stops working immediately. * **Delete** — Removes the webhook. You cannot edit a webhook’s URL, schedule, export type, or platform. To change them, delete the webhook and create a new one. The **Last attempt** column shows when Detour last tried to deliver and whether it succeeded, so you can tell a webhook that never ran from one that keeps failing. On failure, the tooltip on the warning icon gives the reason, for example the HTTP status your endpoint returned. *** ## Related pages [Section titled “Related pages”](#related-pages) [Analytics ](/docs/platform/fundamentals/analytics/)Privacy-aware link and event analytics. [Billing & Payments ](/docs/platform/fundamentals/billing/)Plans, usage limits, and payments. [Dashboard Walkthrough ](/docs/platform/fundamentals/Dashboard/)The Dashboard sections and app details. # API Reference > Complete API reference for the Detour Android SDK. ## Detour [Section titled “Detour”](#detour) Main SDK singleton. All methods are `@JvmStatic` for Java interoperability. ### Methods [Section titled “Methods”](#methods) #### `initialize` [Section titled “initialize”](#initialize) ```kotlin Detour.initialize(context: Context, config: DetourConfig): Unit ``` Initializes the SDK. Must be called once before any other SDK method, usually in `Application.onCreate()` or at the start of your launcher `Activity`. Sets up the internal API client, fingerprint collector, install referrer helper, and analytics subsystem. If `apiKey` or `appId` is blank, the SDK logs an error and does not initialize, and it does not throw. #### `processLink` [Section titled “processLink”](#processlink) ```kotlin suspend fun Detour.processLink(intent: Intent): LinkResult ``` Main entry point for link processing. Automatically detects the link source from the given `Intent` and returns the appropriate result. Checks in priority order: 1. **App Links**: HTTP/HTTPS URLs from verified domains (returns `LinkType.VERIFIED`) 2. **Scheme links**: custom scheme deep links, if `linkProcessingMode` is `ALL` (returns `LinkType.SCHEME`) 3. **Deferred links**: queries the Detour API on first launch (returns `LinkType.DEFERRED`) If a web URL has a single path segment (short link), the SDK resolves it to the full destination URL before returning. For App Links and scheme web URLs, the SDK first verifies the click against your plan’s click limit. If the limit is exceeded, the link is not delivered and the result is `LinkResult.NoLink`. See [Click limits](/docs/sdk/android/sdk-usage#click-limits). #### `getDeferredLink` [Section titled “getDeferredLink”](#getdeferredlink) ```kotlin suspend fun Detour.getDeferredLink(): LinkResult ``` Resolves deferred links only, ignoring App Links and scheme links. Useful when you handle other link types separately. Returns `LinkResult.NotFirstLaunch` on subsequent calls within the same session, because deferred matching runs only once per app install. #### `resolveShortLink` [Section titled “resolveShortLink”](#resolveshortlink) ```kotlin suspend fun Detour.resolveShortLink(url: String): ShortLinkResponse? ``` Resolves a Detour short link URL to its full destination. Returns `null` if resolution fails or the URL is not a valid short link. ## DetourDelegate [Section titled “DetourDelegate”](#detourdelegate) Lifecycle-aware helper that wraps `Detour.processLink()` for `Activity` / `Fragment` integrations. Manages coroutine scope internally and delivers results via callback. ### Constructor [Section titled “Constructor”](#constructor) ```kotlin DetourDelegate( lifecycleOwner: LifecycleOwner, config: DetourConfig, onLinkResult: (LinkResult) -> Unit, ) ``` * `lifecycleOwner` * Type `LifecycleOwner` * The Activity, AppCompatActivity or Fragment that owns this delegate. * `config` * Type [`DetourConfig`](#detourconfig) * SDK configuration. Also used to call `Detour.initialize()` if the SDK is not initialized yet. * `onLinkResult` * Type `(LinkResult) -> Unit` * Callback invoked with the link processing result. ### Methods [Section titled “Methods”](#methods-1) #### `onCreate` [Section titled “onCreate”](#oncreate) ```kotlin fun onCreate(intent: Intent): Unit ``` Call from `Activity.onCreate()`. Processes the launching intent for App Links, scheme links, and deferred links. #### `onNewIntent` [Section titled “onNewIntent”](#onnewintent) ```kotlin fun onNewIntent(intent: Intent): Unit ``` Call from `Activity.onNewIntent()`. Processes links received while the app is already running. ## DetourConfig [Section titled “DetourConfig”](#detourconfig) SDK configuration object. Accepts required credentials and optional behavioral settings. ### Data class [Section titled “Data class”](#data-class) ```kotlin data class DetourConfig( val apiKey: String, val appId: String, val linkProcessingMode: LinkProcessingMode = LinkProcessingMode.ALL, val storage: DetourStorage? = null, ) ``` * `apiKey` required * Type `String` * Publishable API key from the Detour Dashboard. Sent as a Bearer token on every API call: deferred link matching, short-link resolution, and analytics. * `appId` required * Type `String` * Unique app identifier from the Detour Dashboard. Routes API requests to the correct app. * `linkProcessingMode` * Type [`LinkProcessingMode` ](#linkprocessingmode)Default `LinkProcessingMode.ALL` * Which link sources the SDK processes. See [LinkProcessingMode](#linkprocessingmode) for each value. * `storage` * Type [`DetourStorage?` ](#detourstorage)Default `null` * Custom storage adapter for SDK persistence. The SDK stores a first-entrance flag (for deferred link matching) and a device ID (for analytics). Left `null`, it falls back to a SharedPreferences-based implementation. ### Java builder [Section titled “Java builder”](#java-builder) For Java callers, a builder pattern is available: ```kotlin DetourConfig.Builder(apiKey: String, appId: String) .linkProcessingMode(LinkProcessingMode) .storage(DetourStorage) .build() ``` ## LinkResult [Section titled “LinkResult”](#linkresult) Sealed class returned by `processLink()` and `getDeferredLink()`. Pattern-match on the subtype to handle each outcome. ```kotlin sealed class LinkResult { data class Success( val url: String, val route: String, val pathname: String, val type: LinkType, val params: Map, ) : LinkResult() data object NotFirstLaunch : LinkResult() data object NoLink : LinkResult() data class Error(val exception: Exception) : LinkResult() } ``` ### Subtypes [Section titled “Subtypes”](#subtypes) | Subtype | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------ | | `Success` | A link was successfully resolved. Contains the parsed link data, listed below | | `NotFirstLaunch` | No deferred link available because this is not the first app launch. Only returned by the deferred link flow | | `NoLink` | Normal app launch without any deep link | | `Error` | An error occurred during link processing. The `exception` field contains the cause | ### Success fields [Section titled “Success fields”](#success-fields) * `url` * Type `String` * Full URL that was matched or opened. * `route` * Type `String` * In-app route for navigation, as a path with query string, first path segment (app hash) stripped (e.g. `/promo/123?utm=1`). * `pathname` * Type `String` * Route path without query string (e.g. `/promo/123`). * `type` * Type [`LinkType`](#linktype) * How the link arrived: `DEFERRED`, `VERIFIED`, or `SCHEME`. * `params` * Type `Map` * Parsed query parameters as key-value pairs. ## LinkProcessingMode [Section titled “LinkProcessingMode”](#linkprocessingmode) ```kotlin enum class LinkProcessingMode { ALL, WEB_ONLY, DEFERRED_ONLY, } ``` Controls which link sources `Detour.processLink()` handles: | Value | Behavior | | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ALL` (default) | Processes all link sources: deferred links from the Detour API, App Links (verified HTTP/HTTPS URLs), and custom scheme deep links | | `WEB_ONLY` | Same as `ALL` but filters out custom scheme links. Only deferred links and App Links (HTTP/HTTPS) are processed | | `DEFERRED_ONLY` | Only processes deferred links from the Detour API. Skips App Link and scheme link detection. Recommended when your navigation framework already handles runtime deep links | ## LinkType [Section titled “LinkType”](#linktype) ```kotlin enum class LinkType { DEFERRED, VERIFIED, SCHEME, } ``` Describes how the link arrived in the app: | Value | Meaning | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `DEFERRED` | The link was obtained from the Detour deferred-link API. This happens when a user who does not have the app installed clicks a Detour link, installs the app, and opens it. The SDK retrieves the original link from the backend on first launch | | `VERIFIED` | The link arrived as an Android App Link, an HTTP/HTTPS URL from a verified domain that the OS delivered directly to your app | | `SCHEME` | The link arrived via a custom URL scheme (e.g. `myapp://path`). These are non-HTTP URLs that the OS routes to your app based on the registered intent filter | ## ShortLinkResponse [Section titled “ShortLinkResponse”](#shortlinkresponse) ```kotlin data class ShortLinkResponse( val link: String, val route: String?, val parameters: String?, ) ``` Returned by `Detour.resolveShortLink()`. * `link` * Type `String` * The full destination URL after short-link resolution. * `route` * Type `String?` * Extracted in-app route, or `null` if not available. * `parameters` * Type `String?` * Raw query parameter string, or `null` if none. ## DetourStorage [Section titled “DetourStorage”](#detourstorage) ```kotlin interface DetourStorage { suspend fun getItem(key: String): String? suspend fun setItem(key: String, value: String) suspend fun removeItem(key: String) { } } ``` Interface for custom storage adapters. The SDK persists a first-entrance flag (used for deferred link matching, which only runs on first app install) and a stable device ID (used for analytics). All methods are suspending functions. By default, the SDK uses a SharedPreferences-based implementation. Provide a custom adapter if you need encrypted storage, DataStore, or another persistence layer. See [Custom Storage](/docs/sdk/android/custom-storage) for examples. ## DetourAnalytics [Section titled “DetourAnalytics”](#detouranalytics) Analytics module initialized by `Detour.initialize()`. All calls are fire-and-forget. They run on a background dispatcher and never propagate exceptions. Both methods are `@JvmStatic` for Java interoperability. ### Methods [Section titled “Methods”](#methods-2) #### `logEvent` [Section titled “logEvent”](#logevent) ```kotlin DetourAnalytics.logEvent(eventName: DetourEventNames, data: Any? = null): Unit ``` Records a user action or custom event and sends it to the Detour backend. Pass one of the predefined [`DetourEventNames`](#detoureventnames) values. The optional `data` parameter accepts any Gson-serializable object (maps, primitives, data classes, etc.). #### `logRetention` [Section titled “logRetention”](#logretention) ```kotlin DetourAnalytics.logRetention(eventName: String): Unit ``` Records a retention/lifecycle event for user engagement tracking with a free-form event name string. ### Automatic events [Section titled “Automatic events”](#automatic-events) Besides the events you log manually, the SDK reports some on its own, with no wiring needed: * `app_open` — a retention event logged once per session when `Detour.initialize()` runs on a cold start. App Link and Universal Link opens are recorded server-side through a dedicated click endpoint (see [Click limits](/docs/sdk/android/sdk-usage#click-limits)) rather than as a client event. All API requests the SDK sends also include an `X-SDK: android/` header for diagnostics and SDK-usage analysis. It requires no configuration. ## DetourEventNames [Section titled “DetourEventNames”](#detoureventnames) | Category | Event Name | String Value | | :---------- | :--------------------------- | :------------------------------ | | **General** | `Login` | `login` | | | `Search` | `search` | | | `Share` | `share` | | | `SignUp` | `sign_up` | | | `TutorialBegin` | `tutorial_begin` | | | `TutorialComplete` | `tutorial_complete` | | | `ReEngage` | `re_engage` | | | `Invite` | `invite` | | | `OpenedFromPushNotification` | `opened_from_push_notification` | | **Sales** | `AddPaymentInfo` | `add_payment_info` | | | `AddShippingInfo` | `add_shipping_info` | | | `AddToCart` | `add_to_cart` | | | `RemoveFromCart` | `remove_from_cart` | | | `Refund` | `refund` | | | `ViewItem` | `view_item` | | | `BeginCheckout` | `begin_checkout` | | | `Purchase` | `purchase` | | | `AdImpression` | `ad_impression` | ## Related pages [Section titled “Related pages”](#related-pages) * [Usage](/docs/sdk/android/sdk-usage) * [Custom Storage](/docs/sdk/android/custom-storage) * [Testing & Troubleshooting](/docs/sdk/android/testing) # Custom Storage > Provide a custom storage adapter (EncryptedSharedPreferences or your own) for the Detour Android SDK. The SDK persists two internal keys: a first-launch flag (`Detour_firstEntranceFlag`) and an analytics device identifier (`Detour_deviceId`). Left unset, `DetourConfig.storage` is `null` and the SDK falls back to its own `SharedPreferences`-based store. To use encrypted storage, DataStore or another persistence layer, pass a `DetourStorage` implementation instead. Values must persist across restarts Both keys have to persist across app restarts. A storage adapter that loses them makes the SDK re-run the deferred flow and generate a new analytics device id. ## Storage interface [Section titled “Storage interface”](#storage-interface) ```kotlin interface DetourStorage { suspend fun getItem(key: String): String? suspend fun setItem(key: String, value: String) suspend fun removeItem(key: String) { } } ``` ## Examples [Section titled “Examples”](#examples) The adapter below wraps `EncryptedSharedPreferences`. Any class that implements `DetourStorage` works, so use it as a starting point for DataStore or another persistence layer. ### EncryptedSharedPreferences [Section titled “EncryptedSharedPreferences”](#encryptedsharedpreferences) ```kotlin import android.content.Context import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey import com.swmansion.detour.storage.DetourStorage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class EncryptedStorageProvider(context: Context) : DetourStorage { private val masterKey = MasterKey.Builder(context) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build() private val prefs = EncryptedSharedPreferences.create( context, "DetourSecureStorage", masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ) override suspend fun getItem(key: String): String? = withContext(Dispatchers.IO) { prefs.getString(key, null) } override suspend fun setItem(key: String, value: String) = withContext(Dispatchers.IO) { prefs.edit().putString(key, value).apply() } } ``` ```kotlin val config = DetourConfig( apiKey = BuildConfig.DETOUR_API_KEY, appId = BuildConfig.DETOUR_APP_ID, storage = EncryptedStorageProvider(this) ) ``` ## Related pages [Section titled “Related pages”](#related-pages) * [Usage](/docs/sdk/android/sdk-usage) * [API Reference](/docs/sdk/android/api-reference) # Examples > Runnable Detour Android example apps covering full and deferred-only integrations. The Android SDK repository ships two runnable apps, one per integration style. Clone the one closest to your setup, run it, and copy what you need. Each card below links to the example’s `README.md` on GitHub, with the setup steps, the test flow, and the full source. ## Catalog [Section titled “Catalog”](#catalog) [example ](https://github.com/software-mansion-labs/android-detour/blob/main/examples/example/README.md)The full setup. DetourDelegate handles App Links, custom scheme links and deferred links, and the app logs analytics events. [example-deferred-only ](https://github.com/software-mansion-labs/android-detour/blob/main/examples/example-deferred-only/README.md)For apps whose navigation already handles App Links. Detour resolves only the deferred link, while the splash screen is shown. ## Where to go next [Section titled “Where to go next”](#where-to-go-next) 1. Add the SDK to your own app. See [Installation](/docs/sdk/android/sdk-installation). 2. Learn the lifecycle and routing model the examples use. See [Usage](/docs/sdk/android/sdk-usage). 3. Adapt the closest example’s manifest and routing logic to your app. 4. Check the result. See [Testing & Troubleshooting](/docs/sdk/android/testing). # Installation > Install the Detour Android SDK via Gradle (Kotlin or Groovy DSL). Detour provides an Android SDK (`com.swmansion.detour:detour-sdk`) for deferred deep links, App Links, and custom scheme links. AI agent skills Detour ships two AI coding-agent skills: `detour-onboarding` for a setup from scratch, and `migrate-to-detour` for moving off Branch or AppsFlyer. Both cover this page. See [Detour Skills](/docs/platform/fundamentals/skills). A Detour app in the Dashboard is required before the SDK can resolve links. See [Getting Started](/docs/platform/fundamentals/getting-started). ## Requirements [Section titled “Requirements”](#requirements) * Android min SDK: `21` (Android 5.0+) * Kotlin: `2.0+` * JVM target: `11` The SDK calls the Detour API over the network, so it needs the two standard Android network permissions, `INTERNET` and `ACCESS_NETWORK_STATE`. It declares both in its own manifest and manifest merging adds them to your app, so there is nothing to add to your `AndroidManifest.xml`. ## Add dependency [Section titled “Add dependency”](#add-dependency) * build.gradle.kts ```kotlin dependencies { implementation("com.swmansion.detour:detour-sdk:1.1.0") } ``` * build.gradle ```groovy dependencies { implementation "com.swmansion.detour:detour-sdk:1.1.0" } ``` ## Credentials [Section titled “Credentials”](#credentials) Both values come from **API configuration** in the Dashboard: the `appId` and the publishable `apiKey`. See [Getting Started](/docs/platform/fundamentals/getting-started#api-configuration). Store credentials outside source control and pass them through `BuildConfig`, the way the SDK examples do: app/build.gradle.kts ```kotlin buildConfigField("String", "DETOUR_API_KEY", "\"${secrets["DETOUR_API_KEY"] ?: ""}\"") buildConfigField("String", "DETOUR_APP_ID", "\"${secrets["DETOUR_APP_ID"] ?: ""}\"") ``` ```kotlin // Activity/Application code val config = DetourConfig( apiKey = BuildConfig.DETOUR_API_KEY, appId = BuildConfig.DETOUR_APP_ID ) ``` ## Initialization [Section titled “Initialization”](#initialization) Call `Detour.initialize(...)` once, before any link is processed. ```kotlin Detour.initialize(applicationContext, config) ``` `Application.onCreate()` is the usual place. The first activity works too, as long as it runs before `processLink()` or `getDeferredLink()`. ## Manifest integration [Section titled “Manifest integration”](#manifest-integration) Android only dispatches a link to your app if a `VIEW` intent filter claims it. Add the filters to the activity that passes incoming intents to Detour. ### Verified App Links [Section titled “Verified App Links”](#verified-app-links) Copy the generated filter from [**App configuration**](/docs/platform/fundamentals/getting-started#app-configuration) in the Dashboard. It has this structure: AndroidManifest.xml ```xml ``` Keep the generated values rather than copying the placeholders above. New short links reuse `/` and do not require another manifest entry or native build. If your app is served from a [custom domain](/docs/platform/advanced/custom-domain), the Dashboard generates the filter for that domain. Assign the domain before you copy the values. Changing the host an app is assigned to later needs a new build. Do not add hosts it did not generate: on Android 11 and lower that breaks App Link verification for every host at once, see [Verification](/docs/platform/fundamentals/universal-app-links#verification). ### Custom-scheme links (optional) [Section titled “Custom-scheme links (optional)”](#custom-scheme-links-optional) The Dashboard does not generate custom-scheme registration. If your app uses links such as `myapp://product/42`, add a separate filter to the same activity: AndroidManifest.xml ```xml ``` The [`example`](https://github.com/software-mansion-labs/android-detour/blob/main/examples/example/README.md) app shows the completed manifest and activity integration. ## Next steps [Section titled “Next steps”](#next-steps) 1. Wire `DetourDelegate` or manual processing in [Usage](/docs/sdk/android/sdk-usage). 2. Pick the closest runnable pattern in [Examples](/docs/sdk/android/examples). # Usage > Initialize Detour with DetourDelegate, resolve deferred links, and handle App Links and scheme links in your Android app. Detour delivers links to your app through the launching intent. `DetourDelegate` attaches to the Activity lifecycle, resolves the incoming link, whether deferred, App Link, or custom scheme, and returns a `LinkResult` to navigate with. Apps that only need deferred links can skip the delegate and call `Detour.getDeferredLink()` directly. ## Delegate setup [Section titled “Delegate setup”](#delegate-setup) `DetourDelegate` is the recommended integration for classic Android apps. It wraps the coroutine calls to `Detour.processLink(...)` and returns `LinkResult` in a callback. ```kotlin import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.swmansion.detour.Detour import com.swmansion.detour.DetourConfig import com.swmansion.detour.DetourDelegate import com.swmansion.detour.models.LinkProcessingMode import com.swmansion.detour.models.LinkResult class MainActivity : AppCompatActivity() { private val detourDelegate = DetourDelegate( lifecycleOwner = this, config = DetourConfig( apiKey = BuildConfig.DETOUR_API_KEY, appId = BuildConfig.DETOUR_APP_ID, linkProcessingMode = LinkProcessingMode.ALL, ), onLinkResult = { result -> handleLinkResult(result) } ) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Detour.initialize(this, detourDelegate.config) detourDelegate.onCreate(intent) } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) setIntent(intent) detourDelegate.onNewIntent(intent) } private fun handleLinkResult(result: LinkResult) { // handle result and navigate } } ``` `onCreate(intent)` processes the intent that created a new activity instance. `onNewIntent(intent)` processes a link delivered to an existing instance, for example when the activity uses `singleTop` or `singleTask`. Keep the `setIntent(intent)` call so the activity’s current intent stays in sync. Initialize once Call `Detour.initialize(...)` exactly once per process. A second call logs a warning and returns without re-configuring the SDK. For complete, runnable integrations, see [Examples](/docs/sdk/android/examples). ## Handle resolved links [Section titled “Handle resolved links”](#handle-resolved-links) `processLink()` and `getDeferredLink()` return one of four results: * `LinkResult.Success` — carries `url`, `route`, `pathname`, `params`, and `type` * `LinkResult.NotFirstLaunch` — the deferred flow was already consumed in this install or session * `LinkResult.NoLink` — no matching link * `LinkResult.Error` — a runtime or integration error Typical routing pattern: ```kotlin when (result) { is LinkResult.Success -> navigateToRoute(result.route, result.params) is LinkResult.NotFirstLaunch -> Unit is LinkResult.NoLink -> Unit is LinkResult.Error -> Log.e("Detour", "Link error", result.exception) } ``` ## Link processing modes [Section titled “Link processing modes”](#link-processing-modes) `DetourConfig.linkProcessingMode` controls which link sources the SDK handles: | Value | Deferred | App Links (`http/https`) | Custom scheme | | --------------- | -------- | ------------------------ | ------------- | | `ALL` (default) | Yes | Yes | Yes | | `WEB_ONLY` | Yes | Yes | No | | `DEFERRED_ONLY` | Yes | No | No | Use `DEFERRED_ONLY` when your navigation stack already handles App Link and scheme intents, and Detour should only resolve first-install deferred links. ## Click limits [Section titled “Click limits”](#click-limits) Before it delivers an App Link or Universal Link, the SDK checks the click against your plan’s limit. Within the limit, links resolve normally. ### Over the limit [Section titled “Over the limit”](#over-the-limit) The link is **not** delivered. `processLink()` returns `LinkResult.NoLink`, and so does the `DetourDelegate` callback. The SDK logs `[Detour:CLICK_LIMIT_ERROR]` in logcat with `clicksInPeriod` and `effectiveLimit`. ### Other link types [Section titled “Other link types”](#other-link-types) This flow applies only to App Links and Universal Links. * **Deferred and short links** — the same plan limits apply on the backend, but the SDK raises no click-limit error. An over-limit link fails to resolve: `getDeferredLink()` returns `LinkResult.NoLink` and `resolveShortLink()` returns `null`. * **Custom scheme links** — resolved on device, never limit-checked. Fail-open by design If the limit check cannot complete because of a network or backend error, the SDK delivers the link, so a temporary outage does not block navigation. Limits are enforced only on an explicit “denied” response. For per-plan limits, pay-as-you-go overage, and how to raise them, see [Billing & Payments](/docs/platform/fundamentals/billing). ## Deferred-only flow [Section titled “Deferred-only flow”](#deferred-only-flow) In `DEFERRED_ONLY` mode you can skip `DetourDelegate` and call `Detour.getDeferredLink()` directly: ```kotlin Detour.initialize( this, DetourConfig( apiKey = BuildConfig.DETOUR_API_KEY, appId = BuildConfig.DETOUR_APP_ID, linkProcessingMode = LinkProcessingMode.DEFERRED_ONLY, ) ) lifecycleScope.launch { when (val result = Detour.getDeferredLink()) { is LinkResult.Success -> navigateToRoute(result.route, result.params) else -> Unit } } ``` See the `example-deferred-only` app in [Examples](/docs/sdk/android/examples). ## Analytics [Section titled “Analytics”](#analytics) `Detour.initialize(...)` turns analytics on. With the SDK initialized, it logs one event by itself: `app_open`, a retention event sent once per session on cold start. App Link and Universal Link opens are not client events. The backend records them through the click endpoint (see [Click limits](#click-limits)). Everything else you log yourself, through `DetourAnalytics`. ```kotlin import com.swmansion.detour.analytics.DetourAnalytics import com.swmansion.detour.analytics.DetourEventNames DetourAnalytics.logEvent( DetourEventNames.Purchase, mapOf("item" to "subscription_premium", "currency" to "USD"), ) DetourAnalytics.logRetention("home_screen_opened") ``` SDK version header Every request the SDK sends carries an `X-SDK: android/` header, used for diagnostics and SDK-usage analysis. It needs no configuration. ## Related pages [Section titled “Related pages”](#related-pages) * [Examples](/docs/sdk/android/examples) * [API Reference](/docs/sdk/android/api-reference) * [Custom Storage](/docs/sdk/android/custom-storage) * [Testing & Troubleshooting](/docs/sdk/android/testing) # Testing & Troubleshooting > Test deferred linking and App Links on Android and troubleshoot common Detour Android SDK issues. Testing deferred linking on Android has platform constraints, mainly around install referrer availability and app-link verification state. ## Deferred flow constraints [Section titled “Deferred flow constraints”](#deferred-flow-constraints) * Deferred matching is intended for first-launch flow after install. * Deterministic matching uses Play Install Referrer `click_id` when available. * If install referrer is unavailable, SDK falls back to probabilistic matching. * Repeated deferred attempts in same install/session return `NotFirstLaunch`. ## Recommended deferred test flow [Section titled “Recommended deferred test flow”](#recommended-deferred-test-flow) 1. Uninstall the app from the device or emulator. 2. Open a Detour link in a browser. 3. Install and launch the app. 4. Verify `LinkResult.Success` with `type = DEFERRED`, or expected fallback (`NoLink` / `Error`). 5. Relaunch the app and confirm the deferred link is not consumed again, that is `LinkResult.NotFirstLaunch`. Missing `click_id` If you encounter click\_id missing in your build see limitations section [Play Store referrer missing](/docs/platform/architecture/architecture-limitations#play-store-referrer-missing) ## Testing app links and scheme links [Section titled “Testing app links and scheme links”](#testing-app-links-and-scheme-links) For app-link/scheme integration (`LinkProcessingMode.ALL`), verify both cold and warm starts. ### App Link via ADB [Section titled “App Link via ADB”](#app-link-via-adb) ```bash adb shell am start -a android.intent.action.VIEW \ -d "https:///products/42" \ ``` Expected: `LinkResult.Success` with `type = VERIFIED`. ### Custom scheme via ADB [Section titled “Custom scheme via ADB”](#custom-scheme-via-adb) ```bash adb shell am start -a android.intent.action.VIEW \ -d "myapp://products/42" \ ``` Expected: `LinkResult.Success` with `type = SCHEME` (only when `LinkProcessingMode.ALL`). ### App Link verification and the SHA-256 fingerprint [Section titled “App Link verification and the SHA-256 fingerprint”](#app-link-verification-and-the-sha-256-fingerprint) An `autoVerify` App Link opens your app only if the Dashboard SHA-256 matches the key that signed **the installed APK**: | How the app is installed | Signed with | Where to read the SHA-256 | | :----------------------------------------------- | :------------------------- | :------------------------------------------------------------------------------------------------------ | | Google Play, any track | Play App Signing key | Play Console → **Protected with Play** → **Manage Play app signing** → *App signing key certificate* | | Android Studio run / `./gradlew assembleDebug` | AGP default debug keystore | `keytool -list -v -keystore ~/.android/debug.keystore -storepass android`, or `./gradlew signingReport` | | `./gradlew assembleRelease` with your own config | Your release keystore | `keytool -list -v -keystore `, or `./gradlew signingReport` | `./gradlew signingReport` prints the `Store:` path each variant really uses, so it stays correct when a project overrides `signingConfigs.debug`. To check a binary directly: `apksigner verify --print-certs app-debug.apk`. Then check what the device made of it: ```bash adb shell pm verify-app-links --re-verify adb shell pm get-app-links # expect: verified ``` The complete procedure is described in [Finding your Android SHA-256 fingerprint](/docs/platform/fundamentals/universal-app-links#finding-your-android-sha-256-fingerprint). ## Common issues [Section titled “Common issues”](#common-issues) `Detour SDK not initialized` `Detour.initialize(...)` was not called before `processLink()` / `getDeferredLink()`. Immediate `NoLink` No matching click was found. Verify the link click happened before the install. App/Universal link returns `NoLink` despite a valid click Your plan’s click limit may be exceeded. Look for `[Detour:CLICK_LIMIT_ERROR]` in logcat. See [Click limits](/docs/sdk/android/sdk-usage#click-limits). Always `NotFirstLaunch` The first-launch flag is already stored. Reinstall the app to test the deferred flow again. App links open the browser instead of the app * Check the Dashboard app-link domain config and the manifest intent-filter alignment. * Confirm the Dashboard SHA-256 matches the key that signed the installed APK. See [App Link verification and the SHA-256 fingerprint](#app-link-verification-and-the-sha-256-fingerprint). Custom scheme not handled Verify the manifest `` entry and that `linkProcessingMode` is `LinkProcessingMode.ALL`. Analytics events not visible Ensure `apiKey` and `appId` are valid, and call `DetourAnalytics` only after SDK initialization. ## Debugging [Section titled “Debugging”](#debugging) Use logcat while testing: ```bash adb logcat | grep -i detour ``` Useful tags from SDK internals: * `Detour` * `DetourApiClient` * `InstallReferrerHelper` * `DetourAnalytics` For complete end-to-end sample flows, use [Examples](/docs/sdk/android/examples). # API Reference > Complete API reference for the Detour Flutter plugin. ## DetourService [Section titled “DetourService”](#detourservice) High-level orchestration layer for routing-safe integration. Extends `ChangeNotifier` so it can be used with Flutter’s `ListenableBuilder` or provider-based state management. ### Constructor [Section titled “Constructor”](#constructor) ```dart DetourService({ Duration duplicateSuppressionWindow = const Duration(seconds: 2), }) ``` * `duplicateSuppressionWindow` * Type `Duration` Default `Duration(seconds: 2)` * Window used to suppress duplicate emissions of the same link (matched by `type|url`, and the URL comparison ignores case). Prevents double-navigation when the OS delivers the same link to both the initial and the runtime path. ### Properties [Section titled “Properties”](#properties) * `isStarted` * Type `bool` * `true` once `start()` has configured the native SDK, until `stop()` is called. While it is `true`, `start()` calls are no-ops. * `isInitialLinkProcessed` * Type `bool` * `true` once the initial link resolve flow has completed, regardless of whether a link was found. Use it to conditionally show a splash screen. * `pendingIntent` * Type [`DetourIntent?`](#detourintent) * Last navigation intent waiting to be consumed. Set when a link is resolved, cleared by `consumePendingIntent()`. Notifies listeners on change. ### Methods [Section titled “Methods”](#methods) #### `start` [Section titled “start”](#start) ```dart Future start(DetourConfig config) ``` Starts the Detour flow: configures the native SDK, subscribes to the runtime link stream, and resolves the initial link. Safe to call multiple times. Only the first call starts the service. After `stop()`, the next call starts it again. #### `processLink` [Section titled “processLink”](#processlink) ```dart Future processLink(String url, {bool emitIntent = true}) ``` Processes an arbitrary URL through the native SDK. When `emitIntent` is `true` (default) and the result contains a link, a new `DetourIntent` with source `manual` is emitted and `pendingIntent` is updated. #### `logEvent` [Section titled “logEvent”](#logevent) ```dart Future logEvent(DetourEventName eventName, {Map? data}) ``` Records a user action using a predefined [`DetourEventName`](#detoureventname). The optional `data` parameter accepts a map of arbitrary metadata. #### `logRetention` [Section titled “logRetention”](#logretention) ```dart Future logRetention(String eventName) ``` Records a retention/lifecycle event with a free-form event name string. #### `consumePendingIntent` [Section titled “consumePendingIntent”](#consumependingintent) ```dart void consumePendingIntent() ``` Marks the current `pendingIntent` as consumed by setting it to `null` and notifying listeners. Call this after your navigation layer has handled the intent to prevent repeated navigation. #### `stop` [Section titled “stop”](#stop) ```dart Future stop() ``` Cancels the runtime link stream subscription and resets the service. `pendingIntent` becomes `null`, `isStarted` and `isInitialLinkProcessed` become `false`, and the recent links held for duplicate suppression are cleared. Listeners are notified. A later `start()` call runs the full start flow again, including [`resolveInitialLink`](#resolveinitiallink). On Android, the native SDK keeps the configuration from the first `start()`. The repeated [`resolveInitialLink`](#resolveinitiallink) follows the rules for repeated calls. ## DetourFlutterPlugin [Section titled “DetourFlutterPlugin”](#detourflutterplugin) Low-level method/event-channel bridge to native SDKs. Use this directly only when you need fine-grained control over the link lifecycle. For most integrations, prefer [`DetourService`](#detourservice). ### Methods [Section titled “Methods”](#methods-1) #### `configure` [Section titled “configure”](#configure) ```dart Future configure(DetourConfig config) ``` Configures the native SDK with credentials and behavioral settings. Must be called before other methods. #### `resolveInitialLink` [Section titled “resolveInitialLink”](#resolveinitiallink) ```dart Future resolveInitialLink() ``` Resolves the link that launched the app (cold start). On iOS, a repeated call returns a result with no link. On Android, a repeated call processes the launch intent again, so it returns the same App Link or custom scheme link as the first call. On both platforms, only the first call can return a deferred link. #### `processLink` [Section titled “processLink”](#processlink-1) ```dart Future processLink(String url) ``` Processes a single URL through the native SDK and returns the result. #### `linkStream` [Section titled “linkStream”](#linkstream) ```dart Stream get linkStream ``` Stream of results for links the app receives while it is running. It carries Universal Links (iOS) and App Links (Android) unless `linkProcessingMode` is `LinkProcessingMode.deferredOnly`, and custom scheme links only when it is `LinkProcessingMode.all`. #### `logEvent` [Section titled “logEvent”](#logevent-1) ```dart Future logEvent(DetourEventName eventName, {Map? data}) ``` Forwards an analytics event to the native SDK. #### `logRetention` [Section titled “logRetention”](#logretention-1) ```dart Future logRetention(String eventName) ``` Forwards a retention event to the native SDK. ## Types [Section titled “Types”](#types) ### DetourConfig [Section titled “DetourConfig”](#detourconfig) ```dart class DetourConfig { final String apiKey; final String appID; final bool shouldUseClipboard; final LinkProcessingMode linkProcessingMode; } ``` * `apiKey` required * Type `String` * Publishable API key from the Detour Dashboard. Sent as a Bearer token on every API call: deferred link matching, short-link resolution, and analytics. * `appID` required * Type `String` * Unique app identifier from the Detour Dashboard. Routes API requests to the correct app. * `shouldUseClipboard` * Type `bool` Default `true` * iOS only. When `true`, the SDK reads the device clipboard as part of probabilistic fingerprinting for deferred link matching. May trigger the iOS system paste permission prompt. * `linkProcessingMode` * Type [`LinkProcessingMode` ](#linkprocessingmode)Default `LinkProcessingMode.all` * Which link sources the SDK processes. See [LinkProcessingMode](#linkprocessingmode) for each value. ### DetourResult [Section titled “DetourResult”](#detourresult) ```dart class DetourResult { final bool processed; final DetourLink? link; } ``` Result returned by link processing methods. * `processed` * Type `bool` * `true` when the native SDK has finished processing, regardless of whether a link was found. * `link` * Type [`DetourLink?`](#detourlink) * Parsed link data, or `null` if no link was matched. ### DetourLink [Section titled “DetourLink”](#detourlink) ```dart class DetourLink { final String url; final String route; final String pathname; final Map params; final LinkType type; } ``` Parsed link data returned inside a `DetourResult`. * `url` * Type `String` * Full URL that was matched or opened. * `route` * Type `String` * In-app route for navigation, as a path with query string, first path segment (app hash) stripped (e.g. `/promo/123?utm=1`). * `pathname` * Type `String` * Route path without query string (e.g. `/promo/123`). * `params` * Type `Map` * Parsed query parameters as key-value pairs. * `type` * Type [`LinkType`](#linktype) * How the link arrived in the app. ### DetourIntent [Section titled “DetourIntent”](#detourintent) ```dart class DetourIntent { final DetourLink link; final DetourIntentSource source; final DateTime receivedAt; } ``` A single navigation intent produced by `DetourService`. Represents a resolved link that is ready to be consumed by the app’s navigation layer. * `link` * Type [`DetourLink`](#detourlink) * The resolved link data. * `source` * Type [`DetourIntentSource`](#detourintentsource) * Origin of the link, which says how it was resolved. * `receivedAt` * Type `DateTime` * Timestamp when the intent was created. ### DetourIntentSource [Section titled “DetourIntentSource”](#detourintentsource) ```dart enum DetourIntentSource { initial, runtime, manual, } ``` Origin of a resolved Detour link emitted by `DetourService`: | Value | Meaning | | --------- | ----------------------------------------------------------------- | | `initial` | Link resolved from cold-start flow via `resolveInitialLink()` | | `runtime` | Link received while the app is running via the native link stream | | `manual` | Link resolved from an explicit `processLink(url)` call | ### LinkType [Section titled “LinkType”](#linktype) ```dart enum LinkType { deferred, verified, scheme, } ``` Describes how the link arrived in the app: | Value | Meaning | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `deferred` | The link was obtained from the Detour deferred-link API. This happens when a user who does not have the app installed clicks a Detour link, installs the app, and opens it. The SDK retrieves the original link from the backend on first launch | | `verified` | The link arrived as a Universal Link (iOS) or App Link (Android), an HTTP/HTTPS URL that the OS verified belongs to your app and delivered directly to it | | `scheme` | The link arrived via a custom URL scheme (e.g. `myapp://path`). These are non-HTTP URLs that the OS routes to your app based on the registered scheme | ### LinkProcessingMode [Section titled “LinkProcessingMode”](#linkprocessingmode) ```dart enum LinkProcessingMode { all, webOnly, deferredOnly, } ``` Controls which link sources the native SDK processes: | Value | Behavior | | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `all` (default) | Processes all link sources: deferred links from the Detour API, Universal Links (iOS) / App Links (Android), and custom scheme deep links | | `webOnly` | Same as `all` but filters out custom scheme links. Only deferred links and HTTP/HTTPS links are processed | | `deferredOnly` | Only processes deferred links from the Detour API. Skips runtime link listener and initial URL check. Recommended when your navigation framework already handles runtime deep links | ### DetourEventName [Section titled “DetourEventName”](#detoureventname) Standard event names for use with `logEvent()`. | Enum value | String value | | :------------------------------------------- | :------------------------------ | | **General** | | | `DetourEventName.login` | `login` | | `DetourEventName.search` | `search` | | `DetourEventName.share` | `share` | | `DetourEventName.signUp` | `sign_up` | | `DetourEventName.tutorialBegin` | `tutorial_begin` | | `DetourEventName.tutorialComplete` | `tutorial_complete` | | `DetourEventName.reEngage` | `re_engage` | | `DetourEventName.invite` | `invite` | | `DetourEventName.openedFromPushNotification` | `opened_from_push_notification` | | **Sales** | | | `DetourEventName.addPaymentInfo` | `add_payment_info` | | `DetourEventName.addShippingInfo` | `add_shipping_info` | | `DetourEventName.addToCart` | `add_to_cart` | | `DetourEventName.removeFromCart` | `remove_from_cart` | | `DetourEventName.refund` | `refund` | | `DetourEventName.viewItem` | `view_item` | | `DetourEventName.beginCheckout` | `begin_checkout` | | `DetourEventName.purchase` | `purchase` | | `DetourEventName.adImpression` | `ad_impression` | ## Notes [Section titled “Notes”](#notes) * `logEvent` accepts only predefined `DetourEventName` enum values. * The Flutter bridge does not expose `mountAnalytics`, `unmountAnalytics`, or `resetSession`. Analytics are managed automatically by the native SDKs. * Every request the native SDK sends to the Detour backend carries an `X-SDK: flutter/` header, used for diagnostics and SDK-usage analysis. No configuration required. * On unsupported platforms (web, macOS, Linux, Windows), the plugin loads a no-op. * `configure` does nothing. * `resolveInitialLink` returns `DetourResult(processed: true)` with no link. * `processLink` returns `DetourResult(processed: false)`. * `linkStream` is empty. * `logEvent` and `logRetention` do nothing. See [Platform support](/docs/sdk/flutter/sdk-installation#platform-support). ## Related pages [Section titled “Related pages”](#related-pages) * [Usage](/docs/sdk/flutter/sdk-usage) * [Examples](/docs/sdk/flutter/examples) * [Testing & Troubleshooting](/docs/sdk/flutter/testing) # Custom Storage > Provide a custom storage adapter for the Detour Flutter plugin. The plugin has no storage adapter. `DetourConfig` takes no storage argument, so you cannot pass a Flutter-level storage engine such as Hive or Secure Storage to Detour. ## Where state is stored [Section titled “Where state is stored”](#where-state-is-stored) Detour persists two values, a first-entrance flag and an analytics device id, and each native SDK keeps them in its own default store: * **Android** — `SharedPreferences`, in a file named `Detour`, under the keys `Detour_firstEntranceFlag` and `Detour_deviceId`. * **iOS** — `UserDefaults.standard`, under the keys `DetourFirstEntranceFlag` and `Detour_deviceId`. ## What this means [Section titled “What this means”](#what-this-means) * Deferred-link first-launch behavior follows the native persisted state, not anything your Dart code holds. * Clearing app data or reinstalling resets both values. That is how you retest a deferred flow. See [Testing & Troubleshooting](/docs/sdk/flutter/testing). Custom storage The Android native SDK accepts a custom `DetourStorage`, but the Flutter bridge does not forward one. It cannot be configured from Dart. ## Related pages [Section titled “Related pages”](#related-pages) * [Usage](/docs/sdk/flutter/sdk-usage) * [API Reference](/docs/sdk/flutter/api-reference) # Examples > A runnable Detour Flutter example app with end-to-end deep link integration. The plugin repository ships one runnable app: a full `DetourService` integration that handles deferred, Universal/App, and custom scheme links on both Android and iOS. Clone it, run it, and copy what you need. The card below links to the example’s `README.md` on GitHub, with the setup steps and the full source. ## Catalog [Section titled “Catalog”](#catalog) [example ](https://github.com/software-mansion-labs/detour-flutter-plugin/blob/main/example/README.md)A complete DetourService integration for Android and iOS, with deep link configuration for both platforms and analytics calls. Start here for a production-like setup. ## Where to go next [Section titled “Where to go next”](#where-to-go-next) 1. Add the plugin to your own app. See [Installation](/docs/sdk/flutter/sdk-installation). 2. Learn the service lifecycle and routing model the example uses. See [Usage](/docs/sdk/flutter/sdk-usage). 3. Adapt the example to your package ID, bundle ID, scheme, and Detour domain. 4. Check the result. See [Testing & Troubleshooting](/docs/sdk/flutter/testing). # Installation > Install the Detour Flutter plugin and configure Android and iOS deep linking. Detour provides a Flutter plugin (`detour_flutter_plugin`) that bridges the native Detour SDKs on Android and iOS, so both builds share one Dart integration. AI agent skills Detour ships two AI coding-agent skills: `detour-onboarding` for a setup from scratch, and `migrate-to-detour` for moving off Branch or AppsFlyer. Both cover this page. See [Detour Skills](/docs/platform/fundamentals/skills). A Detour app in the Dashboard is required before the SDK can resolve links. See [Getting Started](/docs/platform/fundamentals/getting-started). ## Requirements [Section titled “Requirements”](#requirements) * Flutter: `>=3.3.0` * Dart: `^3.11.1` * Android min SDK: `24` * iOS: `13.0+` Links are processed on **Android and iOS** only. On web, macOS, Linux, and Windows the plugin loads a no-op so a multi-platform codebase keeps building. See [Platform support](#platform-support). ## Install package [Section titled “Install package”](#install-package) Add the package to your `pubspec.yaml`: pubspec.yaml ```yaml dependencies: detour_flutter_plugin: ^1.2.1 ``` Then run: ```bash flutter pub get ``` ## Native SDK dependencies [Section titled “Native SDK dependencies”](#native-sdk-dependencies) `detour_flutter_plugin` wraps the native Detour SDKs: * The Android bridge depends on `com.swmansion.detour:detour-sdk:1.1.0`. * The iOS bridge depends on the `Detour` CocoaPod (`~> 1.1.0`), pulled in automatically by the plugin podspec. Keep `google()` and `mavenCentral()` in your project repositories so Gradle can resolve the Android dependency. After changing iOS dependencies, run: ```bash cd ios pod install cd .. ``` iOS privacy manifest The `Detour` pod ships a `PrivacyInfo.xcprivacy` manifest from `1.1.1`, and the `~> 1.1.0` constraint above resolves to it, so a fresh `pod install` picks it up with no extra work. A project whose `Podfile.lock` still pins `1.1.0` does not have it. Run `pod update Detour` to move to `1.1.1`. You still need to answer the App Store Connect privacy questionnaire for what Detour collects. The answers are listed in the [iOS SDK privacy reference](/docs/sdk/ios/privacy#app-store-connect-questionnaire) and apply unchanged to a Flutter app. ## Credentials [Section titled “Credentials”](#credentials) Both values come from **API configuration** in the Dashboard: the `appID` and the publishable `apiKey`. See [Getting Started](/docs/platform/fundamentals/getting-started#api-configuration). Pass your credentials to `DetourConfig`: ```dart const DetourConfig( apiKey: '', appID: '', shouldUseClipboard: true, linkProcessingMode: LinkProcessingMode.all, ) ``` Keep `apiKey` and `appID` in environment variables or another secure configuration layer. ## Platform integration [Section titled “Platform integration”](#platform-integration) The plugin wires up the bridge and runtime listeners, but your app still needs native deep-link registration. The Dashboard generates the Associated Domains and App Links configuration for verified HTTPS links. It does not generate custom-scheme registration. ### Android [Section titled “Android”](#android) #### Verified App Links [Section titled “Verified App Links”](#verified-app-links) Paste the generated App Links filter into the `MainActivity` declaration in `android/app/src/main/AndroidManifest.xml`: android/app/src/main/AndroidManifest.xml ```xml ``` Use the exact host and paths generated in **App configuration**. New short links reuse `/` and do not require another manifest entry or native build. If your app is served from a [custom domain](/docs/platform/advanced/custom-domain), the Dashboard generates the filter for that domain. Assign the domain before you copy the values. Changing the host an app is assigned to later needs a new build. Do not add hosts it did not generate: on Android 11 and lower that breaks App Link verification for every host at once, see [Verification](/docs/platform/fundamentals/universal-app-links#verification). #### Custom-scheme links (optional) [Section titled “Custom-scheme links (optional)”](#custom-scheme-links-optional) Add a separate filter to the same activity only if you use links such as `myapp://product/42`: android/app/src/main/AndroidManifest.xml ```xml ``` #### Callback delivery [Section titled “Callback delivery”](#callback-delivery) No Android callback override is required. The plugin registers itself as a Flutter `NewIntentListener`, processes the activity’s initial intent, and receives later intents from the Flutter embedding. ### iOS [Section titled “iOS”](#ios) #### Universal Links [Section titled “Universal Links”](#universal-links) In Xcode, open the Runner target, select **Signing & Capabilities**, add the **Associated Domains** capability, and enter `applinks:`. The resulting `Runner.entitlements` entry is: ios/Runner/Runner.entitlements ```xml com.apple.developer.associated-domains applinks: ``` With a [custom domain](/docs/platform/advanced/custom-domain), the Dashboard generates this entry for that domain too. #### Custom-scheme links (optional) [Section titled “Custom-scheme links (optional)”](#custom-scheme-links-optional-1) If you use scheme links, register the scheme under the Runner target’s **Info** → **URL Types**, or add it directly to `ios/Runner/Info.plist`: ios/Runner/Info.plist ```xml CFBundleURLTypes CFBundleURLSchemes myapp ``` #### Delegate forwarding [Section titled “Delegate forwarding”](#delegate-forwarding) Flutter plugins, Detour included, receive lifecycle callbacks through the Flutter delegate chain. If you override an iOS deep-link callback, return the `super` result: ios/Runner/AppDelegate.swift ```swift override func application( _ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void ) -> Bool { return super.application( application, continue: userActivity, restorationHandler: restorationHandler ) } override func application( _ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:] ) -> Bool { return super.application(app, open: url, options: options) } ``` If your project defines a custom `SceneDelegate`, subclass `FlutterSceneDelegate` (recommended) or forward scene events through `FlutterSceneLifeCycleProvider`. An app using Flutter’s default scene delegate does not need a custom class or callback overrides. If your `FlutterSceneDelegate` subclass overrides either link callback, call `super` so Flutter can forward it to Detour: ios/Runner/SceneDelegate.swift ```swift override func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { super.scene(scene, continue: userActivity) } override func scene( _ scene: UIScene, openURLContexts URLContexts: Set ) { super.scene(scene, openURLContexts: URLContexts) } ``` After changing either native target, rebuild the app. A Dart hot reload does not apply entitlements, `Info.plist`, or manifest changes. Reference implementations in the example app: * [AppDelegate.swift](https://github.com/software-mansion-labs/detour-flutter-plugin/blob/main/example/ios/Runner/AppDelegate.swift) * [SceneDelegate.swift](https://github.com/software-mansion-labs/detour-flutter-plugin/blob/main/example/ios/Runner/SceneDelegate.swift) Clipboard reads on iOS 14+ Clipboard matching checks `detectPatterns` before it touches the pasteboard. The clipboard value is read only when a probable URL is detected. ## Platform support [Section titled “Platform support”](#platform-support) The plugin processes links on **Android** and **iOS**. On every other platform, that is web, macOS, Linux, and Windows, it loads a no-op implementation, so apps that target several platforms build and run without conditional imports: * `configure(...)` does nothing and logs `Detour: current platform is not supported, skipping initialization`. * `resolveInitialLink()` returns a result with `processed: true` and no link, so your startup flow continues normally. * `processLink(...)` returns a result with `processed: false`. * `linkStream` is an empty stream. * `logEvent(...)` and `logRetention(...)` do nothing. Call the SDK unconditionally from shared code. The no-op keeps unsupported platforms safe. ## Next steps [Section titled “Next steps”](#next-steps) 1. Wire up the startup and runtime flow in [Usage](/docs/sdk/flutter/sdk-usage). 2. Start from the runnable app in [Examples](/docs/sdk/flutter/examples). 3. Check the result. See [Testing & Troubleshooting](/docs/sdk/flutter/testing). # Usage > Start DetourService, consume pending intents, and handle initial and runtime links in your Flutter app. Detour delivers links to your app through a single object. `DetourService` starts the native bridge, collects every resolved link, whether deferred, Universal/App, or custom scheme, into one pending intent, and notifies you when a link arrives. You read the intent, navigate, and mark it consumed. ## Service setup [Section titled “Service setup”](#service-setup) `DetourService` is the recommended orchestration layer: * It configures the native bridge once. * It merges initial and runtime links into a single pending intent. * `isInitialLinkProcessed` tells you when the startup flow has finished. * `consumePendingIntent()` clears the intent explicitly. An intent stays pending until you clear it. * The same link arriving twice within 2 seconds is emitted once. ```dart import 'package:detour_flutter_plugin/detour_flutter_plugin.dart'; import 'package:flutter/material.dart'; class MyApp extends StatefulWidget { const MyApp({super.key}); @override State createState() => _MyAppState(); } class _MyAppState extends State { final detour = DetourService(); @override void initState() { super.initState(); detour.addListener(_onDetourChanged); _startDetour(); } Future _startDetour() async { await detour.start( const DetourConfig( apiKey: '', appID: '', shouldUseClipboard: true, linkProcessingMode: LinkProcessingMode.all, ), ); } void _onDetourChanged() { final intent = detour.pendingIntent; if (intent == null) return; // Route once, then mark as consumed. // context.go(intent.link.route); detour.consumePendingIntent(); } } ``` Start is idempotent `start()` is safe to call more than once. Until `stop()` resets the service, only the first call configures the native SDK and subscribes to the link stream. For a complete, runnable integration, see [Examples](/docs/sdk/flutter/examples). ## Handle resolved links [Section titled “Handle resolved links”](#handle-resolved-links) `DetourService` extends `ChangeNotifier`. Every time a link resolves, listeners fire and `pendingIntent` holds the result until you consume it. A `DetourIntent` carries three fields: * `intent.link` — the resolved link (see below) * `intent.source` — where it came from, one of `initial`, `runtime`, or `manual` * `intent.receivedAt` — when the service registered it Call `consumePendingIntent()` after you navigate. Until you do, `pendingIntent` keeps returning the same intent, and a rebuild would route again. ### `link` structure [Section titled “link structure”](#link-structure) * `link.route` * Type `String` * Full in-app route, query string included: `/details?utm=1`. * `link.pathname` * Type `String` * Path without the query: `/details`. * `link.params` * Type `Map` * Query params, already parsed. * `link.url` * Type `String` * The original URL. * `link.type` * Type [`LinkType`](/docs/sdk/flutter/api-reference#linktype) * Where the link came from: `deferred`, `verified`, or `scheme`. ## Link processing modes [Section titled “Link processing modes”](#link-processing-modes) `DetourConfig.linkProcessingMode` controls which link sources the plugin handles: | Value | Deferred | Universal / App | Custom scheme | | ---------------------------------- | -------- | --------------- | ------------- | | `LinkProcessingMode.all` (default) | Yes | Yes | Yes | | `LinkProcessingMode.webOnly` | Yes | Yes | No | | `LinkProcessingMode.deferredOnly` | Yes | No | No | Use `deferredOnly` when your navigation framework already handles runtime and initial URLs. ## Manual processing [Section titled “Manual processing”](#manual-processing) You can pass any URL to the plugin yourself: ```dart final result = await detour.processLink( 'https://example.godetour.link/abc123?source=push', ); if (result.link != null) { // Navigate with result.link!.route } ``` `processLink` returns a `DetourResult` with two fields: * `processed` — whether the native SDK ran the URL through link resolution * `link` — the resolved link, or `null` The result is also pushed into the service as a pending intent. Pass `emitIntent: false` to inspect it without triggering your routing listener. ## Low-level API [Section titled “Low-level API”](#low-level-api) If you need full manual control, use `DetourFlutterPlugin` instead of `DetourService`. You are then responsible for configuration, the initial resolve, and the runtime stream. ```dart final plugin = DetourFlutterPlugin(); await plugin.configure( const DetourConfig( apiKey: '', appID: '', ), ); final initial = await plugin.resolveInitialLink(); final stream = plugin.linkStream; final processed = await plugin.processLink('https://example.godetour.link/path'); ``` ## Click limits [Section titled “Click limits”](#click-limits) Before it delivers an App Link or Universal Link, the native SDK checks the click against your plan’s limit. Within the limit, links resolve normally. ### Over the limit [Section titled “Over the limit”](#over-the-limit) The link is **not** delivered, and the plugin reports nothing. The native result comes back without a link, so `DetourService` registers no pending intent and your listener never fires. `processLink()` returns a `DetourResult` with `processed: true` and `link: null`, the same shape as a URL that had no match. The only signal is in the native log, where the SDK writes `[Detour:CLICK_LIMIT_ERROR]` with `clicksInPeriod` and `effectiveLimit`: ```bash adb logcat | grep -i detour # Android xcrun simctl spawn booted log stream \ --predicate 'subsystem == "com.swmansion.detour"' # iOS simulator ``` ### Other link types [Section titled “Other link types”](#other-link-types) This check applies only to App Links and Universal Links. * **Deferred and short links** — the same plan limits apply on the backend. On iOS no click-limit error is raised, an over-limit link fails to resolve, and `link` stays `null`. On Android an over-limit deferred match makes `resolveInitialLink()` raise [`NATIVE_ERROR`](/docs/sdk/flutter/testing#native-error) with a `[402]` message. A short link opened as an App Link returns no link and no error. * **Custom scheme links** — resolved on device, never limit-checked. Fail-open by design If the limit check cannot complete because of a network or backend error, the SDK delivers the link, so a temporary outage does not block navigation. Limits are enforced only on an explicit “denied” response. For per-plan limits, pay-as-you-go overage, and how to raise them, see [Billing & Payments](/docs/platform/fundamentals/billing). ## Analytics [Section titled “Analytics”](#analytics) Starting the service turns analytics on. With `start()` called, the native SDK logs one event by itself: `app_open`, a retention event sent once per session on cold start. Everything else you log yourself: ```dart await detour.logEvent( DetourEventName.purchase, data: {'value': 9.99, 'currency': 'USD'}, ); await detour.logRetention('home_screen_viewed'); ``` `logEvent` takes a `DetourEventName` value, so you can log only the predefined event names. `logRetention` takes any string. The full enum is in the [API Reference](/docs/sdk/flutter/api-reference). SDK version header Every request the SDK sends carries an `X-SDK: flutter/` header, used for diagnostics and SDK-usage analysis. It needs no configuration. ## Related pages [Section titled “Related pages”](#related-pages) * [Examples](/docs/sdk/flutter/examples) * [API Reference](/docs/sdk/flutter/api-reference) * [Custom Storage](/docs/sdk/flutter/custom-storage) * [Testing & Troubleshooting](/docs/sdk/flutter/testing) # Testing & Troubleshooting > Test deferred linking on Android and iOS in Flutter and troubleshoot common Detour Flutter plugin issues. Deferred flow testing has platform constraints: full attribution path depends on store redirect chain. ## Deferred flow constraints [Section titled “Deferred flow constraints”](#deferred-flow-constraints) * Deferred matching is first-launch oriented. * Runtime callbacks do not re-trigger deferred matching logic. * The Flutter bridge does not expose a session reset API. ## Recommended deferred test flow [Section titled “Recommended deferred test flow”](#recommended-deferred-test-flow) 1. Uninstall the app from the device or simulator. 2. Open a Detour link. 3. Install and launch the app. 4. Verify the initial intent and the resolved route in your UI logs. 5. Relaunch the app and confirm the deferred behavior is not repeated. ## Runtime link tests [Section titled “Runtime link tests”](#runtime-link-tests) ### Android App Link [Section titled “Android App Link”](#android-app-link) ```bash adb shell am start -a android.intent.action.VIEW \ -d "https:///" \ com.example.app ``` ### Android custom scheme [Section titled “Android custom scheme”](#android-custom-scheme) ```bash adb shell am start -a android.intent.action.VIEW \ -d "myapp://product/42" \ com.example.app ``` ### iOS custom scheme (simulator) [Section titled “iOS custom scheme (simulator)”](#ios-custom-scheme-simulator) ```bash xcrun simctl openurl booted "myapp://product/42" ``` ### iOS Universal Link (simulator) [Section titled “iOS Universal Link (simulator)”](#ios-universal-link-simulator) ```bash xcrun simctl openurl booted "https:///" ``` ## Platform-specific notes [Section titled “Platform-specific notes”](#platform-specific-notes) ### iOS [Section titled “iOS”](#ios) Copy the link before installing Clipboard matching requires that you copy the link to your clipboard before installing. Ensure `shouldUseClipboard: true` is set in `DetourConfig` and that the copy-link feature is enabled in the Detour Dashboard for your iOS configuration. ### Android [Section titled “Android”](#android) Install referrer `click_id` is often unavailable in local/dev-store scenarios. In that case matching falls back to probabilistic flow. For local install referrer testing workarounds, see [How to test the Play Store Install Referrer API](https://medium.com/@madicdjordje/how-to-test-the-play-store-install-referrer-api-78a63d59945b). Missing `click_id` If you encounter click\_id missing in your production build see limitations section [Play Store referrer missing](/docs/platform/architecture/architecture-limitations#play-store-referrer-missing) ### Testing Android App Links [Section titled “Testing Android App Links”](#testing-android-app-links) If an installed app does not open Detour links, the Dashboard SHA-256 probably does not match the key that signed **the build on your device**: | How you install the app | SHA-256 to use | Where to find it | | :------------------------------------------ | :------------------------- | :------------------------------------------------------------------------------------------------------ | | Google Play, any track | Play App Signing key | Play Console → **Protected with Play** → **Manage Play app signing** → *App signing key certificate* | | `flutter run` / `flutter build apk --debug` | AGP default debug keystore | `keytool -list -v -keystore ~/.android/debug.keystore -storepass android`, or `./gradlew signingReport` | | Release build with your own keystore | Your release keystore | `keytool -list -v -keystore ` | The Flutter template signs `release` with the debug config until you add your own release keystore, so a fresh `flutter build apk --release` is still debug-signed. To check any build directly: `apksigner verify --print-certs build/app/outputs/flutter-apk/app-debug.apk`. The complete procedure is described in [Finding your Android SHA-256 fingerprint](/docs/platform/fundamentals/universal-app-links#finding-your-android-sha-256-fingerprint). ## Common issues [Section titled “Common issues”](#common-issues) No match found (`404`) * Check timestamps. The click may be older than the match window (15 minutes). * Confirm the click was recorded in your Detour Dashboard. * On the Android deterministic flow, confirm `click_id` is present in the install referrer. App/Universal link never arrives despite a valid click Your plan’s click limit may be exceeded. The plugin surfaces nothing in Dart, because the intent never arrives, so check the native log for `[Detour:CLICK_LIMIT_ERROR]`. See [Click limits](/docs/sdk/flutter/sdk-usage#click-limits). `429 Too Many Requests` Detour limits deferred link matching per device and IP address, and per app. Wait between reinstall tests on one device. For tests on many devices at once, create a separate app for testing in the Dashboard, because each app has its own credentials and its own limit. `NOT_CONFIGURED` platform error `resolveInitialLink()` and `processLink()` raise it on both platforms. On Android, `logEvent()` and `logRetention()` raise it too. **Cause** — the method ran before a successful `configure(...)` call. **Fix** — await `DetourService.start(...)` or `DetourFlutterPlugin.configure(...)` before you call these methods. If `configure(...)` failed, fix that error first. `INVALID_ARGS` platform error Android only. `configure(...)` raises it when `apiKey` or `appID` is empty, `processLink(...)` when `url` is empty, and `logRetention(...)` when `eventName` is empty. A value that contains only whitespace counts as empty. **Cause** — a required string argument is empty. **Fix** — pass non-empty values. For where `apiKey` and `appID` come from, see [Credentials](/docs/sdk/flutter/sdk-installation#credentials). `NO_ACTIVITY` platform error **Cause** — on Android, `configure(...)` ran in a Flutter engine that has no attached activity, such as a separate engine started for background work. **Fix** — call `DetourService.start(...)` or `DetourFlutterPlugin.configure(...)` from your app’s UI code, not from a background handler. `NATIVE_ERROR` platform error Android only. `resolveInitialLink()` and `processLink()` raise it, and `linkStream` emits it as an error event, when the native SDK fails to process a link. `DetourService.start()` throws the same error and leaves `isInitialLinkProcessed` at `false`, so catch it if your startup flow waits for that flag. A failed deferred match request on first launch raises it. When the server responded, the message starts with the HTTP status code: * `[401]` — the `apiKey` or `appID` is wrong. See [Credentials](/docs/sdk/flutter/sdk-installation#credentials). * `[402]` — your organization is over its plan’s click limit. See [Click limits](/docs/sdk/flutter/sdk-usage#click-limits). * `[429]` — too many match requests. See [`429 Too Many Requests`](#too-many-requests). * No status code — the request did not complete. Check the network connection. iOS clipboard not read * Ensure `shouldUseClipboard: true` is set in `DetourConfig`. * Ensure the copy-link feature is enabled in the Detour Dashboard for your iOS app configuration. * The link must be copied to the clipboard before the app is opened, because the clipboard is only read during `resolveInitialLink()`. Deferred link not returned on subsequent test runs Deferred matching is first-launch based. Reinstall the app for a clean re-test. Android release build fails with `Type com.swmansion.detour.BuildConfig is defined multiple times` Debug builds of the same project succeed. **Cause** — `detour_flutter_plugin` `1.1.1` and `1.2.0` use the Android namespace `com.swmansion.detour`, which the native Detour SDK also uses. The release build then contains two classes named `com.swmansion.detour.BuildConfig`. **Fix** — upgrade to `detour_flutter_plugin: ^1.2.1` and run `flutter pub get`. ## Debugging [Section titled “Debugging”](#debugging) * Android: * `adb logcat | grep -i detour` * iOS simulator: * `xcrun simctl spawn booted log stream --predicate 'subsystem == "com.swmansion.detour"'` For full runnable setup and commands, use [Examples](/docs/sdk/flutter/examples). # API Reference > Complete API reference for the Detour iOS SDK. ## Detour [Section titled “Detour”](#detour) Main SDK singleton, accessed via `Detour.shared`. All methods are `@MainActor`. ### Link processing methods [Section titled “Link processing methods”](#link-processing-methods) #### `processLink(_:)` [Section titled “processLink(\_:)”](#processlink_) Synchronous. Converts a URL into a `DetourResult`. ```swift func processLink(_ url: URL) -> DetourResult ``` The result carries the parsed route, pathname, params, and link type. Short links are not resolved. Infrastructure URLs, for example Expo dev client URLs, return an empty result. #### `processLink(_:config:)` with a `URL` [Section titled “processLink(\_:config:) with a URL”](#processlink_config-with-a-url) Async. Processes a URL you receive at runtime, and resolves short links when you pass `config`. ```swift func processLink( _ url: URL, config: DetourConfig? ) async -> DetourResult ``` Call it from `onOpenURL` in SwiftUI or `application(_:open:)` in UIKit. For a Universal Link with a non-`nil` `config`, the SDK first checks the click against your plan’s click limit. Over the limit, the link is not delivered and `result.link` is `nil`. See [Click limits](/docs/sdk/ios/sdk-usage#click-limits). #### `processLink(_:config:)` with a `String` [Section titled “processLink(\_:config:) with a String”](#processlink_config-with-a-string) Async. The same as the `URL` variant, for a raw link string. ```swift func processLink( _ rawLink: String, config: DetourConfig? = nil ) async -> DetourResult ``` Accepts full URLs and plain paths. With `config`, a single-segment web path, which is a short link, is resolved to its full destination URL. #### `resolveInitialLink(config:launchOptions:completion:)` [Section titled “resolveInitialLink(config:launchOptions:completion:)”](#resolveinitiallinkconfiglaunchoptionscompletion) UIKit. Resolves the link that launched the app. Call it from `application(_:didFinishLaunchingWithOptions:)`. ```swift func resolveInitialLink( config: DetourConfig, launchOptions: [UIApplication.LaunchOptionsKey: Any]?, completion: @escaping @Sendable (DetourResult) -> Void ) ``` It checks, in order: 1. **Launch URL** from `launchOptions` 2. **Universal Link** from user activities in `launchOptions` 3. **Deferred link** from the Detour API (first install only) It runs once per app session, and it mounts analytics. #### `resolveInitialLink(config:connectionOptions:completion:)` [Section titled “resolveInitialLink(config:connectionOptions:completion:)”](#resolveinitiallinkconfigconnectionoptionscompletion) SceneDelegate and SwiftUI. The same as the UIKit variant, for scene-based apps. ```swift func resolveInitialLink( config: DetourConfig, connectionOptions: UIScene.ConnectionOptions, completion: @escaping @Sendable (DetourResult) -> Void ) ``` It takes `UIScene.ConnectionOptions`. Call it from `scene(_:willConnectTo:options:)` or the SwiftUI app lifecycle. ### Analytics methods [Section titled “Analytics methods”](#analytics-methods) #### `mountAnalytics(config:)` [Section titled “mountAnalytics(config:)”](#mountanalyticsconfig) Mounts the analytics system. ```swift func mountAnalytics(config: DetourConfig) ``` `resolveInitialLink` calls it for you. Call it yourself only if you use `processLink` without `resolveInitialLink`. #### `unmountAnalytics()` [Section titled “unmountAnalytics()”](#unmountanalytics) Unmounts the analytics system and stops listening for events. ```swift func unmountAnalytics() ``` ### Utility methods [Section titled “Utility methods”](#utility-methods) #### `resetSession(allowDeferredRetry:)` [Section titled “resetSession(allowDeferredRetry:)”](#resetsessionallowdeferredretry) Resets the session state, so `resolveInitialLink` can run again. ```swift func resetSession(allowDeferredRetry: Bool = false) ``` With `allowDeferredRetry: true` it also resets the first-launch flag. Use it to test the deferred link flow locally. ## DetourConfig [Section titled “DetourConfig”](#detourconfig) SDK configuration. All properties are immutable and the struct conforms to `Sendable`. ```swift public struct DetourConfig: Sendable { public let apiKey: String public let appID: String public let shouldUseClipboard: Bool public let linkProcessingMode: LinkProcessingMode } ``` ### Initializer [Section titled “Initializer”](#initializer) ```swift DetourConfig( apiKey: String, appID: String, shouldUseClipboard: Bool = true, linkProcessingMode: LinkProcessingMode = .all ) ``` * `apiKey` required * Type `String` * Publishable API key from the Detour Dashboard. Sent as a Bearer token on every API call: deferred link matching, short-link resolution, and analytics. * `appID` required * Type `String` * Unique app identifier from the Detour Dashboard. Routes API requests to the correct app. * `shouldUseClipboard` * Type `Bool` Default `true` * When `true`, the SDK reads the device clipboard and includes it in the probabilistic fingerprint for deferred link matching. May trigger the iOS system paste permission prompt. Set it to `false` to skip clipboard access. * `linkProcessingMode` * Type [`LinkProcessingMode` ](#linkprocessingmode)Default `.all` * Which link sources the SDK processes. See [LinkProcessingMode](#linkprocessingmode) for each value. ## DetourResult [Section titled “DetourResult”](#detourresult) Result returned by all link processing methods. Contains a processing flag and an optional parsed link. ```swift public struct DetourResult: Sendable { public let processed: Bool public let link: DetourLink? } ``` * `processed` * Type `Bool` * `true` when the SDK has finished processing the link, regardless of whether a link was found. * `link` * Type [`DetourLink?`](#detourlink) * Parsed link data, or `nil` if no link was matched. ### Convenience computed properties [Section titled “Convenience computed properties”](#convenience-computed-properties) * `route` * Type `String?` * Shorthand for `link?.route`, the full in-app route including query string. * `pathname` * Type `String?` * Shorthand for `link?.pathname`, the path without query string. * `params` * Type `[String: String]` * Shorthand for `link?.params ?? [:]`, the parsed query parameters. * `linkType` * Type [`LinkType?`](#linktype) * Shorthand for `link?.type`, which says how the link arrived. * `linkURL` * Type `URL?` * `link?.url` parsed into a `URL`. `nil` when there is no link or the string is not a valid URL. ### Factory method [Section titled “Factory method”](#factory-method) ```swift static func empty() -> DetourResult ``` Returns a result with `processed: true` and `link: nil`. Used internally when no link is matched. ## DetourLink [Section titled “DetourLink”](#detourlink) Parsed link data returned inside a `DetourResult`. ```swift public struct DetourLink: Sendable { public let url: String public let route: String public let pathname: String public let params: [String: String] public let type: LinkType } ``` * `url` * Type `String` * Full URL string that was matched or opened. * `route` * Type `String` * In-app route for navigation, as a path with query string, first path segment (app hash) stripped (e.g. `/promo/123?utm=1`). * `pathname` * Type `String` * Route path without query string (e.g. `/promo/123`). * `params` * Type `[String: String]` * Parsed query parameters as key-value pairs. * `type` * Type [`LinkType`](#linktype) * How the link arrived in the app. ## LinkProcessingMode [Section titled “LinkProcessingMode”](#linkprocessingmode) ```swift public enum LinkProcessingMode: String, Sendable { case all case webOnly = "web-only" case deferredOnly = "deferred-only" } ``` Controls which link sources are processed: | Value | Behavior | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `.all` (default) | Processes all link sources: deferred links from the Detour API, Universal Links (verified HTTP/HTTPS URLs), and custom scheme deep links | | `.webOnly` | Same as `.all` but filters out custom scheme links. Only deferred links and Universal Links (HTTP/HTTPS) are processed | | `.deferredOnly` | Only processes deferred links from the Detour API. Skips Universal Link and scheme link detection in `resolveInitialLink`. Recommended when your app handles runtime links separately | ## LinkType [Section titled “LinkType”](#linktype) ```swift public enum LinkType: String, Codable, Sendable { case deferred case verified case scheme } ``` Describes how the link arrived in the app: | Value | Meaning | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `.deferred` | The link was obtained from the Detour deferred-link API. This happens when a user who does not have the app installed clicks a Detour link, installs the app, and opens it. The SDK retrieves the original link from the backend on first launch | | `.verified` | The link arrived as a Universal Link, an HTTP/HTTPS URL that iOS verified belongs to your app via the apple-app-site-association file and delivered directly to it | | `.scheme` | The link arrived via a custom URL scheme (e.g. `myapp://path`). These are non-HTTP URLs that iOS routes to your app based on the registered scheme | ## DetourAnalytics [Section titled “DetourAnalytics”](#detouranalytics) Analytics module for logging events to the Detour backend. Accessed via static methods on the `DetourAnalytics` class. Analytics are automatically mounted when calling `resolveInitialLink`. Mount manually only if you use `processLink` without `resolveInitialLink`. The SDK automatically logs an `app_open` retention event when analytics are first mounted in a session. Universal Link opens are recorded server-side through a dedicated click endpoint (see [Click limits](/docs/sdk/ios/sdk-usage#click-limits)) rather than as a client event. All API requests the SDK sends also include an `X-SDK: ios/` header for diagnostics and SDK-usage analysis. ### Methods [Section titled “Methods”](#methods) #### `mount(config:)` [Section titled “mount(config:)”](#mountconfig) Mounts the analytics system and subscribes to events. A second call does nothing. ```swift static func mount(config: DetourConfig) ``` #### `unmount()` [Section titled “unmount()”](#unmount) Unmounts the analytics system and stops listening for events. ```swift static func unmount() ``` #### `logEvent(_:data:)` with a `DetourEventName` [Section titled “logEvent(\_:data:) with a DetourEventName”](#logevent_data-with-a-detoureventname) Records a user action under a predefined [event name](#detoureventname). ```swift static func logEvent( _ eventName: DetourEventName, data: [String: Any]? = nil ) ``` `data` takes a dictionary of any metadata. #### `logEvent(_:data:)` with a `String` [Section titled “logEvent(\_:data:) with a String”](#logevent_data-with-a-string) Records a user action under a custom event name. ```swift static func logEvent( _ eventName: String, data: [String: Any]? = nil ) ``` #### `logRetention(_:)` [Section titled “logRetention(\_:)”](#logretention_) Records a retention event. ```swift static func logRetention(_ eventName: String) ``` ## DetourEventName [Section titled “DetourEventName”](#detoureventname) | Category | Event Name | Raw value | | :---------- | :---------------------------- | :------------------------------ | | **General** | `.login` | `login` | | | `.search` | `search` | | | `.share` | `share` | | | `.signUp` | `sign_up` | | | `.tutorialBegin` | `tutorial_begin` | | | `.tutorialComplete` | `tutorial_complete` | | | `.reEngage` | `re_engage` | | | `.invite` | `invite` | | | `.openedFromPushNotification` | `opened_from_push_notification` | | **Sales** | `.addPaymentInfo` | `add_payment_info` | | | `.addShippingInfo` | `add_shipping_info` | | | `.addToCart` | `add_to_cart` | | | `.removeFromCart` | `remove_from_cart` | | | `.refund` | `refund` | | | `.viewItem` | `view_item` | | | `.beginCheckout` | `begin_checkout` | | | `.purchase` | `purchase` | | | `.adImpression` | `ad_impression` | ## Related pages [Section titled “Related pages”](#related-pages) * [Usage](/docs/sdk/ios/sdk-usage) * [Examples](/docs/sdk/ios/examples) * [Testing & Troubleshooting](/docs/sdk/ios/testing) # Custom Storage > Provide a custom storage adapter (Keychain or your own) for the Detour iOS SDK. The iOS SDK persists two internal values and keeps both in `UserDefaults`. From **1.1.0** the storage engine is fixed: `DetourConfig` takes no storage adapter, so no storage adapter can be supplied. ## What the SDK stores [Section titled “What the SDK stores”](#what-the-sdk-stores) * `DetourFirstEntranceFlag` — set on the first launch, so deferred matching runs once per install * `Detour_deviceId` — a UUID generated the first time analytics send an event Both are stored in `UserDefaults.standard` and are preserved across app restarts. Deleting the app clears them. ## Reset for testing [Section titled “Reset for testing”](#reset-for-testing) To run the deferred flow again on the same install: ```swift Detour.shared.resetSession(allowDeferredRetry: true) ``` This clears the in-memory session guard and the first-entrance flag, so the next `resolveInitialLink` call attempts a deferred match again. The analytics device id is not changed. ## Related pages [Section titled “Related pages”](#related-pages) * [Usage](/docs/sdk/ios/sdk-usage) * [API Reference](/docs/sdk/ios/api-reference) * [Testing & Troubleshooting](/docs/sdk/ios/testing) # Examples > Runnable Detour iOS example apps for SwiftUI and UIKit integration. The iOS SDK repository ships one runnable app that covers all three link types. Open it in Xcode, fill in the placeholders, run it, and copy what you need. The card below links to the example’s `README.md` on GitHub, with the setup steps and full source. ## Catalog [Section titled “Catalog”](#catalog) [ExampleUsage ](https://github.com/software-mansion-labs/ios-detour/blob/main/ExampleUsage/README.md)A SwiftUI app that handles deferred links, Universal Links and custom scheme links, with analytics calls. Fill in the placeholders and run it. ## Where to go next [Section titled “Where to go next”](#where-to-go-next) 1. Add the SDK to your own app. See [Installation](/docs/sdk/ios/sdk-installation). 2. Learn the API the example uses. See [Usage](/docs/sdk/ios/sdk-usage). 3. Swap the template’s bundle ID, domain, and route mapping for your own. 4. Check the result. See [Testing & Troubleshooting](/docs/sdk/ios/testing). # Privacy > The privacy manifest the Detour iOS SDK ships, what it declares, and how to answer the App Store Connect privacy questionnaire. Apple requires every third-party SDK to declare the APIs it uses and the data it collects. Detour ships that declaration with the package, so you do not have to write it yourself. The App Store Connect questionnaire is still yours to fill in, because Apple holds the app developer responsible for everything the app collects, SDKs included. ## What ships with the SDK [Section titled “What ships with the SDK”](#what-ships-with-the-sdk) From **1.1.1** the package bundles a `PrivacyInfo.xcprivacy` manifest. Both Swift Package Manager and CocoaPods copy it into your app bundle, and Apple aggregates it into the Privacy Report generated at upload. It declares Detour as **non-tracking**: `NSPrivacyTracking = false` and no tracking domains. Integrating the SDK therefore does not by itself require an App Tracking Transparency prompt. Older versions Versions before `1.1.1` ship no manifest. If you are pinned below that, either upgrade or add the equivalent declarations to your own app’s manifest. ## What the manifest declares [Section titled “What the manifest declares”](#what-the-manifest-declares) | Declared | Details | | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `UserDefaults` API, reason `CA92.1` | App-scoped keys only: the first-launch flag and a locally generated device ID. | | `DeviceID` | A random UUID stored on device, sent with analytics events. | | `ProductInteraction` | Event names from `logEvent` / `logRetention`, plus Universal Link opens (link URL, its query parameters, app version, OS version, device model). | | `OtherUserContent` | A web URL extracted from the clipboard, read on first launch (and again after `resetSession(allowDeferredRetry: true)`). Only the URL is sent, and any other copied text is discarded on device, and a clipboard holding no web URL sends nothing. | | `OtherDataTypes` | The deferred-matching signals sent on first launch: device model, OS version, screen size and scale, locales, timezone, user agent. | Every collected type is declared as **not linked to the user** and **not used for tracking**. To check the result for your own build: **Xcode → Organizer**, right-click the archive, **Generate Privacy Report**. ## Clipboard access and the system paste alert [Section titled “Clipboard access and the system paste alert”](#clipboard-access-and-the-system-paste-alert) When the copied content came from another app, iOS shows its own paste-permission modal, `"YourApp" would like to paste from "Safari"`, before your UI appears on first launch, and its default button denies access. Detour checks `detectPatterns(for: [.probableWebURL])` first, so it only reads the clipboard when it actually holds a web URL, but that check does not suppress the alert. Set [`shouldUseClipboard: false`](/docs/sdk/ios/api-reference#detourconfig) to skip the read entirely and never show it. Deferred matching then runs without the clipboard signal, which lowers its success rate on iOS. See [Matching](/docs/platform/architecture/matching). ## App Store Connect questionnaire [Section titled “App Store Connect questionnaire”](#app-store-connect-questionnaire) In App Store Connect, open your app → **App Privacy** → **Data Collection** → **Edit**, and answer **Yes** to *“Do you or your third-party partners collect data from this app?“*. ### 1. Tick the applicable boxes [Section titled “1. Tick the applicable boxes”](#1-tick-the-applicable-boxes) Listed in the order they appear on screen, so you can work top to bottom: | Category | Tick | Applies when | | ------------------------------------- | ---- | ----------------------------------------------------------------------------------------------------------- | | **User Content** → Other User Content | Yes | Only if `shouldUseClipboard` is left enabled (the default). Skip it if you set `shouldUseClipboard: false`. | | **Identifiers** → Device ID | Yes | Always. Detour generates and stores a random device ID for analytics. | | **Usage Data** → Product Interaction | Yes | Always. Event names from `logEvent` / `logRetention`, plus Universal Link opens. | | **Other Data** | Yes | Always. The deferred-matching signals. | ### 2. Fill in the section for each box you ticked [Section titled “2. Fill in the section for each box you ticked”](#2-fill-in-the-section-for-each-box-you-ticked) Once you save, each type you selected has its own setup step. Answer the three questions in each: | Data type | Used for | Linked to identity | Used for tracking | | ------------------- | ---------------------------- | ------------------ | ----------------- | | Other User Content | App Functionality | No | No | | Device ID | App Functionality, Analytics | No | No | | Product Interaction | App Functionality, Analytics | No | No | | Other Data | App Functionality | No | No | These answers mirror the shipped manifest exactly, so they cross-check against the Privacy Report Xcode generates from your archive. The manifest covers the SDK, not your payloads Two paths carry data the SDK never inspects, so the shipped manifest says nothing about them: * `DetourAnalytics.logEvent(_:data:)` sends the `data` dictionary as you built it. * Universal Link query parameters are forwarded as-is. If either one carries personal data, declare it under your own app’s entries. ## Related pages [Section titled “Related pages”](#related-pages) [Installation ](/docs/sdk/ios/sdk-installation/)Add the SDK with Swift Package Manager or CocoaPods. [API Reference ](/docs/sdk/ios/api-reference/)Types, methods, and configuration fields. [Matching ](/docs/platform/architecture/matching/)Which signals feed deferred-link attribution. [Analytics ](/docs/platform/fundamentals/analytics/)What the analytics module records. # Installation > Install the Detour iOS SDK via Swift Package Manager or CocoaPods. Detour provides a native iOS SDK (`Detour`) for deferred links, Universal Links, and custom scheme links. AI agent skills Detour ships two AI coding-agent skills: `detour-onboarding` for a setup from scratch, and `migrate-to-detour` for moving off Branch or AppsFlyer. Both cover this page. See [Detour Skills](/docs/platform/fundamentals/skills). A Detour app in the Dashboard is required before the SDK can resolve links. See [Getting Started](/docs/platform/fundamentals/getting-started). ## Requirements [Section titled “Requirements”](#requirements) * iOS `13+` * Swift `5.5+` ## Swift Package Manager [Section titled “Swift Package Manager”](#swift-package-manager) In Xcode: 1. Open your project. 2. Go to `File > Add Package Dependencies...` 3. Enter repository URL: * `https://github.com/software-mansion-labs/ios-detour` 4. Add product `Detour` to your app target. Or in `Package.swift`: Package.swift ```swift .package(url: "https://github.com/software-mansion-labs/ios-detour", from: "1.1.1") ``` ## CocoaPods [Section titled “CocoaPods”](#cocoapods) In your `Podfile`: Podfile ```ruby platform :ios, '13.0' target 'YourAppTarget' do use_frameworks! pod 'Detour', '1.1.1' end ``` Then run: ```bash pod install ``` ## Credentials [Section titled “Credentials”](#credentials) Both values come from **API configuration** in the Dashboard: the `appID` and the publishable `apiKey`. See [Getting Started](/docs/platform/fundamentals/getting-started#api-configuration). ```swift import Detour let detourConfig = DetourConfig( apiKey: "", appID: "", shouldUseClipboard: true, linkProcessingMode: .all ) ``` Keep `apiKey` and `appID` in a build configuration file or another secure configuration layer rather than hard-coded in source. ## Link capabilities [Section titled “Link capabilities”](#link-capabilities) ### Universal Links [Section titled “Universal Links”](#universal-links) In Xcode, open your app target, select **Signing & Capabilities**, add the **Associated Domains** capability, and enter `applinks:`. Use the domain generated in [**App configuration**](/docs/platform/fundamentals/getting-started#app-configuration) in the Dashboard. The resulting entry in your app’s entitlements file is: \.entitlements ```xml com.apple.developer.associated-domains applinks: ``` Without this capability, iOS does not dispatch Detour Universal Links to your app. If your app is served from a [custom domain](/docs/platform/advanced/custom-domain), the Dashboard generates this entry for that domain. Assign the domain before you copy the entry. Changing the host an app is assigned to later needs a new build. ### Custom-scheme links (optional) [Section titled “Custom-scheme links (optional)”](#custom-scheme-links-optional) Custom schemes are not generated by the Dashboard. Add your scheme in Xcode under the app target’s **Info** → **URL Types**, or add the equivalent entry to `Info.plist`: Info.plist ```xml CFBundleURLTypes CFBundleURLSchemes myapp ``` Without this entry, iOS does not dispatch `myapp://...` links to your app. The startup and runtime callbacks for both link types are covered in [Usage](/docs/sdk/ios/sdk-usage). ## Privacy manifest [Section titled “Privacy manifest”](#privacy-manifest) From `1.1.1` the package bundles a `PrivacyInfo.xcprivacy` manifest, copied into your app by both Swift Package Manager and CocoaPods. There is nothing to configure. You still need to answer the App Store Connect privacy questionnaire for what Detour collects. The answers are listed in [Privacy](/docs/sdk/ios/privacy). ## Next steps [Section titled “Next steps”](#next-steps) 1. Wire startup and runtime handling in [Usage](/docs/sdk/ios/sdk-usage). 2. Run the template app in [Examples](/docs/sdk/ios/examples). # Usage > Initialize Detour, resolve the initial link on cold start, and handle runtime links in your iOS app. Detour delivers links to your app through the `Detour.shared` singleton. Call `resolveInitialLink` once at launch to read the link that opened the app, or the deferred link recorded before the install. Call `processLink` for every link that arrives while the app is running. Both return a `DetourResult` with the matched route, which you navigate to. ## Shared config [Section titled “Shared config”](#shared-config) Build the config once and pass the same value to every call: ```swift import Detour let detourConfig = DetourConfig( apiKey: "", appID: "", shouldUseClipboard: true, linkProcessingMode: .all ) ``` ## Resolve the initial link [Section titled “Resolve the initial link”](#resolve-the-initial-link) Call `resolveInitialLink` exactly once during cold start: * If the app uses `UIScene`, call the `connectionOptions` overload from `scene(_:willConnectTo:options:)`. * Otherwise, call the `launchOptions` overload from `application(_:didFinishLaunchingWithOptions:)`. Do not call both overloads for the same launch. The SDK processes the initial link once per session, so the second call returns an empty result. ### SceneDelegate (UIScene apps) [Section titled “SceneDelegate (UIScene apps)”](#scenedelegate-uiscene-apps) For a scene-based app, UIKit supplies cold-start URLs and user activities in `UIScene.ConnectionOptions`: SceneDelegate.swift ```swift func scene( _ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions ) { Detour.shared.resolveInitialLink( config: detourConfig, connectionOptions: connectionOptions ) { result in if let link = result.link { // Navigate using link.route / link.pathname / link.params } } } ``` ### AppDelegate (apps without scenes) [Section titled “AppDelegate (apps without scenes)”](#appdelegate-apps-without-scenes) AppDelegate.swift ```swift func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { Detour.shared.resolveInitialLink(config: detourConfig, launchOptions: launchOptions) { result in if let link = result.link { // Navigate using link.route / link.pathname / link.params } } return true } ``` Once per session `resolveInitialLink` is session-guarded. The second call in a session returns an empty result, so call it from one place only. To run it again in tests, see [Custom Storage](/docs/sdk/ios/custom-storage#reset-for-testing). For a complete, runnable integration with an AppDelegate and SwiftUI state, see [Examples](/docs/sdk/ios/examples). ## Handle runtime links [Section titled “Handle runtime links”](#handle-runtime-links) ### Universal Links [Section titled “Universal Links”](#universal-links) Verified `https://` links arrive as an `NSUserActivity`. Use the callback for your app’s lifecycle: * SceneDelegate ```swift func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL else { return } Task { @MainActor in let result = await Detour.shared.processLink(url, config: detourConfig) // handle result.link } } ``` * AppDelegate ```swift func application( _ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void ) -> Bool { guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL else { return false } Task { @MainActor in let result = await Detour.shared.processLink(url, config: detourConfig) // handle result.link } return true } ``` ### Custom-scheme links (optional) [Section titled “Custom-scheme links (optional)”](#custom-scheme-links-optional) Registered links such as `myapp://product/42` arrive as a URL. Use the callback for your app’s lifecycle: * SceneDelegate ```swift func scene( _ scene: UIScene, openURLContexts URLContexts: Set ) { guard let url = URLContexts.first?.url else { return } Task { @MainActor in let result = await Detour.shared.processLink(url, config: detourConfig) // handle result.link } } ``` * AppDelegate ```swift func application( _ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:] ) -> Bool { Task { @MainActor in let result = await Detour.shared.processLink(url, config: detourConfig) // handle result.link } return true } ``` ## Handle resolved links [Section titled “Handle resolved links”](#handle-resolved-links) `processLink` and `resolveInitialLink` both return a `DetourResult`: * `processed` * Type `Bool` * Link processing finished. * `link` * Type `DetourLink?` * The matched link, or `nil`. `DetourResult` also forwards the link’s fields, so you can read them without unwrapping: * `route` * Type `String?` * Full in-app route, query string included: `/details?utm=1`. * `pathname` * Type `String?` * Path without the query: `/details`. * `params` * Type `[String: String]` * Query params, already parsed. * `linkType` * Type [`LinkType?`](/docs/sdk/ios/api-reference#linktype) * Where the link came from: `.deferred`, `.verified`, or `.scheme`. * `linkURL` * Type `URL?` * The original URL. ## Link processing modes [Section titled “Link processing modes”](#link-processing-modes) `DetourConfig.linkProcessingMode` controls which link sources the SDK handles: | Value | Deferred | Universal | Custom scheme | | ---------------- | -------- | --------- | ------------- | | `.all` (default) | Yes | Yes | Yes | | `.webOnly` | Yes | Yes | No | | `.deferredOnly` | Yes | No | No | Use `.deferredOnly` when your own routing layer already handles runtime links. ## Click limits [Section titled “Click limits”](#click-limits) Before it delivers a Universal Link, the SDK checks the click against your plan’s limit. Within the limit, links resolve normally. The check runs when `processLink` receives a non-`nil` `config`, and on the Universal Link path inside `resolveInitialLink`. ### Over the limit [Section titled “Over the limit”](#over-the-limit) The link is **not** delivered: `result.link` is `nil`, so there is nothing to navigate to. The SDK logs `[Detour:CLICK_LIMIT_ERROR]` with `clicksInPeriod` and `effectiveLimit`, under the subsystem `com.swmansion.detour`. ### Other link types [Section titled “Other link types”](#other-link-types) This flow applies only to Universal Links. * **Deferred and short links** — the same plan limits apply on the backend, but the SDK raises no click-limit error. An over-limit link fails to resolve and `result.link` stays `nil`. * **Custom scheme links** — resolved on device, never limit-checked. Fail-open by design If the limit check cannot complete because of a network or backend error, the SDK delivers the link, so a temporary outage does not block navigation. Limits are enforced only on an explicit “denied” response. For per-plan limits, pay-as-you-go overage, and how to raise them, see [Billing & Payments](/docs/platform/fundamentals/billing). ## Analytics [Section titled “Analytics”](#analytics) `resolveInitialLink` mounts analytics. Mount them yourself only if you process links without it: ```swift Detour.shared.mountAnalytics(config: detourConfig) ``` Once mounted, the SDK logs one event by itself: `app_open`, a retention event sent once per session on the first mount. Universal Link opens are not client events. The backend records them through the click endpoint (see [Click limits](#click-limits)). Everything else you log yourself: ```swift DetourAnalytics.logEvent(.addToCart, data: ["sku": "abc"]) DetourAnalytics.logEvent("promo_banner_tap", data: ["placement": "home_top"]) DetourAnalytics.logRetention("day_7_return") ``` SDK version header Every request the SDK sends carries an `X-SDK: ios/` header, used for diagnostics and SDK-usage analysis. It needs no configuration. ## Related pages [Section titled “Related pages”](#related-pages) * [Examples](/docs/sdk/ios/examples) * [API Reference](/docs/sdk/ios/api-reference) * [Custom Storage](/docs/sdk/ios/custom-storage) * [Testing & Troubleshooting](/docs/sdk/ios/testing) # Testing & Troubleshooting > Test deferred linking and Universal Links on iOS and troubleshoot common Detour iOS SDK issues. Testing deferred links on iOS has platform constraints (App Store redirect chain, first-launch-only semantics). ## Deferred flow constraints [Section titled “Deferred flow constraints”](#deferred-flow-constraints) * Deferred matching is expected on first launch after install. * SDK stores the first-entrance flag in `UserDefaults`. Later launches return an empty result for the deferred fallback. * Session handling is single-pass. Repeated startup resolution in one session is skipped. ## Recommended deferred test flow [Section titled “Recommended deferred test flow”](#recommended-deferred-test-flow) 1. Uninstall the app from the device or simulator. 2. Open a Detour link. 3. Install and launch the app. 4. Verify `result.link?.type == .deferred` and expected route mapping. 5. Reopen app and confirm deferred link is not consumed again. For local iteration without reinstall every time: ```swift Detour.shared.resetSession(allowDeferredRetry: true) ``` ## Testing runtime links [Section titled “Testing runtime links”](#testing-runtime-links) ### Simulator custom scheme [Section titled “Simulator custom scheme”](#simulator-custom-scheme) ```bash xcrun simctl openurl booted "myapp://products/42" ``` Expected: runtime `processLink` resolves to `.scheme`. ### Simulator universal link [Section titled “Simulator universal link”](#simulator-universal-link) ```bash xcrun simctl openurl booted "https:///products/42" ``` Expected: runtime `processLink` resolves to `.verified` when associated domains are configured. ## Common issues [Section titled “Common issues”](#common-issues) No link handled on cold start Ensure `resolveInitialLink(config:launchOptions:)`, or the `connectionOptions` overload, is called once at startup. Universal links open Safari instead of the app Verify the associated domains entitlement and the domain configuration in the Dashboard. Universal link resolves to no route despite a valid click Your plan’s click limit may be exceeded. Look for `[Detour:CLICK_LIMIT_ERROR]` in the unified log (subsystem `com.swmansion.detour`). See [Click limits](/docs/sdk/ios/sdk-usage#click-limits). Scheme links do nothing Verify `CFBundleURLTypes` in `Info.plist`. Expected scheme link is ignored Check `linkProcessingMode`: `.webOnly` and `.deferredOnly` intentionally skip non-web URL-like links. Analytics events missing in the Dashboard Ensure analytics is mounted. `resolveInitialLink` mounts it automatically, and a manual flow can call `mountAnalytics`. ## Debugging [Section titled “Debugging”](#debugging) SDK logs through Apple unified logging under subsystem `com.swmansion.detour`. On simulator/device: ```bash xcrun simctl spawn booted log stream --predicate 'subsystem == "com.swmansion.detour"' ``` For end-to-end setup template, use [Examples](/docs/sdk/ios/examples). # API Reference > Complete API reference for the Detour React Native SDK. ## DetourProvider [Section titled “DetourProvider”](#detourprovider) Top-level React component that initializes the Detour SDK. It handles deferred link matching, Universal Links and App Links, the initial URL, and sending analytics events. On mount, the provider: 1. Checks for an initial URL that launched the app, a Universal Link or App Link on cold start. 2. If there is none, asks the Detour API for a deferred link. This happens on the first launch after install only. 3. Starts listening for Universal Links and App Links while the app is running. 4. Forwards analytics events to the Detour backend. With `linkProcessingMode: 'deferred-only'`, steps 1 and 3 are skipped. Use exactly one `DetourProvider` at the root of your app tree. With more than one mounted, analytics events are dropped. ### Signature [Section titled “Signature”](#signature) ```ts function DetourProvider(props: { config: Config; children: React.ReactNode; }): JSX.Element; ``` ### Web behavior [Section titled “Web behavior”](#web-behavior) On `Platform.OS === 'web'`, the provider does nothing: no link processing, no analytics, no storage access. It returns this context value: * `isLinkProcessed: true` * `link: null` * `clearLink: () => {}` In development builds the SDK logs one warning, `🔗[Detour:WEB_UNSUPPORTED]`. Code shared between web and native can use the provider safely. ### Config fields [Section titled “Config fields”](#config-fields) * `appID` required * Type `string` * App identifier from the Detour Dashboard. * `apiKey` required * Type `string` * Publishable API key from the Detour Dashboard. Sent as a Bearer token on every API call. * `shouldUseClipboard` * Type `boolean` Default `true` * iOS only. When `true`, the SDK reads the clipboard as a signal for deferred link matching. Set it to `false` to skip the read. On Android the clipboard is never read. The Play Install Referrer carries the click ID there. The clipboard string is sent as read, so declare it in the store questionnaires unless you turn this off. See [Privacy](/docs/sdk/react-native/privacy). * `shouldTrackAutomaticEvents` * Type `boolean` Default `true` * From **2.3.1**. Controls the `app_open` retention event the SDK logs once per session on cold start. Set it to `false` to opt out, for example until the user grants tracking consent. Manual `DetourAnalytics` calls still send. * `linkProcessingMode` * Type [`LinkProcessingMode` ](#linkprocessingmode)Default `'all'` * Which link sources the SDK processes. See [LinkProcessingMode](#linkprocessingmode). * `storage` * Type [`DetourStorage` ](#detourstorage)Default `AsyncStorage` * Storage adapter for the first-launch flag and the device ID. Defaults to `@react-native-async-storage/async-storage`. If that package is not installed and you pass no adapter, the provider throws an error. ## useDetourContext [Section titled “useDetourContext”](#usedetourcontext) Hook that returns the link state from the nearest `DetourProvider`. Throws an error when called outside a `DetourProvider`. ### Signature [Section titled “Signature”](#signature-1) ```ts function useDetourContext(): DetourContextType; ``` ### Returned object [Section titled “Returned object”](#returned-object) * `isLinkProcessed` * Type `boolean` * `true` once the SDK has finished looking for a link, whether or not it found one. Use it to show a loading screen during cold start. * `link` * Type [`DetourLink`](#detourlink) * The resolved link, or `null`. Stays set until `clearLink()` is called. * `clearLink` * Type `() => void` * Resets `link` to `null`. Call it after navigating, so the same link does not redirect again. ## DetourAnalytics [Section titled “DetourAnalytics”](#detouranalytics) Analytics module exported from the package root. It needs a mounted `DetourProvider`. Without one, each call is dropped and the SDK logs `🔗[Detour:ANALYTICS_WARNING]`. ### Methods [Section titled “Methods”](#methods) #### `logEvent` [Section titled “logEvent”](#logevent) ```ts DetourAnalytics.logEvent( eventName: DetourEventNames | `${DetourEventNames}`, data?: any, ): void; ``` Records a user action. `eventName` must be one of the predefined [`DetourEventNames`](#detoureventnames), as the enum member or its string value. Custom names are not accepted. `data` takes any metadata, for example `{ itemId: '123', price: 49.99 }`. #### `logRetention` [Section titled “logRetention”](#logretention) ```ts DetourAnalytics.logRetention(retentionEventName: string): void; ``` Records a retention event under any name you choose. ### Automatic events [Section titled “Automatic events”](#automatic-events) `DetourProvider` logs an `app_open` retention event once per session, on cold start. Set [`shouldTrackAutomaticEvents: false`](#config-fields) to turn it off. Universal Link and App Link opens are recorded on the server through the click endpoint, not as client events. See [Click limits](/docs/sdk/react-native/sdk-usage#click-limits). Every API request the SDK sends carries an `X-SDK: react-native/` header, used for diagnostics and SDK-usage analysis. ## Expo Router helper [Section titled “Expo Router helper”](#expo-router-helper) Import it from the subpath: ```ts import { createDetourNativeIntentHandler } from '@swmansion/react-native-detour/expo-router'; ``` ### Signature [Section titled “Signature”](#signature-2) ```ts function createDetourNativeIntentHandler( options?: DetourNativeIntentOptions, ): DetourNativeIntentHandler; ``` The returned handler leaves a URL whose host does not match `hosts` unchanged. ### Options [Section titled “Options”](#options) * `fallbackPath` * Type `string` Default `''` * Route returned for a matching URL when resolve mode is off, when resolution fails, or when the click limit is reached. For example `''` for the home screen or `'/welcome'` for onboarding. * `hosts` * Type `Array` Default `[/\.godetour\.link$/i]` * Host patterns the handler intercepts. Each entry is matched against `url.hostname`: * an exact string, for example `go.example.com` * a suffix wildcard, for example `*.example.com` * a regular expression, for example `/\.example\.org$/i` An empty array falls back to the default. `hosts` replaces the default Passing `hosts` replaces `[/\.godetour\.link$/i]`, it does not add to it. Keep the default pattern next to your [custom domain](/docs/platform/advanced/custom-domain). If the custom domain is removed or the paid plan lapses, Detour serves the app’s links from `.godetour.link` again, and a build without the pattern ignores them. ```ts hosts: ["links.yourdomain.com", /\.godetour\.link$/i], ``` * `config` * Type [`DetourNativeIntentResolveConfig`](#detournativeintentresolveconfig) * Passing this object turns on **resolve mode**. The handler checks the click against your plan’s click limit, resolves short links to their destination, and returns the matching route. Without it the handler runs in **intercept mode** and returns `fallbackPath` for every matching URL. Over the click limit, the handler returns `fallbackPath` and logs `🔗[Detour:CLICK_LIMIT_ERROR]`. See [Click limits](/docs/sdk/react-native/sdk-usage#click-limits). * `mapToRoute` * Type `(value: DetourNativeIntentResolvedValue) => string` * Turns a resolved URL into an in-app route. It receives a [`DetourNativeIntentResolvedValue`](#detournativeintentresolvedvalue). The default drops the first path segment, the app hash, from web URLs with more than one segment, and converts custom scheme URLs to a path. Pass your own when link destinations do not match your route structure. DetourProvider configuration In resolve mode, set `linkProcessingMode: 'deferred-only'` on `DetourProvider`, so the provider handles only deferred links and `+native-intent.tsx` handles the rest. See [Configure DetourProvider for native intent](/docs/sdk/react-native/sdk-usage#configure-detourprovider-for-native-intent). ## React Navigation adapter [Section titled “React Navigation adapter”](#react-navigation-adapter) Exported from the package root, for React Navigation’s custom linking API. See [React Navigation integration](/docs/sdk/react-native/sdk-usage#react-navigation-integration) for a walkthrough. ### `Detour` [Section titled “Detour”](#detour) ```ts const Detour: { getInitialURL: () => Promise; addEventListener: ( event: 'url', listener: (event: DetourUrlEvent) => void, ) => DetourUrlSubscription; }; ``` A `Linking`-shaped object for a React Navigation `linking` config. It needs a `DetourProvider` mounted above your `NavigationContainer`. * `getInitialURL` * Type `() => Promise` * Resolves to the route Detour matched on cold start, or `undefined`. It waits until link processing finishes and returns the URL only once. * `addEventListener` * Type [`(event, listener) => DetourUrlSubscription`](#detoururlsubscription) * Subscribes to routes Detour resolves while the app is running. ### `DETOUR_LINKING_PREFIX` [Section titled “DETOUR\_LINKING\_PREFIX”](#detour_linking_prefix) ```ts const DETOUR_LINKING_PREFIX: string; // "detour://" ``` The prefix of the URLs the adapter emits. Add it to `prefixes` in your `linking` config. Each URL carries `fromDeepLink=true` and a `linkType` query parameter. ## Types [Section titled “Types”](#types) ### LinkProcessingMode [Section titled “LinkProcessingMode”](#linkprocessingmode) ```ts type LinkProcessingMode = 'all' | 'web-only' | 'deferred-only'; ``` | Value | Processes | | ----------------- | --------------------------------------------------------------------------------------- | | `'all'` (default) | Deferred links, Universal Links and App Links, the initial URL, and custom scheme links | | `'web-only'` | The same as `'all'`, without custom scheme links | | `'deferred-only'` | Deferred links only. Use it when Expo Router’s `+native-intent.tsx` handles the rest | The package does not export this type by name. Use `Config['linkProcessingMode']` to refer to it. ### LinkType [Section titled “LinkType”](#linktype) ```ts type LinkType = 'deferred' | 'verified' | 'scheme'; ``` | Value | How the link arrived | | ------------ | ----------------------------------------------------------------- | | `'deferred'` | From the Detour API on the first launch after install | | `'verified'` | As a Universal Link or App Link that the OS verified for your app | | `'scheme'` | Through a custom URL scheme, for example `myapp://path` | ### DetourLink [Section titled “DetourLink”](#detourlink) ```ts type DetourLink = { url: string | URL; route: string; pathname: string; params: Record; type: LinkType; } | null; ``` * `url` * Type `string | URL` * The original URL as a `URL` object, or the raw string if it could not be parsed. * `route` * Type `string` * In-app route with the query string, for example `/details?utm=1`. For web and deferred links, the first path segment, the app hash, is removed. * `pathname` * Type `string` * The route without the query string, for example `/details`. * `params` * Type `Record` * Query parameters. All values are strings. * `type` * Type [`LinkType`](#linktype) * How the link arrived in the app. ### DetourContextType [Section titled “DetourContextType”](#detourcontexttype) ```ts type DetourContextType = { isLinkProcessed: boolean; link: DetourLink; clearLink: () => void; }; ``` The value returned by `useDetourContext`. See [Returned object](#returned-object) for each field. ### DetourStorage [Section titled “DetourStorage”](#detourstorage) ```ts interface DetourStorage { getItem(key: string): Promise | string | null; setItem(key: string, value: string): Promise | void; removeItem?(key: string): Promise | void; } ``` Interface for a custom storage adapter. Methods can be synchronous or asynchronous. The SDK stores a first-launch flag, which limits deferred matching to the first launch, and a device ID for analytics. To use MMKV, Expo SecureStore or another store, see [Custom Storage](/docs/sdk/react-native/custom-storage). ### DetourUrlEvent [Section titled “DetourUrlEvent”](#detoururlevent) ```ts type DetourUrlEvent = { url: string; }; ``` Passed to the [`Detour.addEventListener`](#detour) listener. `url` is the resolved route, prefixed with [`DETOUR_LINKING_PREFIX`](#detour_linking_prefix). ### DetourUrlSubscription [Section titled “DetourUrlSubscription”](#detoururlsubscription) ```ts type DetourUrlSubscription = { remove: () => void; }; ``` Returned by [`Detour.addEventListener`](#detour). Call `remove()` to detach the listener, usually in the cleanup of your `linking.subscribe`. ### DetourNativeIntentArgs [Section titled “DetourNativeIntentArgs”](#detournativeintentargs) ```ts type DetourNativeIntentArgs = { path: string; initial: boolean; }; ``` * `path` * Type `string` * The incoming system path or URL from Expo Router. * `initial` * Type `boolean` * `true` for the URL that launched the app, `false` for links received while it runs. ### DetourNativeIntentHandler [Section titled “DetourNativeIntentHandler”](#detournativeintenthandler) ```ts type DetourNativeIntentHandler = ( args: DetourNativeIntentArgs, ) => string | Promise; ``` The function `createDetourNativeIntentHandler` returns. Export it as `redirectSystemPath` from `+native-intent.tsx`. ### DetourNativeIntentResolveConfig [Section titled “DetourNativeIntentResolveConfig”](#detournativeintentresolveconfig) ```ts type DetourNativeIntentResolveConfig = { apiKey: string; appID: string; timeoutMs?: number; onResolveError?: (error: unknown, context: DetourNativeIntentArgs) => void; }; ``` * `apiKey` required * Type `string` * Publishable API key, the same value as in the `DetourProvider` config. * `appID` required * Type `string` * App identifier, the same value as in the `DetourProvider` config. * `timeoutMs` * Type `number` Default `1200` * How long to wait for short-link resolution, in milliseconds. On timeout the handler returns `fallbackPath`. * `onResolveError` * Type `(error, context) => void` * Called when short-link resolution fails or times out, or when `mapToRoute` throws. Use it for logging. ### DetourNativeIntentOptions [Section titled “DetourNativeIntentOptions”](#detournativeintentoptions) ```ts type DetourNativeIntentOptions = { fallbackPath?: string; hosts?: Array; config?: DetourNativeIntentResolveConfig; mapToRoute?: (value: DetourNativeIntentResolvedValue) => string; }; ``` See [Options](#options) for each field. ### DetourNativeIntentResolvedValue [Section titled “DetourNativeIntentResolvedValue”](#detournativeintentresolvedvalue) ```ts type DetourNativeIntentResolvedValue = { originalPath: string; initial: boolean; resolvedUrl: URL; }; ``` * `originalPath` * Type `string` * The incoming system path, before resolution. * `initial` * Type `boolean` * `true` for the URL that launched the app. * `resolvedUrl` * Type `URL` * The destination URL after short-link resolution. ### DetourEventNames [Section titled “DetourEventNames”](#detoureventnames) | Category | Event name | String value | | :---------- | :--------------------------- | :------------------------------ | | **General** | `Login` | `login` | | | `Search` | `search` | | | `Share` | `share` | | | `SignUp` | `sign_up` | | | `TutorialBegin` | `tutorial_begin` | | | `TutorialComplete` | `tutorial_complete` | | | `ReEngage` | `re_engage` | | | `Invite` | `invite` | | | `OpenedFromPushNotification` | `opened_from_push_notification` | | **Sales** | `AddPaymentInfo` | `add_payment_info` | | | `AddShippingInfo` | `add_shipping_info` | | | `AddToCart` | `add_to_cart` | | | `RemoveFromCart` | `remove_from_cart` | | | `Refund` | `refund` | | | `ViewItem` | `view_item` | | | `BeginCheckout` | `begin_checkout` | | | `Purchase` | `purchase` | | | `AdImpression` | `ad_impression` | # Custom Storage > Provide a custom storage adapter (MMKV, Expo Secure Store, or your own) for the Detour React Native SDK. By default, the SDK uses `@react-native-async-storage/async-storage` for internal persistence keys. If you prefer another storage engine, pass a custom adapter in `config.storage`. ## Storage interface [Section titled “Storage interface”](#storage-interface) ```ts interface DetourStorage { getItem(key: string): Promise | string | null; setItem(key: string, value: string): Promise | void; removeItem?(key: string): Promise | void; } ``` ## Examples [Section titled “Examples”](#examples) The two adapters below cover the storage engines React Native apps use most often. Any object that matches the `DetourStorage` interface works, so use these as starting points for other engines. ### React Native MMKV [Section titled “React Native MMKV”](#react-native-mmkv) ```ts import { createMMKV } from 'react-native-mmkv'; import { DetourProvider } from '@swmansion/react-native-detour'; const storage = createMMKV(); const mmkvAdapter = { getItem: (key: string) => storage.getString(key) ?? null, setItem: (key: string, value: string) => storage.set(key, value), }; export default function App() { return ( {/* Your app */} ); } ``` ### Expo Secure Store [Section titled “Expo Secure Store”](#expo-secure-store) ```ts import * as SecureStore from 'expo-secure-store'; import { DetourProvider } from '@swmansion/react-native-detour'; const secureStoreAdapter = { getItem: SecureStore.getItemAsync, setItem: SecureStore.setItemAsync, }; export default function App() { return ( {/* Your app */} ); } ``` ## Related pages [Section titled “Related pages”](#related-pages) * [Usage](/docs/sdk/react-native/sdk-usage) * [API Reference](/docs/sdk/react-native/api-reference) # Examples > Runnable Detour React Native examples for Expo Router and React Navigation. The RN SDK repository ships six runnable apps, one per integration style. Clone the one closest to your setup, run it, and copy what you need. Each card below links to the example’s `README.md` on GitHub, with the project structure, setup steps, and full source. ## Catalog [Section titled “Catalog”](#catalog) [expo-bare ](https://github.com/software-mansion-labs/react-native-detour/blob/main/examples/expo-bare/README.md)The SDK on its own, without a router. Use it to check that your configuration works. [expo-router ](https://github.com/software-mansion-labs/react-native-detour/blob/main/examples/expo-router/README.md)The smallest Expo Router setup. The app opens the link's screen as soon as a link arrives. [expo-router-native-intent ](https://github.com/software-mansion-labs/react-native-detour/blob/main/examples/expo-router-native-intent/README.md)Expo Router resolves short links in its native intent handler, so the app opens the final screen directly. [expo-router-advanced ](https://github.com/software-mansion-labs/react-native-detour/blob/main/examples/expo-router-advanced/README.md)Expo Router with sign-in. A link that arrives while the user is signed out opens after they sign in. [react-navigation ](https://github.com/software-mansion-labs/react-native-detour/blob/main/examples/react-navigation/README.md)The smallest React Navigation setup, using the Detour linking adapter. [react-navigation-advanced ](https://github.com/software-mansion-labs/react-native-detour/blob/main/examples/react-navigation-advanced/README.md)React Navigation with sign-in and onboarding. A link waits until the user finishes both, then opens. ## Where to go next [Section titled “Where to go next”](#where-to-go-next) 1. Add the SDK to your own app. See [Installation](/docs/sdk/react-native/sdk-installation). 2. Learn the API the examples use. See [Usage](/docs/sdk/react-native/sdk-usage). 3. Adapt the closest example’s route mapping and auth flow to your app. 4. Check the result. See [Testing & Troubleshooting](/docs/sdk/react-native/testing). # Migration to v2 > Migrate to @swmansion/react-native-detour@2.0.0 from 0.x or 1.0.1. Two paths lead to `@swmansion/react-native-detour@2.0.0`. Pick the one that matches your current version: * still on `0.x`: start from [From 0.x](#from-0x) * already on `1.0.1`: start from [From 1.0.1](#from-101) Device info provider from 2.3.0 The steps below uninstall `react-native-device-info`, because `2.0.0` relied on `expo-device` alone. From **2.3.0** the SDK accepts either library, so keep `react-native-device-info` if your project already uses it. See [Device info provider](/docs/sdk/react-native/sdk-installation#device-info-provider). ## From 0.x [Section titled “From 0.x”](#from-0x) ### Changes from `0.x` [Section titled “Changes from 0.x”](#changes-from-0x) | Area | In `0.x` | In `2.0.0` | | ------------------------------- | ------------------------------------------- | ------------------------------------------------------------ | | Config key | Legacy `API_KEY` form in older integrations | `apiKey` | | Link data in context | `linkRoute` / legacy fields | `link` object (`route`, `pathname`, `params`, `url`, `type`) | | Link-source control | No explicit mode in older flows | `linkProcessingMode: 'all' \| 'web-only' \| 'deferred-only'` | | Device fingerprint dependencies | `react-native-device-info` | `expo-constants` + `expo-device` | | Expo Web behavior | Not explicitly guarded | `DetourProvider` disabled on web (no-op context) | ### 1. Update dependencies [Section titled “1. Update dependencies”](#1-update-dependencies) Remove old dependency: ```bash npm uninstall react-native-device-info ``` Install current peer dependencies: ```bash npm install expo-localization expo-clipboard expo-constants expo-device @react-native-async-storage/async-storage expo-application ``` If you use Yarn/PNPM/Bun, use equivalent commands from [Installation](/docs/sdk/react-native/sdk-installation). ### 2. Update config shape [Section titled “2. Update config shape”](#2-update-config-shape) ```ts // before (legacy) const config = { API_KEY: process.env.EXPO_PUBLIC_DETOUR_API_KEY, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID, }; // after const config = { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID, linkProcessingMode: 'all', }; ``` ### 3. Update context usage [Section titled “3. Update context usage”](#3-update-context-usage) ```ts // before (legacy route string) const { isLinkProcessed, linkRoute, clearLink } = useDetourContext(); if (isLinkProcessed && linkRoute) { router.replace(linkRoute); clearLink(); } // after const { isLinkProcessed, link, clearLink } = useDetourContext(); if (isLinkProcessed && link) { router.replace({ pathname: link.pathname, params: link.params }); clearLink(); } ``` ### 4. Choose a link processing mode [Section titled “4. Choose a link processing mode”](#4-choose-a-link-processing-mode) * `'all'` (default) — deferred + Universal/App + scheme links * `'web-only'` — deferred + Universal/App links, no scheme links * `'deferred-only'` — deferred only (recommended if Expo Router `+native-intent.tsx` handles runtime links) ## From 1.0.1 [Section titled “From 1.0.1”](#from-101) ### Changes from `1.0.1` [Section titled “Changes from 1.0.1”](#changes-from-101) | Area | In `1.0.1` | In `2.0.0` | | ------------------------------- | ----------------------------------- | --------------------------------------------------------------------- | | Device fingerprint dependencies | `react-native-device-info` | `expo-constants` + `expo-device` | | Peer dependency install command | Included `react-native-device-info` | Uses Expo modules only (no `react-native-device-info`) | | Expo Web behavior | Not explicitly guarded | `DetourProvider` is disabled on web (no-op context + one dev warning) | ### Update dependencies [Section titled “Update dependencies”](#update-dependencies) 1. Remove old dependency: ```bash npm uninstall react-native-device-info ``` 2. Install current peer dependencies: ```bash npm install expo-localization expo-clipboard expo-constants expo-device @react-native-async-storage/async-storage expo-application ``` If you use Yarn/PNPM/Bun, use equivalent commands from [Installation](/docs/sdk/react-native/sdk-installation). ### Web behavior [Section titled “Web behavior”](#web-behavior) In `2.0.0`, `DetourProvider` does not initialize on Expo Web. It returns a no-op context instead: * `isLinkProcessed: true` * `link: null` * `clearLink: () => {}` This keeps the app tree stable and avoids a partial initialization on a platform the SDK does not support. ## Validation checklist [Section titled “Validation checklist”](#validation-checklist) Whichever path you took, confirm that: 1. The iOS and Android deferred flow still resolves the route and its parameters. 2. Runtime Universal/App Links still resolve according to `linkProcessingMode`. 3. Expo Web no longer attempts Detour processing, and the app renders without provider errors. 4. Exactly one `DetourProvider` is mounted in the app root. ## Related pages [Section titled “Related pages”](#related-pages) * [Installation](/docs/sdk/react-native/sdk-installation) * [Usage](/docs/sdk/react-native/sdk-usage) * [API Reference](/docs/sdk/react-native/api-reference) * [Examples](/docs/sdk/react-native/examples) # Privacy > What the Detour React Native SDK sends, and how to answer the App Store Connect and Google Play privacy forms. The App Store and Google Play both make you declare what your app collects, including what the third-party SDKs inside it collect. This page lists everything `@swmansion/react-native-detour` sends, so you can fill in both forms without reading the SDK source. ## What the SDK sends [Section titled “What the SDK sends”](#what-the-sdk-sends) The SDK makes four network calls, all to `godetour.dev`. | Sent | When | Contents | | -------------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | Deferred-matching signals | First launch after install, once | Platform, device model, manufacturer, OS version, screen size and scale, locales, timezone, user agent. On iOS also the clipboard string. | | Universal / App Link click | Every link open the SDK handles | The link URL and its query parameters, app version, OS version, device model. | | Analytics event | Each `DetourAnalytics.logEvent` call | Event name, your `data` payload, platform, device ID. | | Retention event | `app_open` on cold start, plus `logRetention` calls | Event name, platform, device ID. | The **device ID** is a random UUID the SDK generates on first use and keeps in the configured storage. It is scoped to your app’s storage, is not IDFA, IDFV or the Android advertising ID, and is regenerated when app data is cleared. Set [`shouldTrackAutomaticEvents: false`](/docs/sdk/react-native/api-reference#config-fields) to stop the automatic `app_open` event. It does not affect the other three calls. ### Clipboard [Section titled “Clipboard”](#clipboard) On **iOS** the SDK calls `Clipboard.getStringAsync()` on first launch and sends the result with the matching signals. It does not check the content of the string, so the string can be any text the user copied. Set [`shouldUseClipboard: false`](/docs/sdk/react-native/api-reference#config-fields) to skip the read. Deferred matching then runs without that signal, which lowers its success rate on iOS. See [Matching](/docs/platform/architecture/matching). On **Android** the SDK never reads the clipboard, even with the flag left at `true`. The Play install referrer provides the click id there, so matching does not need the clipboard. ## App Store Connect questionnaire [Section titled “App Store Connect questionnaire”](#app-store-connect-questionnaire) These answers are required on every submission, and they have to cover the SDKs you integrate, not only your own code. See [App privacy details on the App Store](https://developer.apple.com/app-store/app-privacy-details/). Open your app → **App Privacy** → **Data Collection** → **Edit**, and answer **Yes** to *“Do you or your third-party partners collect data from this app?”*. Answer for the version that is live on the App Store, not for the build you are working on. The next screen lists every data type, grouped by category. Select these four. Each selected type then gets its own setup step with three questions, answered by the last three columns. | Data type | Select when | Used for | Linked to identity | Used for tracking | | ------------------------------------- | -------------------------------------------------- | ---------------------------- | ------------------ | ----------------- | | **User Content** → Other User Content | `shouldUseClipboard` left at its default of `true` | App Functionality | No | No | | **Identifiers** → Device ID | Always | App Functionality, Analytics | No | No | | **Usage Data** → Product Interaction | Always | App Functionality, Analytics | No | No | | **Other Data** | Always | App Functionality | No | No | **Device ID** covers any device-level ID, not only the advertising identifier, so the UUID the SDK generates belongs there. **Other Data** is the last row in the list and covers data types Apple does not name separately. Select it for the deferred-matching signals. All four have to be declared. Apple lets you leave out a data type only when it is collected infrequently, the user chooses to provide it, and the collection is outside the app’s primary functionality. Link resolution is none of those. The package ships no privacy manifest The package is pure TypeScript, so it ships no `PrivacyInfo.xcprivacy` of its own, and the Expo modules it depends on each declare an empty `NSPrivacyCollectedDataTypes`. The Privacy Report that Xcode generates from your archive therefore lists no collected data for Detour. Answer the questionnaire from this page rather than from that report. Your app’s own manifest does not have to cover the SDK. Apple expects each SDK to ship its own, and states that your file does not need to repeat what the SDKs you link collect. See [Describing data use in privacy manifests](https://developer.apple.com/documentation/bundleresources/describing-data-use-in-privacy-manifests). ## Google Play Data safety [Section titled “Google Play Data safety”](#google-play-data-safety) In Play Console, select the app, then open **Monitor and improve** → **Policy and programmes** → **App content**, and start the **Data safety** form. It asks first whether the app collects or shares any of the required user data types. Answer **Yes**, because the SDK sends the data listed below off the device. ### Data types [Section titled “Data types”](#data-types) Declare Android behavior only. The SDK does not read the clipboard on Android, so no user content applies. Two groups do. | Group | Check | Select when | | ----------------------- | ------------------- | ----------------------------------------------------------------------------------------------- | | **Device or other IDs** | Device or other IDs | Always. The generated device ID, and the deferred-matching signals, which identify one install. | | **App activity** | App interactions | Always. Link opens, `app_open`, and any event you log yourself. | | **App activity** | Other actions | Only if you log events that do not describe a screen or a tap. Whatever you pass to `logEvent`. | ### Data usage and handling [Section titled “Data usage and handling”](#data-usage-and-handling) Each type you selected gets its own set of questions. For all of them: purpose **App functionality** and **Analytics**, the data is **required** rather than optional, because link resolution does not work without it, and it is **not** processed ephemerally, because Detour stores it to match a later install. The form also asks whether each type is **shared**. Google does not count data sent to a service provider that processes it on your instructions, and does count data sent to a third party. Whether Detour is one or the other is set by your contract with us, so take that answer from the contract. The definitions are in [Provide information for Google Play’s Data safety section](https://support.google.com/googleplay/android-developer/answer/10787469). The package requests **no Android permissions of its own**. In particular it does not add `com.google.android.gms.permission.AD_ID`, so it does not force an advertising-ID declaration onto your app. These declarations cover the SDK, not your payloads The SDK does not inspect two kinds of data it sends, so nothing above covers them: * `DetourAnalytics.logEvent` sends the `data` object as you built it. * Universal Link query parameters are forwarded as-is. If either one carries personal data, declare it under your own app’s entries. ## Related pages [Section titled “Related pages”](#related-pages) [Installation ](/docs/sdk/react-native/sdk-installation/)Add the package and configure native link handling. [API Reference ](/docs/sdk/react-native/api-reference/)Config fields, including the two privacy switches. [Matching ](/docs/platform/architecture/matching/)Which signals feed deferred-link attribution. [Analytics ](/docs/platform/fundamentals/analytics/)What the analytics module records. # Installation > Install the Detour React Native SDK in Expo or bare React Native projects. Detour provides a React Native SDK (`@swmansion/react-native-detour`) compatible with Expo and bare React Native projects. AI agent skills Detour ships two AI coding-agent skills: `detour-onboarding` for a setup from scratch, and `migrate-to-detour` for moving off Branch or AppsFlyer. Both cover this page. See [Detour Skills](/docs/platform/fundamentals/skills). A Detour app in the Dashboard is required before the SDK can resolve links. See [Getting Started](/docs/platform/fundamentals/getting-started). ## Install package [Section titled “Install package”](#install-package) * npm ```bash npm install @swmansion/react-native-detour ``` * yarn ```bash yarn add @swmansion/react-native-detour ``` * pnpm ```bash pnpm add @swmansion/react-native-detour ``` * bun ```bash bun add @swmansion/react-native-detour ``` ## Peer dependencies (Expo) [Section titled “Peer dependencies (Expo)”](#peer-dependencies-expo) For Expo projects (including prebuild), install peer dependencies with Expo CLI so versions are matched to your Expo SDK: ```bash npx expo install expo-application expo-clipboard expo-constants expo-localization @react-native-async-storage/async-storage ``` ## Bare React Native projects [Section titled “Bare React Native projects”](#bare-react-native-projects) If you are not using Expo CLI, install peer dependencies with your package manager and pin versions compatible with your React Native/Expo Modules setup. * npm ```bash npm install expo-application expo-clipboard expo-constants expo-localization @react-native-async-storage/async-storage ``` * yarn ```bash yarn add expo-application expo-clipboard expo-constants expo-localization @react-native-async-storage/async-storage ``` * pnpm ```bash pnpm add expo-application expo-clipboard expo-constants expo-localization @react-native-async-storage/async-storage ``` * bun ```bash bun add expo-application expo-clipboard expo-constants expo-localization @react-native-async-storage/async-storage ``` Async Storage is optional Skip `@react-native-async-storage/async-storage` if you pass a custom storage adapter in `config.storage`. See [Custom Storage](/docs/sdk/react-native/custom-storage). ### Expo Modules setup (React Native CLI) [Section titled “Expo Modules setup (React Native CLI)”](#expo-modules-setup-react-native-cli) Four of the peer dependencies above are [Expo Modules](https://docs.expo.dev/bare/installing-expo-modules/): * `expo-application` * `expo-clipboard` * `expo-constants` * `expo-localization` They ship native code that has to be autolinked into your `android/` and `ios/` projects. An Expo project does this for you. A bare React Native CLI project needs Expo Modules added once: ```bash npx install-expo-modules@latest ``` This sets up Expo Modules autolinking (and on iOS raises the deployment target to the Expo Modules minimum). `install-expo-modules` also switches the native JS bundling step to Expo CLI (`export:embed`). That requires `metro.config.js` to extend `expo/metro-config`, which is a superset of `@react-native/metro-config` and safe in bare projects. The tool usually migrates the file for you, but verify it, especially if you had customized it: metro.config.js ```js const { getDefaultConfig } = require("expo/metro-config"); const { mergeConfig } = require("@react-native/metro-config"); module.exports = mergeConfig(getDefaultConfig(__dirname), {}); ``` Then align the Expo package versions to your setup: ```bash npx expo install --fix ``` Build errors after adding Expo Modules If the app throws at startup or a release build fails after adding Expo Modules, see [bare React Native CLI build issues](/docs/sdk/react-native/testing#bare-react-native-cli-build-issues). ## Device info provider [Section titled “Device info provider”](#device-info-provider) Deferred-link matching builds a probabilistic fingerprint of the device. The SDK reads three values for it: * device model * manufacturer * OS version Those values come from a device info library. Install **one** of the two supported ones. Both are optional peer dependencies, and the SDK uses whichever it finds: * expo-device (recommended for Expo) ```bash npx expo install expo-device ``` * react-native-device-info ```bash npm install react-native-device-info ``` Which one to pick Choose `react-native-device-info` if your project already depends on it. Otherwise `expo-device` is the simplest option for Expo apps. The device info source is not configurable. Unlike storage, it has no adapter. The SDK detects the installed library at import time and prefers `expo-device` when both are present. One of the two is required Install neither and the SDK throws while building the fingerprint: ```txt [react-native-detour] No device info library found. Install either "expo-device" or "react-native-device-info". ``` Deferred-link matching then fails and Universal/App Link clicks are not recorded. Link navigation itself keeps working, because the SDK fails open. ## Native link configuration [Section titled “Native link configuration”](#native-link-configuration) Installing the package does not register incoming links with iOS or Android. ### Expo [Section titled “Expo”](#expo) Copy the generated `ios.associatedDomains` and `android.intentFilters` values from **App configuration** in the Dashboard into `app.json`, `app.config.js`, or `app.config.ts`: app.json ```json { "expo": { "ios": { "associatedDomains": ["applinks:"] }, "android": { "intentFilters": [ { "action": "VIEW", "autoVerify": true, "data": [ { "scheme": "https", "host": "", "pathPrefix": "/" } ], "category": ["BROWSABLE", "DEFAULT"] } ] } } } ``` Use the exact Dashboard output: it also contains any required legacy short-link prefixes. New short links reuse `/` and do not require another native build. If your app is served from a [custom domain](/docs/platform/advanced/custom-domain), the Dashboard generates both values for that domain. Assign the domain before you copy them. Changing the host an app is assigned to later needs a new build. Do not add hosts it did not generate: on Android 11 and lower that breaks App Link verification for every host at once, see [Verification](/docs/platform/fundamentals/universal-app-links#verification). The same host belongs in the handler’s [`hosts`](/docs/sdk/react-native/sdk-usage#custom-domains) list, which is separate from the native configuration. Custom schemes are optional and are not generated by the Dashboard. If you use links such as `myapp://product/42`, add the scheme separately: app.json ```json { "expo": { "scheme": "myapp" } } ``` These are build-time settings. After changing the app config, create a new native build. In a Continuous Native Generation workflow, regenerate the native projects with `npx expo prebuild --clean`, then run `npx expo run:ios` or `npx expo run:android`. Alternatively, create a new EAS development build. Reloading Metro or publishing an OTA update does not apply native configuration changes. Expo Go cannot test the complete flow Expo Go cannot register your app’s Associated Domains, intent filters, or custom scheme, and it does not represent an installation of your app. It can still be useful for JavaScript-only navigation work, but use a development build to test Universal/App Links and a real device to test the complete deferred-link flow. ### Bare React Native [Section titled “Bare React Native”](#bare-react-native) #### iOS Universal Links [Section titled “iOS Universal Links”](#ios-universal-links) In Xcode, open your app target, select **Signing & Capabilities**, add the **Associated Domains** capability, and enter `applinks:`. The resulting entitlements entry is: \.entitlements ```xml com.apple.developer.associated-domains applinks: ``` #### Android App Links [Section titled “Android App Links”](#android-app-links) Paste the generated Dashboard filter into the `MainActivity` declaration in `AndroidManifest.xml`: AndroidManifest.xml ```xml ``` Use the exact host and paths generated for your app. #### Custom-scheme links (optional) [Section titled “Custom-scheme links (optional)”](#custom-scheme-links-optional) The Dashboard does not generate custom-scheme registration. If you use scheme links, add `CFBundleURLTypes` to the iOS `Info.plist`: Info.plist ```xml CFBundleURLTypes CFBundleURLSchemes myapp ``` Add a separate filter to the Android `MainActivity`: AndroidManifest.xml ```xml ``` #### iOS callback forwarding [Section titled “iOS callback forwarding”](#ios-callback-forwarding) `DetourProvider` reads cold-start and runtime URLs through React Native’s `Linking` API. Verify that your `AppDelegate` forwards Universal Link and custom-scheme callbacks to `RCTLinkingManager`. The snippets below are for an `AppDelegate` that does not already provide this forwarding. If its superclass or another integration handles links, preserve that behavior and merge the handlers instead of adding duplicate forwarding or replacing the existing implementation. * Swift ```swift import React func application( _ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void ) -> Bool { return RCTLinkingManager.application( application, continue: userActivity, restorationHandler: restorationHandler ) } func application( _ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:] ) -> Bool { return RCTLinkingManager.application(app, open: url, options: options) } ``` * Objective-C++ ```objc #import - (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler { return [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; } - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { return [RCTLinkingManager application:application openURL:url options:options]; } ``` #### Android callback delivery [Section titled “Android callback delivery”](#android-callback-delivery) With the standard `MainActivity : ReactActivity`, no Detour-specific callback is required. React Native reads the cold-start `VIEW` intent from the activity and forwards later `onNewIntent` calls to `Linking`. Set `android:launchMode="singleTask"` when links should be delivered to the existing `MainActivity` instance. React Native then emits the runtime `url` event consumed by `DetourProvider`. If you use a custom activity that does not inherit React Native’s default intent handling, you must forward its intents to React Native yourself. ## Credentials [Section titled “Credentials”](#credentials) Both values come from **API configuration** in the Dashboard: the `appID` and the publishable `apiKey`. See [Getting Started](/docs/platform/fundamentals/getting-started#api-configuration). ```ts import { type Config } from '@swmansion/react-native-detour'; export const detourConfig: Config = { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, shouldUseClipboard: true, }; ``` Keep `apiKey` and `appID` in environment variables or another secure configuration layer. ## Next steps [Section titled “Next steps”](#next-steps) 1. Implement the provider and link handling in [Usage](/docs/sdk/react-native/sdk-usage). 2. Pick a runnable app pattern in [Examples](/docs/sdk/react-native/examples). 3. Verify the native link configuration in [Testing](/docs/sdk/react-native/testing). 4. Fill in the store declarations from [Privacy](/docs/sdk/react-native/privacy) before you submit a build. 5. If upgrading existing code, follow [Migration to v2](/docs/sdk/react-native/migration-2-0-0) and start from [From 0.x](/docs/sdk/react-native/migration-2-0-0#from-0x) or [From 1.0.1](/docs/sdk/react-native/migration-2-0-0#from-101). # Usage > Initialize DetourProvider, handle resolved links, and integrate Detour with Expo Router or React Navigation. Detour delivers links to your app through a single provider. `DetourProvider` resolves the incoming link, whether deferred, Universal/App, or custom scheme, and `useDetourContext()` returns the result. You then navigate with your router, or navigate manually. ## Provider setup [Section titled “Provider setup”](#provider-setup) ```ts import { DetourProvider, type Config } from '@swmansion/react-native-detour'; const config: Config = { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, shouldUseClipboard: true, // linkProcessingMode: 'all' // default }; export default function RootLayout() { return ( ); } ``` One provider per app Mount exactly one `DetourProvider` in your app tree. A second one disables analytics until only one remains. Expo Web `DetourProvider` is disabled on web. It returns a no-op context (`isLinkProcessed: true`, `link: null`) and processes no links. For complete, runnable integrations of each navigation style, see [Examples](/docs/sdk/react-native/examples). ## Handle resolved links [Section titled “Handle resolved links”](#handle-resolved-links) ```ts import { useDetourContext } from '@swmansion/react-native-detour'; import { useEffect } from 'react'; import { useRouter } from 'expo-router'; export function RootNavigator() { const { isLinkProcessed, link, clearLink } = useDetourContext(); const router = useRouter(); useEffect(() => { if (!isLinkProcessed || !link) return; router.replace({ pathname: link.pathname, params: link.params, }); clearLink(); }, [clearLink, isLinkProcessed, link, router]); if (!isLinkProcessed) { return null; } return ; } ``` ### `link` structure [Section titled “link structure”](#link-structure) `useDetourContext()` returns a `link` object, or `null` when no link was detected. * `link.route` * Type `string` * Full in-app route, query string included: `/details?utm=1`. * `link.pathname` * Type `string` * Path without the query: `/details`. * `link.params` * Type `Record` * Query params, already parsed. * `link.url` * Type `string | URL` * The original URL, or the raw value when it is not a URL. * `link.type` * Type [`LinkType`](/docs/sdk/react-native/api-reference#linktype) * Where the link came from: `'deferred'`, `'verified'`, or `'scheme'`. Use `clearLink()` after handling navigation to avoid repeated redirects. ## Link processing modes [Section titled “Link processing modes”](#link-processing-modes) `Config.linkProcessingMode` controls which link sources the SDK handles: | Value | Deferred | Universal / App | Custom scheme | | ----------------- | -------- | --------------- | ------------- | | `'all'` (default) | Yes | Yes | Yes | | `'web-only'` | Yes | Yes | No | | `'deferred-only'` | Yes | No | No | Use `'deferred-only'` when Expo Router `+native-intent.tsx` already handles runtime and initial URLs. ## Click limits [Section titled “Click limits”](#click-limits) Before it delivers a Universal Link or App Link, the SDK checks the click against your plan’s limit. Within the limit, links resolve normally. ### Over the limit [Section titled “Over the limit”](#over-the-limit) The link is **not** delivered: * `DetourProvider` leaves `link` as `null`, so `useDetourContext()` returns no route to navigate to. * `createDetourNativeIntentHandler` in resolve mode returns `fallbackPath`. Either way the SDK logs `🔗[Detour:CLICK_LIMIT_ERROR]` with `clicksInPeriod` and `effectiveLimit`. ### Other link types [Section titled “Other link types”](#other-link-types) This flow applies only to Universal Links and App Links. * **Deferred and short links** — the same plan limits apply on the backend, but the SDK raises no click-limit error. An over-limit link fails to resolve and returns `null`. * **Custom scheme links** — resolved on device, never limit-checked. Fail-open by design If the limit check cannot complete because of a network or backend error, the SDK delivers the link, so a temporary outage does not block navigation. Limits are enforced only on an explicit “denied” response. For per-plan limits, pay-as-you-go overage, and how to raise them, see [Billing & Payments](/docs/platform/fundamentals/billing). ## Expo Router integration [Section titled “Expo Router integration”](#expo-router-integration) Recommended for Expo apps On Expo Router, use `createDetourNativeIntentHandler` in a [`+native-intent.tsx`](https://docs.expo.dev/router/advanced/native-intent/) file. It handles URLs at the router level, before any screen renders, so the user never sees the wrong screen. Expo Router’s `+native-intent.tsx` intercepts and transforms incoming system URLs before the router processes them. `createDetourNativeIntentHandler` is a factory that builds such a handler, pre-configured for Detour link resolution. ### Basic setup [Section titled “Basic setup”](#basic-setup) Create a `+native-intent.tsx` file in your app directory (e.g. `src/app/+native-intent.tsx`): src/app/+native-intent.tsx ```ts import { createDetourNativeIntentHandler } from "@swmansion/react-native-detour/expo-router"; export const redirectSystemPath = createDetourNativeIntentHandler(); ``` This is intercept mode Any URL matching a Detour host is intercepted and the user opens the home screen, the default `fallbackPath` of `''`. `DetourProvider` still handles deferred links separately. ### Resolve mode [Section titled “Resolve mode”](#resolve-mode) To resolve Detour short links to their full destination URLs at the router level, pass a `config` object: src/app/+native-intent.tsx ```ts import { createDetourNativeIntentHandler } from "@swmansion/react-native-detour/expo-router"; export const redirectSystemPath = createDetourNativeIntentHandler({ config: { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, }, }); ``` In resolve mode, when a user taps a Detour short link (e.g. `https://abc123.godetour.link/xyz`), the handler resolves it to the full destination URL and routes directly to the correct screen, all before the first render. See [Expo Router helper](/docs/sdk/react-native/api-reference#expo-router-helper) for the full list of options (custom hosts, fallback path, timeout, route mapping, etc.). ### Custom domains [Section titled “Custom domains”](#custom-domains) The handler only processes URLs whose hostname matches its `hosts` list, which defaults to `[/\.godetour\.link$/i]`. An app served from a [custom domain](/docs/platform/advanced/custom-domain) needs its own entry. Without one, a Universal Link on `links.yourdomain.com` is passed to Expo Router unhandled and Detour never processes it. src/app/+native-intent.tsx ```ts import { createDetourNativeIntentHandler } from "@swmansion/react-native-detour/expo-router"; export const redirectSystemPath = createDetourNativeIntentHandler({ hosts: ["links.yourdomain.com", /\.godetour\.link$/i], config: { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, }, }); ``` Passing `hosts` **replaces** the default rather than adding to it. The example lists both hosts, so the app keeps handling `.godetour.link` links as well. Keep that second entry: if the custom domain is removed, or the paid plan lapses, Detour serves the app’s links from `.godetour.link` again, and a build that listed the custom domain alone ignores them. Each entry is matched against the hostname alone, in one of three forms: * an exact hostname: `"links.yourdomain.com"` * a suffix wildcard: `"*.yourdomain.com"`, when you serve links from several subdomains * a regular expression: `/\.yourdomain\.com$/i` `hosts` is consulted only after the OS has decided to open your app, so the same host has to be in the native configuration as well: `associatedDomains` on iOS, an `` on Android. See [Installation](/docs/sdk/react-native/sdk-installation#native-link-configuration) for where each one goes. ### Configure `DetourProvider` for native intent [Section titled “Configure DetourProvider for native intent”](#configure-detourprovider-for-native-intent) When using `+native-intent.tsx` to handle Universal Links and App Links, configure the provider to only process deferred links: ```ts const config: Config = { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, linkProcessingMode: "deferred-only", }; ``` This avoids double-processing: `+native-intent.tsx` handles runtime and initial URLs, while `DetourProvider` handles deferred deep links only. ### Advanced examples [Section titled “Advanced examples”](#advanced-examples) The SDK repository covers two common native intent scenarios: * **`expo-router-native-intent`** — resolve mode with a custom `mapToRoute` and scheme URL handling. Start here to transform resolved URLs before routing, for example to strip a path prefix, or to handle custom scheme links alongside Detour links. * **`expo-router-advanced`** — Detour inside an auth- and onboarding-gated flow. Deferred and runtime links are preserved through sign-in and one-time onboarding before the target screen opens. For all runnable examples, see [Examples](/docs/sdk/react-native/examples). ### Alternative: catch-all route [Section titled “Alternative: catch-all route”](#alternative-catch-all-route) If you prefer not to use `+native-intent.tsx`, keep `linkProcessingMode: 'all'` (the default) and add a catch-all route to your navigation stack. `DetourProvider` then processes every link type: deferred, Universal/App, and custom scheme. Read the resolved link with `useDetourContext()` in a top-level component and navigate programmatically: ```ts const { isLinkProcessed, link, clearLink } = useDetourContext(); useEffect(() => { if (!isLinkProcessed || !link) return; router.replace({ pathname: link.pathname, params: link.params }); clearLink(); }, [isLinkProcessed, link]); ``` This was the primary integration pattern before the SDK added native intent support (pre-1.x). It still works. `+native-intent.tsx` resolves links before the first screen renders. ## React Navigation integration [Section titled “React Navigation integration”](#react-navigation-integration) If you use **React Navigation** instead of Expo Router, the SDK provides a linking adapter for React Navigation’s [custom linking API](https://reactnavigation.org/docs/deep-linking?config=static#integrating-with-other-tools). Detour resolves the incoming link, whether deferred, Universal/App, or scheme, and feeds the matched route to React Navigation. Navigation itself runs through your existing `linking` configuration. The adapter needs a `DetourProvider` mounted **above** your `NavigationContainer`. ### Linking setup [Section titled “Linking setup”](#linking-setup) ```ts import { DETOUR_LINKING_PREFIX, Detour } from "@swmansion/react-native-detour"; const linking = { prefixes: [DETOUR_LINKING_PREFIX], async getInitialURL() { return await Detour.getInitialURL(); }, subscribe(listener) { const subscription = Detour.addEventListener("url", ({ url }) => { listener(url); }); return () => subscription.remove(); }, config: { // your existing React Navigation screen-to-path mapping }, }; ``` Pass this object to your navigation container: ```tsx ``` The adapter does not pass the original URL to React Navigation. It rewrites every resolved link into an internal URL of its own, and that URL carries two additions: * **The `detour://` prefix**, exported as `DETOUR_LINKING_PREFIX`. React Navigation only parses URLs whose prefix it recognizes, which is why the constant belongs in `prefixes`. * **Two query params**, `fromDeepLink=true` and `linkType` (`deferred`, `verified`, or `scheme`). They arrive as route params, so a screen can distinguish a deep-link entry from a normal one. A resolved route `/details?id=1` is therefore passed to React Navigation as: ```txt detour://details?id=1&fromDeepLink=true&linkType=deferred ``` Your `config` screen mapping is unchanged. It still matches on `details`. ### Auth-gated deep links [Section titled “Auth-gated deep links”](#auth-gated-deep-links) For apps that gate screens behind sign-in or onboarding, let React Navigation hold the deep link until the target screen becomes reachable. Render the screens conditionally on auth state, and opt in to React Navigation’s pending-link behavior on the navigator: ```tsx // Stack.Navigator only accepts Stack.Screen as a direct child, // so this is a render function rather than a component. function renderAuthScreens({ isSignedIn, isOnboardingCompleted }) { if (!isSignedIn) { return ; } if (!isOnboardingCompleted) { return ; } return ( <> ); } {renderAuthScreens({ isSignedIn, isOnboardingCompleted })} ; ``` A deep link that arrives while the user is signed out is parsed, found unreachable because its target screen is not rendered yet, and remembered. Each time the rendered screen set changes, after sign-in and again after onboarding, React Navigation retries and opens the target screen. For a complete setup, see the [`react-navigation-advanced`](/docs/sdk/react-native/examples) example. Cold start is buffered The adapter holds the initial URL until Detour finishes link processing, so `getInitialURL()` resolves only once the matched route is known. ## Analytics [Section titled “Analytics”](#analytics) Mounting `DetourProvider` turns analytics on. With the provider in place, it logs one event by itself: `app_open`, a retention event sent once per session on first mount (cold start). Universal Link and App Link opens are not client events. The backend records them through the click endpoint (see [Click limits](#click-limits)). Everything else you log yourself, through `DetourAnalytics`. The provider sends every event it receives. ```ts import { DetourAnalytics, DetourEventNames, } from "@swmansion/react-native-detour"; DetourAnalytics.logEvent(DetourEventNames.Purchase, { item: "subscription_premium", currency: "USD", }); DetourAnalytics.logRetention("home_screen_opened"); ``` ### Opting out of automatic events [Section titled “Opting out of automatic events”](#opting-out-of-automatic-events) From **2.3.1** the `app_open` event can be turned off with `shouldTrackAutomaticEvents`. Use it when the SDK must send no events until the user has consented to tracking, for example under App Tracking Transparency on iOS, a consent banner, or a regional privacy requirement: ```ts const config: Config = { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, shouldTrackAutomaticEvents: false, }; ``` Only the automatic event is affected. `DetourAnalytics.logEvent` and `DetourAnalytics.logRetention` still send, so you keep control over what is reported and when. Link resolution is unchanged either way. Deferred and runtime links resolve exactly as before. SDK version header Every request the SDK sends carries an `X-SDK: react-native/` header, used for diagnostics and SDK-usage analysis. It needs no configuration. ## Related pages [Section titled “Related pages”](#related-pages) * [Examples](/docs/sdk/react-native/examples) * [Migration to v2](/docs/sdk/react-native/migration-2-0-0) * [API Reference](/docs/sdk/react-native/api-reference) * [Custom Storage](/docs/sdk/react-native/custom-storage) * [Testing & Troubleshooting](/docs/sdk/react-native/testing) # Testing & Troubleshooting > Test deferred linking on iOS and Android and troubleshoot common Detour React Native SDK issues. Testing deferred linking has platform constraints. The complete deferred flow requires the store redirect chain, that is the App Store or Google Play. Use a native app build Expo Go cannot use your app’s Associated Domains, intent filters, custom scheme, or signing identity, so it cannot validate the complete Detour flow. Use a development build for Universal/App Links and a real device for end-to-end deferred-link testing. ## Testing iOS [Section titled “Testing iOS”](#testing-ios) If your app is not yet released: 1. Remove any existing install from the device so the next open counts as a first launch. 2. Open a Detour link in a mobile browser. 3. Expect the App Store redirect to fail. With no public listing yet, the store has no listing to open, which does not affect the test. 4. Install your development build. 5. Launch the app and confirm the link resolves within the matching window (currently 15 minutes). ## Testing Android [Section titled “Testing Android”](#testing-android) For non-production builds, the install referrer often does not work in store test tracks, so deterministic matching can be unavailable. You can still verify the probabilistic flow: 1. Uninstall the app from the device. 2. Open a Detour link in a mobile browser. 3. Expect the Play Store redirect to fail when the production listing is unavailable. 4. Install your development build. 5. Launch the app and confirm the link resolves within the matching window (currently 15 minutes). For local install referrer testing workarounds, see [How to test the Play Store Install Referrer API](https://medium.com/@madicdjordje/how-to-test-the-play-store-install-referrer-api-78a63d59945b). Missing `click_id` If you encounter click\_id missing in your build see limitations section [Play Store referrer missing](/docs/platform/architecture/architecture-limitations#play-store-referrer-missing) ## Testing Android App Links [Section titled “Testing Android App Links”](#testing-android-app-links) Whether an installed app opens a Detour link depends on the Dashboard SHA-256 matching the key that signed **the build on your device**: | Installation method | SHA-256 to configure | Where to find it | | :------------------------------------------------------ | :------------------------------------- | :------------------------------------------------------------------------------------------------------- | | Google Play, any track | Play App Signing key | Play Console → **Protected with Play** → **Manage Play app signing** → *App signing key certificate* | | `eas build`, any profile, including `development` | Keystore in your EAS account | `eas credentials -p android` → profile → **Keystore** → `SHA256 Fingerprint` | | `npx expo run:android` / `npx react-native run-android` | Project’s `android/app/debug.keystore` | `keytool -list -v -keystore android/app/debug.keystore -storepass android`, or `./gradlew signingReport` | To check any build directly: `apksigner verify --print-certs app-debug.apk`. The complete procedure is described in [Finding your Android SHA-256 fingerprint](/docs/platform/fundamentals/universal-app-links#finding-your-android-sha-256-fingerprint). ## Common issues [Section titled “Common issues”](#common-issues) No match found (`404`) Expected when no deferred link matches, for example on a normal first launch. The SDK treats it as benign: it logs `🔗[Detour:WARNING] No matching deferred link found` and resolves to `null` instead of raising an error. If you did expect a match, check that: * the click is not older than the matching window * the click was recorded in the Dashboard logs * for the deterministic Android flow, `click_id` is present in the referrer `429 Too Many Requests` Add retry/backoff, and avoid heavy testing with one key across many devices. iOS clipboard prompt not appearing * Check that `shouldUseClipboard` is `true`. * Verify the copy-link flow is enabled in the Dashboard. iOS Universal Link opens the browser instead of the app during development Before your domain and App Store listing have fully propagated, iOS resolves the `apple-app-site-association` file through Apple’s CDN, which may not have it yet. Append `?mode=developer` to the matching entry in your iOS `associatedDomains` (e.g. `applinks:yourorg.godetour.link?mode=developer`) so the device fetches the association file directly from your server, bypassing the CDN. This also requires **Developer Mode** on the test device (Settings → Privacy & Security → Developer Mode). Remove the `?mode=developer` suffix before releasing the app. Universal/App link not delivered, `[Detour:CLICK_LIMIT_ERROR]` in logs Your app exceeded its click limit for the current period. Check your plan’s limits in [Billing & Payments](/docs/platform/fundamentals/billing). The dedicated `CLICK_LIMIT_ERROR` is emitted only for Universal/App links. Deferred and short links share the same plan limits, but over the limit they fail to resolve (they return `null`, with no dedicated error), and custom scheme links are not limit-checked at all. A temporary network or backend error never blocks a link, because the check fails open. Link processed twice * Confirm you call `clearLink()` after navigation. * Confirm exactly one `DetourProvider` instance is mounted. Link is not processed on Expo Web The web runtime is intentionally unsupported: on web the provider returns a no-op context and skips SDK initialization. ## Bare React Native CLI build issues [Section titled “Bare React Native CLI build issues”](#bare-react-native-cli-build-issues) These apply only to bare React Native CLI projects that added Expo Modules with `install-expo-modules` (see [Bare React Native projects](/docs/sdk/react-native/sdk-installation#bare-react-native-projects)). They are bundler/environment issues, not SDK bugs. The SDK itself is plain JavaScript. iOS crashes at startup with `TypeError: Cannot read property '…' of undefined` **Cause** — Expo Modules native code is not linked. **Fix** — run `npx install-expo-modules@latest`, then reinstall CocoaPods. Android release build fails with `Serializer did not return expected format` The full message is `Unexpected token 'v', "var __BUND"... is not valid JSON`. **Cause** — `metro.config.js` uses `@react-native/metro-config` while the build bundles with Expo CLI (`export:embed`). **Fix** — make `metro.config.js` extend `expo/metro-config`. See [Expo Modules setup](/docs/sdk/react-native/sdk-installation#expo-modules-setup-react-native-cli). `The NODE_ENV environment variable is required but was not specified` **Cause** — `export:embed` ran without an environment set. **Fix** — set `NODE_ENV=production` for the release build. `cliFile … is not a file … it was a directory` **Cause** — `@expo/cli` is not hoisted to the top-level `node_modules`. **Fix** — install it at the project root (`npm install @expo/cli@`), or do a clean reinstall. Bundling fails because `node` is not found **Cause** — `node` is not on the PATH of the Gradle process. Common with `nvm`. **Fix** — make sure `node` is on PATH for the build, then restart the Gradle daemon with `./gradlew --stop`. ## Debugging [Section titled “Debugging”](#debugging) The SDK runs entirely in JavaScript, so its output goes wherever you already read `console.log`: the Metro terminal, React Native DevTools, or your IDE’s log panel. Filter on `Detour`. Every message carries a prefix that says what happened. Logging is unconditional, and there is no level to raise: | Prefix | What it means | | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `🔗[Detour]` | Normal flow worth seeing: an infrastructure URL was ignored, a short link did not resolve and the original URL was used, no storage implementation was provided. | | `🔗[Detour:WARNING]` | No matching deferred link was found. Expected on a second test with the same link on the same device. | | `🔗[Detour:ERROR]` | Link processing failed. | | `🔗[Detour:RUNTIME_ERROR]` | `useDetourContext()` was called outside a `DetourProvider`. | | `🔗[Detour:NETWORK_ERROR]` | The deferred-link request failed. Check the API key and connectivity. | | `🔗[Detour:CLICK_LIMIT_ERROR]` | The click was over your plan’s limit. See [Click limits](/docs/sdk/react-native/sdk-usage#click-limits). Carries `clicksInPeriod` and `effectiveLimit`. | | `🔗[Detour:ANALYTICS_ERROR]` | An event or retention call failed, or analytics were disabled by a storage failure or by a second mounted provider. | | `🔗[Detour:ANALYTICS_WARNING]` | A `DetourAnalytics` method ran with no provider mounted. The event was dropped. | | `🔗[Detour:WEB_UNSUPPORTED]` | Running on Expo Web, where the provider is a no-op. | For integration patterns that are easy to test end-to-end, use [Examples](/docs/sdk/react-native/examples). # API Reference > Complete API reference for the Detour React Native SDK. ## DetourProvider [Section titled “DetourProvider”](#detourprovider) Top-level React component that initializes the Detour SDK. It manages the full link processing lifecycle: deferred link matching, Universal/App Link listening, initial URL handling, and analytics event transport. On mount, the provider: 1. Checks for an initial URL that launched the app (Universal/App Link on cold start) 2. If no initial link is found, queries the Detour API for a deferred link (first install only) 3. Attaches a runtime listener for Universal/App Links received while the app is running 4. Subscribes to analytics events and forwards them to the Detour backend Use exactly one `DetourProvider` at the root of your app tree. Multiple providers are detected at runtime and will cause analytics events to be dropped. ### Signature [Section titled “Signature”](#signature) ```ts function DetourProvider(props: { config: Config; children: React.ReactNode; }): JSX.Element; ``` ### Web behavior [Section titled “Web behavior”](#web-behavior) On `Platform.OS === 'web'`, provider initialization is skipped. No link processing hooks run, no analytics listeners attach, and no storage is accessed. `DetourProvider` returns a no-op context value: * `isLinkProcessed: true` * `link: null` * `clearLink: () => {}` In development builds, the SDK logs a single warning: `[Detour:WEB_UNSUPPORTED]`. This is safe to use in universal code shared between web and native. ### Config fields [Section titled “Config fields”](#config-fields) * `appID` required * Type `string` * Unique app identifier from the Detour Dashboard. Used to authenticate API requests and route links to the correct app. * `apiKey` required * Type `string` * Publishable API key from the Detour Dashboard. Used as a Bearer token for all API calls (deferred link matching, short-link resolution, analytics) * `shouldUseClipboard` * Type `boolean` Default `true` * iOS only. When `true`, the SDK reads the device clipboard as part of probabilistic fingerprinting for deferred link matching. Set to `false` if your app lacks clipboard permissions or you want to skip clipboard access. * `linkProcessingMode` * Type [`LinkProcessingMode` ](#linkprocessingmode)Default `'all'` * Controls which link sources the SDK processes. See [LinkProcessingMode](#linkprocessingmode) for details on each value. * `storage` * Type [`DetourStorage` ](#detourstorage)Default `AsyncStorage` * Custom storage adapter for SDK persistence. The SDK stores a first-entrance flag (for deferred link matching) and a device ID (for analytics). Defaults to `@react-native-async-storage/async-storage` if installed. ## useDetourContext [Section titled “useDetourContext”](#usedetourcontext) Hook that returns the current link processing state and resolved link payload from the nearest `DetourProvider`. Throws an error if called outside of a `DetourProvider` tree. ### Signature [Section titled “Signature”](#signature-1) ```ts function useDetourContext(): DetourContextType; ``` ### Returned object [Section titled “Returned object”](#returned-object) * `isLinkProcessed` * Type `boolean` * `true` once the SDK has finished all link detection attempts, regardless of whether a link was found. Use this to conditionally show a loading/splash screen during cold start. * `link` * Type [`DetourLink`](#detourlink) * Resolved link data, or `null` if no link was detected. Persists until `clearLink()` is called. * `clearLink` * Type `() => void` * Resets `link` to `null`. Call after handling navigation to prevent repeated redirects. ## DetourAnalytics [Section titled “DetourAnalytics”](#detouranalytics) Analytics module exported from the package root. Requires an active `DetourProvider` in the component tree. Events are silently dropped if no provider is mounted. ### Methods [Section titled “Methods”](#methods) #### `logEvent` [Section titled “logEvent”](#logevent) ```ts DetourAnalytics.logEvent( eventName: DetourEventNames | `${DetourEventNames}`, data?: any, ): void; ``` Records a user action or custom event and sends it to the Detour backend. Pass one of the predefined [`DetourEventNames`](#detoureventnames) or a custom string. The optional `data` parameter accepts arbitrary metadata (e.g. `{ itemId: '123', price: 49.99 }`). #### `logRetention` [Section titled “logRetention”](#logretention) ```ts DetourAnalytics.logRetention(retentionEventName: string): void; ``` Records a retention/lifecycle event for user engagement tracking. The SDK automatically logs an `app_open` retention event when `DetourProvider` mounts for the first time in a session. ### Automatic events [Section titled “Automatic events”](#automatic-events) `DetourProvider` logs an `app_open` retention event once per session on first mount (cold start) without any manual call. Universal/App link opens are recorded server-side through the click endpoint (see [Click limits](/docs/sdk/react-native/2.2.x/sdk-usage#click-limits)) rather than as a client event. All API requests the SDK sends also include an `X-SDK: react-native/` header for diagnostics and SDK-usage analysis. ## Expo Router helper [Section titled “Expo Router helper”](#expo-router-helper) Available under subpath import: ```ts import { createDetourNativeIntentHandler } from '@swmansion/react-native-detour/expo-router'; ``` ### Signature [Section titled “Signature”](#signature-2) ```ts function createDetourNativeIntentHandler( options?: DetourNativeIntentOptions, ): DetourNativeIntentHandler; ``` ### Options [Section titled “Options”](#options) #### `fallbackPath` [Section titled “fallbackPath”](#fallbackpath) | | | | ----------- | -------- | | **Type** | `string` | | **Default** | `''` | The in-app route path returned when an incoming URL matches a Detour host but resolve mode is not enabled or resolution fails. Set this to the route you want users to land on as a safe default, for example `''` for the home screen or `'/welcome'` for an onboarding screen. #### `hosts` [Section titled “hosts”](#hosts) | | | | ----------- | ------------------------- | | **Type** | `Array` | | **Default** | `[/\.godetour\.link$/i]` | Host-matching patterns that determine which incoming URLs the handler should process. Only URLs whose hostname matches at least one entry are intercepted. All other URLs pass through unchanged to Expo Router’s default routing. Use plain strings for exact domain matches, strings beginning with `*.` for suffix wildcards (`*.example.com` matches any subdomain of `example.com`), or regular expressions for anything else. The default pattern matches any subdomain of `godetour.link` (e.g. `abc123.godetour.link`). `hosts` replaces the default The default value is `[/\.godetour\.link$/i]`. Passing `hosts` replaces that value, it does not add to it. An app that passes only its [custom domain](/docs/platform/advanced/custom-domain) stops matching `.godetour.link` links. Keep the default pattern in the list as well: if the custom domain is removed, or the paid plan lapses, Detour serves that app’s links from `.godetour.link` again, and a build that dropped the pattern ignores them. ```ts hosts: ["links.yourdomain.com", /\.godetour\.link$/i], ``` #### `config` [Section titled “config”](#config) | | | | ----------- | --------------------------------- | | **Type** | `DetourNativeIntentResolveConfig` | | **Default** | `undefined` | Providing this object **enables resolve mode**. In resolve mode the handler resolves Detour short links (e.g. `https://abc123.godetour.link/xyz`) to their full destination URLs before routing to the matched in-app screen. Without `config`, the handler operates in **intercept mode**, which matches Detour hosts and returns `fallbackPath` without resolving the link. In resolve mode the handler also verifies each click against your plan’s click limit. If the limit is exceeded, it returns `fallbackPath` and logs `🔗[Detour:CLICK_LIMIT_ERROR]`. See [Click limits](/docs/sdk/react-native/2.2.x/sdk-usage#click-limits). * `apiKey` required * Type `string` * Publishable API key from the Detour Dashboard (same value used in `DetourProvider` config) * `appID` required * Type `string` * App identifier from the Detour Dashboard (same value used in `DetourProvider` config) * `timeoutMs` * Type `number` Default `1200` * Maximum time in milliseconds to wait for short-link resolution. If exceeded, `fallbackPath` is returned. * `onResolveError` * Type `(error, context) => void` * Callback invoked when resolution fails (e.g. network error, invalid link). Useful for logging or error reporting. DetourProvider configuration When using resolve mode, set `linkProcessingMode: 'deferred-only'` in your `DetourProvider` config so the provider only handles deferred links while `+native-intent.tsx` handles Universal Links and App Links. See [Configure DetourProvider for native intent](/docs/sdk/react-native/2.2.x/sdk-usage#configure-detourprovider-for-native-intent). #### `mapToRoute` [Section titled “mapToRoute”](#maptoroute) | | | | ----------- | ----------------------------------------------------------------------------------------- | | **Type** | `(value: DetourNativeIntentResolvedValue) => string` | | **Default** | Built-in mapper that strips the first path segment (app hash) from multi-segment web URLs | Custom function that transforms a resolved URL into an in-app route string. The function receives a `DetourNativeIntentResolvedValue` object containing the `originalPath`, `initial` flag, and the `resolvedUrl`. Use this when your Detour link destinations do not map directly to your app’s route structure, for example if your link URLs contain a prefix segment that does not correspond to an in-app screen. ## Types [Section titled “Types”](#types) ### LinkProcessingMode [Section titled “LinkProcessingMode”](#linkprocessingmode) ```ts type LinkProcessingMode = 'all' | 'web-only' | 'deferred-only'; ``` Controls which link sources `DetourProvider` processes: | Value | Behavior | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `'all'` (default) | Processes all link sources: deferred links from the Detour API, runtime Universal Links (iOS) / App Links (Android), the initial URL that launched the app, and custom scheme deep links (e.g. `myapp://path`) | | `'web-only'` | Same as `'all'` but filters out custom scheme links. Only HTTP/HTTPS URLs (Universal Links and App Links) and deferred links are processed | | `'deferred-only'` | Only processes deferred links obtained from the Detour API. Disables the runtime link listener and skips the initial URL check. Recommended when Expo Router’s `+native-intent.tsx` already handles runtime and initial URLs | ### LinkType [Section titled “LinkType”](#linktype) ```ts type LinkType = 'deferred' | 'verified' | 'scheme'; ``` Describes how the link arrived in the app: | Value | Meaning | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `'deferred'` | The link was obtained from the Detour deferred-link API. This happens when a user who does not have the app installed clicks a Detour link, installs the app, and opens it. The SDK retrieves the original link from the backend on first launch | | `'verified'` | The link arrived as a Universal Link (iOS) or App Link (Android), an HTTP/HTTPS URL that the OS verified belongs to your app and delivered directly to it | | `'scheme'` | The link arrived via a custom URL scheme (e.g. `myapp://path`). These are non-HTTP URLs that the OS routes to your app based on the registered scheme | ### DetourLink [Section titled “DetourLink”](#detourlink) ```ts type DetourLink = { url: string | URL; route: string; pathname: string; params: Record; type: LinkType; } | null; ``` Resolved link data, or `null` if no link was detected. * `url` * Type `string | URL` * Original URL as parsed by the `URL` constructor, or raw string if parsing failed. * `route` * Type `string` * Full in-app route including query string (e.g. `/details?utm=1`). For web URLs, the first path segment (app hash) is stripped. * `pathname` * Type `string` * Path without query string (e.g. `/details`). For web URLs, the first path segment (app hash) is stripped. * `params` * Type `Record` * Query string parameters as a key-value record. All values are strings. * `type` * Type [`LinkType`](#linktype) * How the link arrived in the app. ### DetourContextType [Section titled “DetourContextType”](#detourcontexttype) ```ts type DetourContextType = { isLinkProcessed: boolean; link: DetourLink; clearLink: () => void; }; ``` Shape of the value returned by [`useDetourContext`](#usedetourcontext). * `isLinkProcessed` * Type `boolean` * `true` once the SDK has finished all link detection attempts. `false` during cold start processing. * `link` * Type [`DetourLink`](#detourlink) * Resolved link data or `null`. Persists until `clearLink()` is called. * `clearLink` * Type `() => void` * Resets `link` to `null`. Call after navigating to prevent repeated redirects. ### DetourStorage [Section titled “DetourStorage”](#detourstorage) ```ts interface DetourStorage { getItem(key: string): Promise | string | null; setItem(key: string, value: string): Promise | void; removeItem?(key: string): Promise | void; } ``` Interface for custom storage adapters. The SDK persists a first-entrance flag (used for deferred link matching, which only runs on first app install) and a stable device ID (used for analytics). Methods can be synchronous or asynchronous. By default, `@react-native-async-storage/async-storage` is used. Provide a custom adapter if you use a different storage solution (e.g. MMKV, Expo SecureStore). See [Custom Storage](/docs/sdk/react-native/2.2.x/custom-storage) for examples. ### DetourNativeIntentArgs [Section titled “DetourNativeIntentArgs”](#detournativeintentargs) ```ts type DetourNativeIntentArgs = { path: string; initial: boolean; }; ``` Arguments passed by Expo Router to the `redirectSystemPath` handler. * `path` * Type `string` * Incoming system path or URL from Expo Router. * `initial` * Type `boolean` * `true` when this is the initial URL used to launch the app (cold start), `false` for runtime link events. ### DetourNativeIntentHandler [Section titled “DetourNativeIntentHandler”](#detournativeintenthandler) ```ts type DetourNativeIntentHandler = ( args: DetourNativeIntentArgs, ) => string | Promise; ``` Function signature returned by `createDetourNativeIntentHandler`. Takes the incoming path and returns the resolved in-app route (synchronously or as a promise). ### DetourNativeIntentResolveConfig [Section titled “DetourNativeIntentResolveConfig”](#detournativeintentresolveconfig) ```ts type DetourNativeIntentResolveConfig = { apiKey: string; appID: string; timeoutMs?: number; onResolveError?: (error: unknown, context: DetourNativeIntentArgs) => void; }; ``` Configuration that enables resolve mode in `createDetourNativeIntentHandler`. See [`config` option](#config) for field descriptions. ### DetourNativeIntentOptions [Section titled “DetourNativeIntentOptions”](#detournativeintentoptions) ```ts type DetourNativeIntentOptions = { fallbackPath?: string; hosts?: Array; config?: DetourNativeIntentResolveConfig; mapToRoute?: (value: DetourNativeIntentResolvedValue) => string; }; ``` Options for `createDetourNativeIntentHandler`. See [Options](#options) for detailed field descriptions. ### DetourNativeIntentResolvedValue [Section titled “DetourNativeIntentResolvedValue”](#detournativeintentresolvedvalue) ```ts type DetourNativeIntentResolvedValue = { originalPath: string; initial: boolean; resolvedUrl: URL; }; ``` Object passed to the `mapToRoute` callback after short-link resolution. * `originalPath` * Type `string` * The original incoming system path before resolution. * `initial` * Type `boolean` * `true` if this is the initial URL that launched the app. * `resolvedUrl` * Type `URL` * The resolved destination URL after short-link resolution. ### DetourEventNames [Section titled “DetourEventNames”](#detoureventnames) | Category | Event Name | String Value | | :---------- | :--------------------------- | :------------------------------ | | **General** | `Login` | `login` | | | `Search` | `search` | | | `Share` | `share` | | | `SignUp` | `sign_up` | | | `TutorialBegin` | `tutorial_begin` | | | `TutorialComplete` | `tutorial_complete` | | | `ReEngage` | `re_engage` | | | `Invite` | `invite` | | | `OpenedFromPushNotification` | `opened_from_push_notification` | | **Sales** | `AddPaymentInfo` | `add_payment_info` | | | `AddShippingInfo` | `add_shipping_info` | | | `AddToCart` | `add_to_cart` | | | `RemoveFromCart` | `remove_from_cart` | | | `Refund` | `refund` | | | `ViewItem` | `view_item` | | | `BeginCheckout` | `begin_checkout` | | | `Purchase` | `purchase` | | | `AdImpression` | `ad_impression` | # Custom Storage > Provide a custom storage adapter (MMKV, Expo Secure Store, or your own) for the Detour React Native SDK. By default, the SDK uses `@react-native-async-storage/async-storage` for internal persistence keys. If you prefer another storage engine, pass a custom adapter in `config.storage`. ## Storage interface [Section titled “Storage interface”](#storage-interface) ```ts interface DetourStorage { getItem(key: string): Promise | string | null; setItem(key: string, value: string): Promise | void; removeItem?(key: string): Promise | void; } ``` ## Examples [Section titled “Examples”](#examples) The two adapters below cover the storage engines React Native apps use most often. Any object that matches the `DetourStorage` interface works, so use these as starting points for other engines. ### React Native MMKV [Section titled “React Native MMKV”](#react-native-mmkv) ```ts import { createMMKV } from 'react-native-mmkv'; import { DetourProvider } from '@swmansion/react-native-detour'; const storage = createMMKV(); const mmkvAdapter = { getItem: (key: string) => storage.getString(key) ?? null, setItem: (key: string, value: string) => storage.set(key, value), }; export default function App() { return ( {/* Your app */} ); } ``` ### Expo Secure Store [Section titled “Expo Secure Store”](#expo-secure-store) ```ts import * as SecureStore from 'expo-secure-store'; import { DetourProvider } from '@swmansion/react-native-detour'; const secureStoreAdapter = { getItem: SecureStore.getItemAsync, setItem: SecureStore.setItemAsync, }; export default function App() { return ( {/* Your app */} ); } ``` ## Related pages [Section titled “Related pages”](#related-pages) * [Usage](/docs/sdk/react-native/2.2.x/sdk-usage) * [API Reference](/docs/sdk/react-native/2.2.x/api-reference) # Examples > Runnable Detour React Native examples for Expo Router and React Navigation. The RN SDK repository ships six runnable apps, one per integration style. Clone the one closest to your setup, run it, and copy what you need. Each card below links to the example’s `README.md` on GitHub, with the project structure, setup steps, and full source. ## Example catalog [Section titled “Example catalog”](#example-catalog) [expo-bare ](https://github.com/software-mansion-labs/react-native-detour/blob/v2.2.1/examples/expo-bare/README.md)The SDK on its own, without a router. Use it to check that your configuration works. [expo-router ](https://github.com/software-mansion-labs/react-native-detour/blob/v2.2.1/examples/expo-router/README.md)The smallest Expo Router setup. The app opens the link's screen as soon as a link arrives. [expo-router-native-intent ](https://github.com/software-mansion-labs/react-native-detour/blob/v2.2.1/examples/expo-router-native-intent/README.md)Expo Router resolves short links in its native intent handler, so the app opens the final screen directly. [expo-router-advanced ](https://github.com/software-mansion-labs/react-native-detour/blob/v2.2.1/examples/expo-router-advanced/README.md)Expo Router with sign-in. A link that arrives while the user is signed out opens after they sign in. [react-navigation ](https://github.com/software-mansion-labs/react-native-detour/blob/v2.2.1/examples/react-navigation/README.md)The smallest React Navigation setup. The app maps the link's path to a screen. [react-navigation-advanced ](https://github.com/software-mansion-labs/react-native-detour/blob/v2.2.1/examples/react-navigation-advanced/README.md)React Navigation with sign-in. Links to protected screens open after the user signs in. ## Recommended user path [Section titled “Recommended user path”](#recommended-user-path) 1. Complete [Installation](/docs/sdk/react-native/2.2.x/sdk-installation). 2. Read [Usage](/docs/sdk/react-native/2.2.x/sdk-usage) for API model. 3. Start from the closest example and adapt route mapping/auth flow. 4. Validate behavior with [Testing & Troubleshooting](/docs/sdk/react-native/2.2.x/testing). # Migration to v2 > Migrate to @swmansion/react-native-detour@2.0.0 from 0.x or 1.0.1. This guide covers both migration paths to `@swmansion/react-native-detour@2.0.0`. Choose the right migration path * If your app is still on `0.x`, start from [From 0.x](#from-0x). * If your app is already on `1.0.1`, start from [From 1.0.1](#from-101). ## From 0.x [Section titled “From 0.x”](#from-0x) ### Migration checklist (`0.x` -> `2.0.0`) [Section titled “Migration checklist (0.x -> 2.0.0)”](#migration-checklist-0x---200) | Area | In `0.x` | In `2.0.0` | | ------------------------------- | ------------------------------------------- | ------------------------------------------------------------ | | Config key | Legacy `API_KEY` form in older integrations | `apiKey` | | Link data in context | `linkRoute` / legacy fields | `link` object (`route`, `pathname`, `params`, `url`, `type`) | | Link-source control | No explicit mode in older flows | `linkProcessingMode: 'all' \| 'web-only' \| 'deferred-only'` | | Device fingerprint dependencies | `react-native-device-info` | `expo-constants` + `expo-device` | | Expo Web behavior | Not explicitly guarded | `DetourProvider` disabled on web (no-op context) | ### 1. Update dependencies [Section titled “1. Update dependencies”](#1-update-dependencies) Remove old dependency: ```bash npm uninstall react-native-device-info ``` Install current peer dependencies: ```bash npm install expo-localization expo-clipboard expo-constants expo-device @react-native-async-storage/async-storage expo-application ``` If you use Yarn/PNPM/Bun, use equivalent commands from [Installation](/docs/sdk/react-native/2.2.x/sdk-installation). ### 2. Update config shape [Section titled “2. Update config shape”](#2-update-config-shape) ```ts // before (legacy) const config = { API_KEY: process.env.EXPO_PUBLIC_DETOUR_API_KEY, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID, }; // after const config = { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID, linkProcessingMode: 'all', }; ``` ### 3. Update context usage [Section titled “3. Update context usage”](#3-update-context-usage) ```ts // before (legacy route string) const { isLinkProcessed, linkRoute, clearLink } = useDetourContext(); if (isLinkProcessed && linkRoute) { router.replace(linkRoute); clearLink(); } // after const { isLinkProcessed, link, clearLink } = useDetourContext(); if (isLinkProcessed && link) { router.replace({ pathname: link.pathname, params: link.params }); clearLink(); } ``` ### 4. Choose `linkProcessingMode` [Section titled “4. Choose linkProcessingMode”](#4-choose-linkprocessingmode) * `'all'` (default) — deferred + Universal/App + scheme links * `'web-only'` — deferred + Universal/App links, no scheme links * `'deferred-only'` — deferred only (recommended if Expo Router `+native-intent.tsx` handles runtime links) ## From 1.0.1 [Section titled “From 1.0.1”](#from-101) ### Migration checklist (`1.0.1` -> `2.0.0`) [Section titled “Migration checklist (1.0.1 -> 2.0.0)”](#migration-checklist-101---200) | Area | In `1.0.1` | In `2.0.0` | | ------------------------------- | ----------------------------------- | --------------------------------------------------------------------- | | Device fingerprint dependencies | `react-native-device-info` | `expo-constants` + `expo-device` | | Peer dependency install command | Included `react-native-device-info` | Uses Expo modules only (no `react-native-device-info`) | | Expo Web behavior | Not explicitly guarded | `DetourProvider` is disabled on web (no-op context + one dev warning) | ### Update dependencies [Section titled “Update dependencies”](#update-dependencies) 1. Remove old dependency: ```bash npm uninstall react-native-device-info ``` 2. Install current peer dependencies: ```bash npm install expo-localization expo-clipboard expo-constants expo-device @react-native-async-storage/async-storage expo-application ``` If you use Yarn/PNPM/Bun, use equivalent commands from [Installation](/docs/sdk/react-native/2.2.x/sdk-installation). ### Web behavior update [Section titled “Web behavior update”](#web-behavior-update) In `2.0.0`, `DetourProvider` does not initialize on Expo Web. Returned context on web: * `isLinkProcessed: true` * `link: null` * `clearLink: () => {}` This prevents partial/unsupported initialization on web while keeping app tree stable. ## Shared validation checklist [Section titled “Shared validation checklist”](#shared-validation-checklist) 1. iOS/Android deferred flow still resolves route and parameters. 2. Runtime Universal/App Links still resolve according to `linkProcessingMode`. 3. Expo Web no longer attempts Detour processing and app still renders without provider runtime errors. 4. Exactly one `DetourProvider` is mounted in app root. ## Related pages [Section titled “Related pages”](#related-pages) * [Installation](/docs/sdk/react-native/2.2.x/sdk-installation) * [Usage](/docs/sdk/react-native/2.2.x/sdk-usage) * [API Reference](/docs/sdk/react-native/2.2.x/api-reference) * [Examples](/docs/sdk/react-native/2.2.x/examples) # Installation > Install the Detour React Native SDK in Expo or bare React Native projects. Detour provides a React Native SDK (`@swmansion/react-native-detour`) compatible with Expo and bare React Native projects. Before installation, make sure your app is created in the Detour Dashboard and you have: * `appID` from **API configuration** * publishable `apiKey` from **API configuration** * platform integration snippets from **App configuration** ## Install package [Section titled “Install package”](#install-package) * npm ```bash npm install @swmansion/react-native-detour ``` * yarn ```bash yarn add @swmansion/react-native-detour ``` * pnpm ```bash pnpm add @swmansion/react-native-detour ``` * bun ```bash bun add @swmansion/react-native-detour ``` ## Install peer dependencies (Expo projects) [Section titled “Install peer dependencies (Expo projects)”](#install-peer-dependencies-expo-projects) For Expo projects (including prebuild), install peer dependencies with Expo CLI so versions are matched to your Expo SDK: ```bash npx expo install expo-application expo-clipboard expo-constants expo-device expo-localization @react-native-async-storage/async-storage ``` ## Bare React Native projects [Section titled “Bare React Native projects”](#bare-react-native-projects) If you are not using Expo CLI, install peer dependencies with your package manager and pin versions compatible with your React Native/Expo Modules setup. * npm ```bash npm install expo-application expo-clipboard expo-constants expo-device expo-localization @react-native-async-storage/async-storage ``` * yarn ```bash yarn add expo-application expo-clipboard expo-constants expo-device expo-localization @react-native-async-storage/async-storage ``` * pnpm ```bash pnpm add expo-application expo-clipboard expo-constants expo-device expo-localization @react-native-async-storage/async-storage ``` * bun ```bash bun add expo-application expo-clipboard expo-constants expo-device expo-localization @react-native-async-storage/async-storage ``` Device info provider `expo-device` is a required peer dependency. Unlike storage, the device info source is not configurable. The SDK imports it directly to read the device model, manufacturer, and OS version for the deferred-link fingerprint. There is no adapter to plug in, and `react-native-device-info` is not accepted as an alternative until `2.3.0`. Custom storage `@react-native-async-storage/async-storage` is optional if you provide a custom storage adapter in config. See [Custom Storage](/docs/sdk/react-native/2.2.x/custom-storage). ### Expo Modules setup (React Native CLI) [Section titled “Expo Modules setup (React Native CLI)”](#expo-modules-setup-react-native-cli) The peer dependencies above (`expo-application`, `expo-clipboard`, `expo-constants`, `expo-localization`) are [Expo Modules](https://docs.expo.dev/bare/installing-expo-modules/), and they ship native code that has to be autolinked into your `android/` and `ios/` projects. In an Expo project this happens automatically. In a bare React Native CLI project you add Expo Modules once: ```bash npx install-expo-modules@latest ``` This sets up Expo Modules autolinking (and on iOS raises the deployment target to the Expo Modules minimum). `install-expo-modules` also switches the native JS bundling step to Expo CLI (`export:embed`), which requires `metro.config.js` to extend `expo/metro-config` (a superset of `@react-native/metro-config`, safe in bare projects). The tool usually migrates this for you. Verify it, especially if you had a customized `metro.config.js`: metro.config.js ```js const { getDefaultConfig } = require("expo/metro-config"); const { mergeConfig } = require("@react-native/metro-config"); module.exports = mergeConfig(getDefaultConfig(__dirname), {}); ``` Then align the Expo package versions to your setup: ```bash npx expo install --fix ``` Build errors after adding Expo Modules If the app throws at startup or a release build fails after adding Expo Modules, see [bare React Native CLI build issues](/docs/sdk/react-native/2.2.x/testing#bare-react-native-cli-build-issues). ## Credentials in app config [Section titled “Credentials in app config”](#credentials-in-app-config) ```ts import { type Config } from '@swmansion/react-native-detour'; export const detourConfig: Config = { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, shouldUseClipboard: true, }; ``` Keep `apiKey` and `appID` in environment variables or another secure configuration layer. ## Next steps [Section titled “Next steps”](#next-steps) 1. Implement the provider and link handling in [Usage](/docs/sdk/react-native/2.2.x/sdk-usage). 2. Pick a runnable app pattern in [Examples](/docs/sdk/react-native/2.2.x/examples). 3. If upgrading existing code, follow [Migration to v2](/docs/sdk/react-native/2.2.x/migration-2-0-0) and start from [From 0.x](/docs/sdk/react-native/2.2.x/migration-2-0-0#from-0x) or [From 1.0.1](/docs/sdk/react-native/2.2.x/migration-2-0-0#from-101). # Usage > Initialize DetourProvider, handle resolved links, and integrate Detour with Expo Router or React Navigation. ## Initialize provider in app root [Section titled “Initialize provider in app root”](#initialize-provider-in-app-root) ```ts import { DetourProvider, type Config } from '@swmansion/react-native-detour'; const config: Config = { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, shouldUseClipboard: true, // linkProcessingMode: 'all' // default }; export default function RootLayout() { return ( ); } ``` One DetourProvider per app Use exactly one `DetourProvider` in your app tree. On Expo Web, `DetourProvider` is disabled: it returns a no-op context (`isLinkProcessed: true`, `link: null`) and does not process links. If you want complete, runnable integrations for each navigation style, go to [Examples](/docs/sdk/react-native/2.2.x/examples). ## Handle resolved links with `useDetourContext()` [Section titled “Handle resolved links with useDetourContext()”](#handle-resolved-links-with-usedetourcontext) ```ts import { useDetourContext } from '@swmansion/react-native-detour'; import { useEffect } from 'react'; import { useRouter } from 'expo-router'; export function RootNavigator() { const { isLinkProcessed, link, clearLink } = useDetourContext(); const router = useRouter(); useEffect(() => { if (!isLinkProcessed || !link) return; router.replace({ pathname: link.pathname, params: link.params, }); clearLink(); }, [clearLink, isLinkProcessed, link, router]); if (!isLinkProcessed) { return null; } return ; } ``` ### `link` structure [Section titled “link structure”](#link-structure) `useDetourContext()` returns a `link` object (or `null`): * `link.route` Full in-app route with query string (e.g. `/details?utm=1`). * `link.pathname` Path without query (e.g. `/details`). * `link.params` Parsed query params record. * `link.url` Original URL or raw value. * `link.type` Type `'deferred' | 'verified' | 'scheme'` Use `clearLink()` after handling navigation to avoid repeated redirects. ## Choose `linkProcessingMode` [Section titled “Choose linkProcessingMode”](#choose-linkprocessingmode) `Config.linkProcessingMode` controls which link sources are handled by the SDK: | Value | Deferred | Universal / App | Custom scheme | | ----------------- | -------- | --------------- | ------------- | | `'all'` (default) | Yes | Yes | Yes | | `'web-only'` | Yes | Yes | No | | `'deferred-only'` | Yes | No | No | Use `'deferred-only'` when Expo Router `+native-intent.tsx` already handles runtime and initial URLs. ## Click limits [Section titled “Click limits”](#click-limits) When a Universal Link or App Link is processed, the SDK verifies the click against your plan’s click limit before delivering the resolved link. While your app is within the limit, links resolve normally. When the limit is exceeded, the link is **not** delivered: * With `DetourProvider`, `link` stays `null` and `useDetourContext()` performs no navigation. * With `createDetourNativeIntentHandler` in resolve mode, the handler returns `fallbackPath`. In both cases the SDK logs a `🔗[Detour:CLICK_LIMIT_ERROR]` message with the limit details (`clicksInPeriod`, `effectiveLimit`). This dedicated `🔗[Detour:CLICK_LIMIT_ERROR]` flow applies only to Universal/App links. Deferred-link matching and short-link resolution are subject to the same plan limits on the backend, but the SDK does not surface a dedicated limit error for them. When the limit is exceeded the affected link fails to resolve (it returns `null`) instead of logging a click-limit error. Custom scheme links are handled locally and are not limit-checked. For the actual per-plan click limits, pay-as-you-go overage, and how to raise them, see [Billing & Payments](/docs/platform/fundamentals/billing). Fail-open by design If the limit check cannot complete because of a network or backend error, the SDK delivers the link, so a temporary outage does not block navigation. Limits are enforced only on an explicit “denied” response from the backend. ## Expo Router integration [Section titled “Expo Router integration”](#expo-router-integration) Recommended for Expo apps If you use Expo Router, we **strongly recommend** using our dedicated `createDetourNativeIntentHandler` API in a [`+native-intent.tsx`](https://docs.expo.dev/router/advanced/native-intent/) file. This intercepts URLs at the router level before any screen renders, avoiding flash-of-wrong-screen issues and providing the smoothest deep-linking experience. Expo Router’s `+native-intent.tsx` file lets you intercept and transform incoming system URLs before the router processes them. The Detour SDK provides `createDetourNativeIntentHandler`, a factory that creates a handler pre-configured for Detour link resolution. ### Basic setup [Section titled “Basic setup”](#basic-setup) Create a `+native-intent.tsx` file in your app directory (e.g. `src/app/+native-intent.tsx`): ```ts import { createDetourNativeIntentHandler } from "@swmansion/react-native-detour/expo-router"; export const redirectSystemPath = createDetourNativeIntentHandler(); ``` This is intercept mode Any URL matching a Detour host is caught and the user is routed to the home screen (default `fallbackPath`). Deferred links are still handled separately by `DetourProvider`. ### Resolve mode [Section titled “Resolve mode”](#resolve-mode) To resolve Detour short links to their full destination URLs at the router level, pass a `config` object: ```ts import { createDetourNativeIntentHandler } from "@swmansion/react-native-detour/expo-router"; export const redirectSystemPath = createDetourNativeIntentHandler({ config: { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, }, }); ``` In resolve mode, when a user taps a Detour short link (e.g. `https://abc123.godetour.link/xyz`), the handler resolves it to the full destination URL and routes directly to the correct screen, all before the first render. See [Expo Router helper](/docs/sdk/react-native/2.2.x/api-reference#expo-router-helper) for the full list of options (custom hosts, fallback path, timeout, route mapping, etc.). ### Configure `DetourProvider` for native intent [Section titled “Configure DetourProvider for native intent”](#configure-detourprovider-for-native-intent) When using `+native-intent.tsx` to handle Universal Links and App Links, configure the provider to only process deferred links: ```ts const config: Config = { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, linkProcessingMode: "deferred-only", }; ``` This avoids double-processing: `+native-intent.tsx` handles runtime and initial URLs, while `DetourProvider` handles deferred deep links only. ### Advanced examples [Section titled “Advanced examples”](#advanced-examples) The SDK repository includes examples for common native intent scenarios: * **`expo-router-native-intent`** — Full resolve mode setup with a custom `mapToRoute` function and scheme URL handling. Use this as a starting point when you need to transform resolved URLs before routing (e.g. stripping path prefixes) or when you want to handle custom scheme links alongside Detour links. * **`expo-router-advanced`** — Demonstrates Detour inside an auth- and onboarding-gated flow. Deferred and runtime links are preserved through sign-in and one-time onboarding before navigating to their destination. Use this when your app requires the user to be signed in (and optionally onboarded) before a deep link should take effect. For all runnable examples, see [Examples](/docs/sdk/react-native/2.2.x/examples). ### Alternative: catch-all route without `+native-intent.tsx` [Section titled “Alternative: catch-all route without +native-intent.tsx”](#alternative-catch-all-route-without-native-intenttsx) If you prefer not to use `+native-intent.tsx`, you can handle all incoming links through `DetourProvider` with `linkProcessingMode: 'all'` (the default) and implement a catch-all route in your navigation stack. In this approach, `DetourProvider` processes all link types (deferred, Universal/App Links, and custom scheme). You consume the resolved link via `useDetourContext()` in a top-level component and navigate programmatically: ```ts const { isLinkProcessed, link, clearLink } = useDetourContext(); useEffect(() => { if (!isLinkProcessed || !link) return; router.replace({ pathname: link.pathname, params: link.params }); clearLink(); }, [isLinkProcessed, link]); ``` This was the primary integration pattern before the SDK introduced native intent support (pre-1.x). It still works but the `+native-intent.tsx` approach provides better UX by resolving links before the first screen renders. ## Analytics module [Section titled “Analytics module”](#analytics-module) `DetourAnalytics` emits events that are sent by the active `DetourProvider`. ```ts import { DetourAnalytics, DetourEventNames, } from "@swmansion/react-native-detour"; DetourAnalytics.logEvent(DetourEventNames.Purchase, { item: "subscription_premium", currency: "USD", }); DetourAnalytics.logRetention("home_screen_opened"); ``` ### Automatic events [Section titled “Automatic events”](#automatic-events) Besides the events you log manually, `DetourProvider` reports some events on its own, with no wiring needed: * `app_open` — a retention event logged once per session when the provider first mounts (cold start). Universal Link and App Link opens are no longer emitted as a client event. They are recorded by the backend through a dedicated click endpoint (see [Click limits](#click-limits)). SDK version header Every request the SDK sends to the Detour backend carries an `X-SDK: react-native/` header. It is used for diagnostics and SDK-usage analysis and requires no configuration. ## Related pages [Section titled “Related pages”](#related-pages) * [Examples](/docs/sdk/react-native/2.2.x/examples) * [Migration to v2](/docs/sdk/react-native/2.2.x/migration-2-0-0) * [API Reference](/docs/sdk/react-native/2.2.x/api-reference) * [Custom Storage](/docs/sdk/react-native/2.2.x/custom-storage) * [Testing & Troubleshooting](/docs/sdk/react-native/2.2.x/testing) # Testing & Troubleshooting > Test deferred linking on iOS and Android and troubleshoot common Detour React Native SDK issues. Testing deferred linking has platform constraints. The complete deferred flow requires the store redirect chain, that is the App Store or Google Play. ## Testing iOS [Section titled “Testing iOS”](#testing-ios) If your app is not yet released: 1. Remove any existing install from the device so the next open counts as a first launch. 2. Open a Detour link in a mobile browser. 3. Expect the App Store redirect to fail. With no public listing yet, the store has no listing to open, which does not affect the test. 4. Install your development build. 5. Launch the app and confirm the link resolves within the matching window (currently 15 minutes). ## Testing Android [Section titled “Testing Android”](#testing-android) For non-production builds, the install referrer often does not work in store test tracks, so deterministic matching can be unavailable. You can still verify the probabilistic flow: 1. Uninstall the app from the device. 2. Open a Detour link in a mobile browser. 3. Expect the Play Store redirect to fail when the production listing is unavailable. 4. Install your development build. 5. Launch the app and confirm the link resolves within the matching window (currently 15 minutes). For local install referrer testing workarounds, see [How to test the Play Store Install Referrer API](https://medium.com/@madicdjordje/how-to-test-the-play-store-install-referrer-api-78a63d59945b). Missing `click_id` If you encounter click\_id missing in your build see limitations section [Play Store referrer missing](/docs/platform/architecture/architecture-limitations#play-store-referrer-missing) ## Testing Android App Links [Section titled “Testing Android App Links”](#testing-android-app-links) Whether an installed app opens a Detour link depends on the Dashboard SHA-256 matching the key that signed **the build on your device**: | Installation method | SHA-256 to configure | Where to find it | | :------------------------------------------------------ | :------------------------------------- | :------------------------------------------------------------------------------------------------------- | | Google Play, any track | Play App Signing key | Play Console → **Protected with Play** → **Manage Play app signing** → *App signing key certificate* | | `eas build`, any profile, including `development` | Keystore in your EAS account | `eas credentials -p android` → profile → **Keystore** → `SHA256 Fingerprint` | | `npx expo run:android` / `npx react-native run-android` | Project’s `android/app/debug.keystore` | `keytool -list -v -keystore android/app/debug.keystore -storepass android`, or `./gradlew signingReport` | To check any build directly: `apksigner verify --print-certs app-debug.apk`. The complete procedure is described in [Finding your Android SHA-256 fingerprint](/docs/platform/fundamentals/universal-app-links#finding-your-android-sha-256-fingerprint). ## Common issues [Section titled “Common issues”](#common-issues) * No match found (`404`) * click is older than matching window * click was not recorded in Dashboard logs * for deterministic Android flow: missing `click_id` in referrer * `429 Too Many Requests` * add retry/backoff and avoid heavy testing with one key across many devices * iOS clipboard prompt not appearing * check `shouldUseClipboard: true` * verify copy-link flow is enabled in Dashboard * iOS Universal Link opens the browser instead of the app during development * before your domain and App Store listing have fully propagated, iOS resolves the `apple-app-site-association` file through Apple’s CDN, which may not have it yet * append `?mode=developer` to the matching entry in your iOS `associatedDomains` (e.g. `applinks:yourorg.godetour.link?mode=developer`) so the device fetches the association file directly from your server, bypassing the CDN * this also requires enabling **Developer Mode** on the test device (Settings → Privacy & Security → Developer Mode). Remove the `?mode=developer` suffix before shipping * Universal/App link not delivered, `[Detour:CLICK_LIMIT_ERROR]` in logs * your app exceeded its click limit for the current period. Check your plan’s limits in [Billing & Payments](/docs/platform/fundamentals/billing) * the dedicated `CLICK_LIMIT_ERROR` is emitted only for Universal/App links. Deferred and short links share the same plan limits, but over the limit they fail to resolve (they return `null`, with no dedicated error), and custom scheme links are not limit-checked * a temporary network/backend error never blocks a link (the check fails open) * Link processed twice * confirm you call `clearLink()` after navigation * confirm exactly one `DetourProvider` instance is mounted * Link is not processed on Expo Web * web runtime is intentionally unsupported * provider on web returns a no-op context and skips SDK initialization ## Bare React Native CLI build issues [Section titled “Bare React Native CLI build issues”](#bare-react-native-cli-build-issues) These apply only to bare React Native CLI projects that added Expo Modules with `install-expo-modules` (see [Bare React Native projects](/docs/sdk/react-native/2.2.x/sdk-installation#bare-react-native-projects)). They are bundler/environment issues, not SDK bugs. The SDK itself is plain JavaScript. | Symptom | Cause | Fix | | :--------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | | iOS: `TypeError: Cannot read property '…' of undefined` at startup | Expo Modules native code is not linked | Run `npx install-expo-modules@latest`, then reinstall CocoaPods | | Android release: `Serializer did not return expected format … Unexpected token 'v', "var __BUND"... is not valid JSON` | `metro.config.js` uses `@react-native/metro-config` while the build bundles with Expo CLI (`export:embed`) | Make `metro.config.js` extend `expo/metro-config`. See [Expo Modules setup](/docs/sdk/react-native/2.2.x/sdk-installation#expo-modules-setup-react-native-cli) | | `The NODE_ENV environment variable is required but was not specified` | `export:embed` ran without an environment set | Set `NODE_ENV=production` for the release build | | `cliFile … is not a file … it was a directory` | `@expo/cli` is not hoisted to the top-level `node_modules` | Install it at the project root (`npm install @expo/cli@`) or do a clean reinstall | | Bundling fails because `node` is not found | `node` is not on the PATH of the Gradle process (common with `nvm`) | Make sure `node` is on PATH for the build and restart the Gradle daemon (`./gradlew --stop`) | For integration patterns that are easy to test end-to-end, use [Examples](/docs/sdk/react-native/2.2.x/examples). # API Reference > Complete API reference for the Detour React Native SDK. ## DetourProvider [Section titled “DetourProvider”](#detourprovider) Top-level React component that initializes the Detour SDK. It manages the full link processing lifecycle: deferred link matching, Universal/App Link listening, initial URL handling, and analytics event transport. On mount, the provider: 1. Checks for an initial URL that launched the app (Universal/App Link on cold start) 2. If no initial link is found, queries the Detour API for a deferred link (first install only) 3. Attaches a runtime listener for Universal/App Links received while the app is running 4. Subscribes to analytics events and forwards them to the Detour backend Use exactly one `DetourProvider` at the root of your app tree. Multiple providers are detected at runtime and will cause analytics events to be dropped. ### Signature [Section titled “Signature”](#signature) ```ts function DetourProvider(props: { config: Config; children: React.ReactNode; }): JSX.Element; ``` ### Web behavior [Section titled “Web behavior”](#web-behavior) On `Platform.OS === 'web'`, provider initialization is skipped. No link processing hooks run, no analytics listeners attach, and no storage is accessed. `DetourProvider` returns a no-op context value: * `isLinkProcessed: true` * `link: null` * `clearLink: () => {}` In development builds, the SDK logs a single warning: `[Detour:WEB_UNSUPPORTED]`. This is safe to use in universal code shared between web and native. ### Config fields [Section titled “Config fields”](#config-fields) * `appID` required * Type `string` * Unique app identifier from the Detour Dashboard. Used to authenticate API requests and route links to the correct app. * `apiKey` required * Type `string` * Publishable API key from the Detour Dashboard. Used as a Bearer token for all API calls (deferred link matching, short-link resolution, analytics) * `shouldUseClipboard` * Type `boolean` Default `true` * iOS only. When `true`, the SDK reads the device clipboard as part of probabilistic fingerprinting for deferred link matching. Set to `false` if your app lacks clipboard permissions or you want to skip clipboard access. * `linkProcessingMode` * Type [`LinkProcessingMode` ](#linkprocessingmode)Default `'all'` * Controls which link sources the SDK processes. See [LinkProcessingMode](#linkprocessingmode) for details on each value. * `storage` * Type [`DetourStorage` ](#detourstorage)Default `AsyncStorage` * Custom storage adapter for SDK persistence. The SDK stores a first-entrance flag (for deferred link matching) and a device ID (for analytics). Defaults to `@react-native-async-storage/async-storage` if installed. ## useDetourContext [Section titled “useDetourContext”](#usedetourcontext) Hook that returns the current link processing state and resolved link payload from the nearest `DetourProvider`. Throws an error if called outside of a `DetourProvider` tree. ### Signature [Section titled “Signature”](#signature-1) ```ts function useDetourContext(): DetourContextType; ``` ### Returned object [Section titled “Returned object”](#returned-object) * `isLinkProcessed` * Type `boolean` * `true` once the SDK has finished all link detection attempts, regardless of whether a link was found. Use this to conditionally show a loading/splash screen during cold start. * `link` * Type [`DetourLink`](#detourlink) * Resolved link data, or `null` if no link was detected. Persists until `clearLink()` is called. * `clearLink` * Type `() => void` * Resets `link` to `null`. Call after handling navigation to prevent repeated redirects. ## DetourAnalytics [Section titled “DetourAnalytics”](#detouranalytics) Analytics module exported from the package root. Requires an active `DetourProvider` in the component tree. Events are silently dropped if no provider is mounted. ### Methods [Section titled “Methods”](#methods) #### `logEvent` [Section titled “logEvent”](#logevent) ```ts DetourAnalytics.logEvent( eventName: DetourEventNames | `${DetourEventNames}`, data?: any, ): void; ``` Records a user action or custom event and sends it to the Detour backend. Pass one of the predefined [`DetourEventNames`](#detoureventnames) or a custom string. The optional `data` parameter accepts arbitrary metadata (e.g. `{ itemId: '123', price: 49.99 }`). #### `logRetention` [Section titled “logRetention”](#logretention) ```ts DetourAnalytics.logRetention(retentionEventName: string): void; ``` Records a retention/lifecycle event for user engagement tracking. The SDK automatically logs an `app_open` retention event when `DetourProvider` mounts for the first time in a session. ### Automatic events [Section titled “Automatic events”](#automatic-events) `DetourProvider` emits two events without any manual call: | Event | When | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `app_open` | Retention event logged once per session on first provider mount (cold start) | | `opened_via_universal_link` | Logged when the app is opened through a Universal Link (iOS) or App Link (Android). Not emitted for custom scheme or deferred links | All API requests the SDK sends also include an `X-SDK: react-native/` header for diagnostics and SDK-usage analysis. ## Expo Router helper [Section titled “Expo Router helper”](#expo-router-helper) Available under subpath import: ```ts import { createDetourNativeIntentHandler } from '@swmansion/react-native-detour/expo-router'; ``` ### Signature [Section titled “Signature”](#signature-2) ```ts function createDetourNativeIntentHandler( options?: DetourNativeIntentOptions, ): DetourNativeIntentHandler; ``` ### Options [Section titled “Options”](#options) #### `fallbackPath` [Section titled “fallbackPath”](#fallbackpath) | | | | ----------- | -------- | | **Type** | `string` | | **Default** | `''` | The in-app route path returned when an incoming URL matches a Detour host but resolve mode is not enabled or resolution fails. Set this to the route you want users to land on as a safe default, for example `''` for the home screen or `'/welcome'` for an onboarding screen. #### `hosts` [Section titled “hosts”](#hosts) | | | | ----------- | ------------------------- | | **Type** | `Array` | | **Default** | `[/\.godetour\.link$/i]` | Host-matching patterns that determine which incoming URLs the handler should process. Only URLs whose hostname matches at least one entry are intercepted. All other URLs pass through unchanged to Expo Router’s default routing. Use plain strings for exact domain matches, strings beginning with `*.` for suffix wildcards (`*.example.com` matches any subdomain of `example.com`), or regular expressions for anything else. The default pattern matches any subdomain of `godetour.link` (e.g. `abc123.godetour.link`). `hosts` replaces the default The default value is `[/\.godetour\.link$/i]`. Passing `hosts` replaces that value, it does not add to it. An app that passes only its [custom domain](/docs/platform/advanced/custom-domain) stops matching `.godetour.link` links. Keep the default pattern in the list as well: if the custom domain is removed, or the paid plan lapses, Detour serves that app’s links from `.godetour.link` again, and a build that dropped the pattern ignores them. ```ts hosts: ["links.yourdomain.com", /\.godetour\.link$/i], ``` #### `config` [Section titled “config”](#config) | | | | ----------- | --------------------------------- | | **Type** | `DetourNativeIntentResolveConfig` | | **Default** | `undefined` | Providing this object **enables resolve mode**. In resolve mode the handler resolves Detour short links (e.g. `https://abc123.godetour.link/xyz`) to their full destination URLs before routing to the matched in-app screen. Without `config`, the handler operates in **intercept mode**, which matches Detour hosts and returns `fallbackPath` without resolving the link. * `apiKey` required * Type `string` * Publishable API key from the Detour Dashboard (same value used in `DetourProvider` config) * `appID` required * Type `string` * App identifier from the Detour Dashboard (same value used in `DetourProvider` config) * `timeoutMs` * Type `number` Default `1200` * Maximum time in milliseconds to wait for short-link resolution. If exceeded, `fallbackPath` is returned. * `onResolveError` * Type `(error, context) => void` * Callback invoked when resolution fails (e.g. network error, invalid link). Useful for logging or error reporting. DetourProvider configuration When using resolve mode, set `linkProcessingMode: 'deferred-only'` in your `DetourProvider` config so the provider only handles deferred links while `+native-intent.tsx` handles Universal Links and App Links. See [Configure DetourProvider for native intent](/docs/sdk/react-native/2.1.x/sdk-usage#configure-detourprovider-for-native-intent). #### `mapToRoute` [Section titled “mapToRoute”](#maptoroute) | | | | ----------- | ----------------------------------------------------------------------------------------- | | **Type** | `(value: DetourNativeIntentResolvedValue) => string` | | **Default** | Built-in mapper that strips the first path segment (app hash) from multi-segment web URLs | Custom function that transforms a resolved URL into an in-app route string. The function receives a `DetourNativeIntentResolvedValue` object containing the `originalPath`, `initial` flag, and the `resolvedUrl`. Use this when your Detour link destinations do not map directly to your app’s route structure, for example if your link URLs contain a prefix segment that does not correspond to an in-app screen. ## Types [Section titled “Types”](#types) ### LinkProcessingMode [Section titled “LinkProcessingMode”](#linkprocessingmode) ```ts type LinkProcessingMode = 'all' | 'web-only' | 'deferred-only'; ``` Controls which link sources `DetourProvider` processes: | Value | Behavior | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `'all'` (default) | Processes all link sources: deferred links from the Detour API, runtime Universal Links (iOS) / App Links (Android), the initial URL that launched the app, and custom scheme deep links (e.g. `myapp://path`) | | `'web-only'` | Same as `'all'` but filters out custom scheme links. Only HTTP/HTTPS URLs (Universal Links and App Links) and deferred links are processed | | `'deferred-only'` | Only processes deferred links obtained from the Detour API. Disables the runtime link listener and skips the initial URL check. Recommended when Expo Router’s `+native-intent.tsx` already handles runtime and initial URLs | ### LinkType [Section titled “LinkType”](#linktype) ```ts type LinkType = 'deferred' | 'verified' | 'scheme'; ``` Describes how the link arrived in the app: | Value | Meaning | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `'deferred'` | The link was obtained from the Detour deferred-link API. This happens when a user who does not have the app installed clicks a Detour link, installs the app, and opens it. The SDK retrieves the original link from the backend on first launch | | `'verified'` | The link arrived as a Universal Link (iOS) or App Link (Android), an HTTP/HTTPS URL that the OS verified belongs to your app and delivered directly to it | | `'scheme'` | The link arrived via a custom URL scheme (e.g. `myapp://path`). These are non-HTTP URLs that the OS routes to your app based on the registered scheme | ### DetourLink [Section titled “DetourLink”](#detourlink) ```ts type DetourLink = { url: string | URL; route: string; pathname: string; params: Record; type: LinkType; } | null; ``` Resolved link data, or `null` if no link was detected. * `url` * Type `string | URL` * Original URL as parsed by the `URL` constructor, or raw string if parsing failed. * `route` * Type `string` * Full in-app route including query string (e.g. `/details?utm=1`). For web URLs, the first path segment (app hash) is stripped. * `pathname` * Type `string` * Path without query string (e.g. `/details`). For web URLs, the first path segment (app hash) is stripped. * `params` * Type `Record` * Query string parameters as a key-value record. All values are strings. * `type` * Type [`LinkType`](#linktype) * How the link arrived in the app. ### DetourContextType [Section titled “DetourContextType”](#detourcontexttype) ```ts type DetourContextType = { isLinkProcessed: boolean; link: DetourLink; clearLink: () => void; }; ``` Shape of the value returned by [`useDetourContext`](#usedetourcontext). * `isLinkProcessed` * Type `boolean` * `true` once the SDK has finished all link detection attempts. `false` during cold start processing. * `link` * Type [`DetourLink`](#detourlink) * Resolved link data or `null`. Persists until `clearLink()` is called. * `clearLink` * Type `() => void` * Resets `link` to `null`. Call after navigating to prevent repeated redirects. ### DetourStorage [Section titled “DetourStorage”](#detourstorage) ```ts interface DetourStorage { getItem(key: string): Promise | string | null; setItem(key: string, value: string): Promise | void; removeItem?(key: string): Promise | void; } ``` Interface for custom storage adapters. The SDK persists a first-entrance flag (used for deferred link matching, which only runs on first app install) and a stable device ID (used for analytics). Methods can be synchronous or asynchronous. By default, `@react-native-async-storage/async-storage` is used. Provide a custom adapter if you use a different storage solution (e.g. MMKV, Expo SecureStore). See [Custom Storage](/docs/sdk/react-native/2.1.x/custom-storage) for examples. ### DetourNativeIntentArgs [Section titled “DetourNativeIntentArgs”](#detournativeintentargs) ```ts type DetourNativeIntentArgs = { path: string; initial: boolean; }; ``` Arguments passed by Expo Router to the `redirectSystemPath` handler. * `path` * Type `string` * Incoming system path or URL from Expo Router. * `initial` * Type `boolean` * `true` when this is the initial URL used to launch the app (cold start), `false` for runtime link events. ### DetourNativeIntentHandler [Section titled “DetourNativeIntentHandler”](#detournativeintenthandler) ```ts type DetourNativeIntentHandler = ( args: DetourNativeIntentArgs, ) => string | Promise; ``` Function signature returned by `createDetourNativeIntentHandler`. Takes the incoming path and returns the resolved in-app route (synchronously or as a promise). ### DetourNativeIntentResolveConfig [Section titled “DetourNativeIntentResolveConfig”](#detournativeintentresolveconfig) ```ts type DetourNativeIntentResolveConfig = { apiKey: string; appID: string; timeoutMs?: number; onResolveError?: (error: unknown, context: DetourNativeIntentArgs) => void; }; ``` Configuration that enables resolve mode in `createDetourNativeIntentHandler`. See [`config` option](#config) for field descriptions. ### DetourNativeIntentOptions [Section titled “DetourNativeIntentOptions”](#detournativeintentoptions) ```ts type DetourNativeIntentOptions = { fallbackPath?: string; hosts?: Array; config?: DetourNativeIntentResolveConfig; mapToRoute?: (value: DetourNativeIntentResolvedValue) => string; }; ``` Options for `createDetourNativeIntentHandler`. See [Options](#options) for detailed field descriptions. ### DetourNativeIntentResolvedValue [Section titled “DetourNativeIntentResolvedValue”](#detournativeintentresolvedvalue) ```ts type DetourNativeIntentResolvedValue = { originalPath: string; initial: boolean; resolvedUrl: URL; }; ``` Object passed to the `mapToRoute` callback after short-link resolution. * `originalPath` * Type `string` * The original incoming system path before resolution. * `initial` * Type `boolean` * `true` if this is the initial URL that launched the app. * `resolvedUrl` * Type `URL` * The resolved destination URL after short-link resolution. ### DetourEventNames [Section titled “DetourEventNames”](#detoureventnames) | Category | Event Name | String Value | | :---------- | :--------------------------- | :------------------------------ | | **General** | `Login` | `login` | | | `Search` | `search` | | | `Share` | `share` | | | `SignUp` | `sign_up` | | | `TutorialBegin` | `tutorial_begin` | | | `TutorialComplete` | `tutorial_complete` | | | `ReEngage` | `re_engage` | | | `Invite` | `invite` | | | `OpenedFromPushNotification` | `opened_from_push_notification` | | **Sales** | `AddPaymentInfo` | `add_payment_info` | | | `AddShippingInfo` | `add_shipping_info` | | | `AddToCart` | `add_to_cart` | | | `RemoveFromCart` | `remove_from_cart` | | | `Refund` | `refund` | | | `ViewItem` | `view_item` | | | `BeginCheckout` | `begin_checkout` | | | `Purchase` | `purchase` | | | `AdImpression` | `ad_impression` | # Custom Storage > Provide a custom storage adapter (MMKV, Expo Secure Store, or your own) for the Detour React Native SDK. By default, the SDK uses `@react-native-async-storage/async-storage` for internal persistence keys. If you prefer another storage engine, pass a custom adapter in `config.storage`. ## Storage interface [Section titled “Storage interface”](#storage-interface) ```ts interface DetourStorage { getItem(key: string): Promise | string | null; setItem(key: string, value: string): Promise | void; removeItem?(key: string): Promise | void; } ``` ## Examples [Section titled “Examples”](#examples) The two adapters below cover the storage engines React Native apps use most often. Any object that matches the `DetourStorage` interface works, so use these as starting points for other engines. ### React Native MMKV [Section titled “React Native MMKV”](#react-native-mmkv) ```ts import { createMMKV } from 'react-native-mmkv'; import { DetourProvider } from '@swmansion/react-native-detour'; const storage = createMMKV(); const mmkvAdapter = { getItem: (key: string) => storage.getString(key) ?? null, setItem: (key: string, value: string) => storage.set(key, value), }; export default function App() { return ( {/* Your app */} ); } ``` ### Expo Secure Store [Section titled “Expo Secure Store”](#expo-secure-store) ```ts import * as SecureStore from 'expo-secure-store'; import { DetourProvider } from '@swmansion/react-native-detour'; const secureStoreAdapter = { getItem: SecureStore.getItemAsync, setItem: SecureStore.setItemAsync, }; export default function App() { return ( {/* Your app */} ); } ``` ## Related pages [Section titled “Related pages”](#related-pages) * [Usage](/docs/sdk/react-native/2.1.x/sdk-usage) * [API Reference](/docs/sdk/react-native/2.1.x/api-reference) # Examples > Runnable Detour React Native examples for Expo Router and React Navigation. The RN SDK repository ships six runnable apps, one per integration style. Clone the one closest to your setup, run it, and copy what you need. Each card below links to the example’s `README.md` on GitHub, with the project structure, setup steps, and full source. ## Example catalog [Section titled “Example catalog”](#example-catalog) [expo-bare ](https://github.com/software-mansion-labs/react-native-detour/blob/v2.1.1/examples/expo-bare/README.md)The SDK on its own, without a router. Use it to check that your configuration works. [expo-router ](https://github.com/software-mansion-labs/react-native-detour/blob/v2.1.1/examples/expo-router/README.md)The smallest Expo Router setup. The app opens the link's screen as soon as a link arrives. [expo-router-native-intent ](https://github.com/software-mansion-labs/react-native-detour/blob/v2.1.1/examples/expo-router-native-intent/README.md)Expo Router resolves short links in its native intent handler, so the app opens the final screen directly. [expo-router-advanced ](https://github.com/software-mansion-labs/react-native-detour/blob/v2.1.1/examples/expo-router-advanced/README.md)Expo Router with sign-in. A link that arrives while the user is signed out opens after they sign in. [react-navigation ](https://github.com/software-mansion-labs/react-native-detour/blob/v2.1.1/examples/react-navigation/README.md)The smallest React Navigation setup. The app maps the link's path to a screen. [react-navigation-advanced ](https://github.com/software-mansion-labs/react-native-detour/blob/v2.1.1/examples/react-navigation-advanced/README.md)React Navigation with sign-in. Links to protected screens open after the user signs in. ## Recommended user path [Section titled “Recommended user path”](#recommended-user-path) 1. Complete [Installation](/docs/sdk/react-native/2.1.x/sdk-installation). 2. Read [Usage](/docs/sdk/react-native/2.1.x/sdk-usage) for API model. 3. Start from the closest example and adapt route mapping/auth flow. 4. Validate behavior with [Testing & Troubleshooting](/docs/sdk/react-native/2.1.x/testing). # Migration to v2 > Migrate to @swmansion/react-native-detour@2.0.0 from 0.x or 1.0.1. This guide covers both migration paths to `@swmansion/react-native-detour@2.0.0`. Choose the right migration path * If your app is still on `0.x`, start from [From 0.x](#from-0x). * If your app is already on `1.0.1`, start from [From 1.0.1](#from-101). ## From 0.x [Section titled “From 0.x”](#from-0x) ### Migration checklist (`0.x` -> `2.0.0`) [Section titled “Migration checklist (0.x -> 2.0.0)”](#migration-checklist-0x---200) | Area | In `0.x` | In `2.0.0` | | ------------------------------- | ------------------------------------------- | ------------------------------------------------------------ | | Config key | Legacy `API_KEY` form in older integrations | `apiKey` | | Link data in context | `linkRoute` / legacy fields | `link` object (`route`, `pathname`, `params`, `url`, `type`) | | Link-source control | No explicit mode in older flows | `linkProcessingMode: 'all' \| 'web-only' \| 'deferred-only'` | | Device fingerprint dependencies | `react-native-device-info` | `expo-constants` + `expo-device` | | Expo Web behavior | Not explicitly guarded | `DetourProvider` disabled on web (no-op context) | ### 1. Update dependencies [Section titled “1. Update dependencies”](#1-update-dependencies) Remove old dependency: ```bash npm uninstall react-native-device-info ``` Install current peer dependencies: ```bash npm install expo-localization expo-clipboard expo-constants expo-device @react-native-async-storage/async-storage expo-application ``` If you use Yarn/PNPM/Bun, use equivalent commands from [Installation](/docs/sdk/react-native/2.1.x/sdk-installation). ### 2. Update config shape [Section titled “2. Update config shape”](#2-update-config-shape) ```ts // before (legacy) const config = { API_KEY: process.env.EXPO_PUBLIC_DETOUR_API_KEY, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID, }; // after const config = { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID, linkProcessingMode: 'all', }; ``` ### 3. Update context usage [Section titled “3. Update context usage”](#3-update-context-usage) ```ts // before (legacy route string) const { isLinkProcessed, linkRoute, clearLink } = useDetourContext(); if (isLinkProcessed && linkRoute) { router.replace(linkRoute); clearLink(); } // after const { isLinkProcessed, link, clearLink } = useDetourContext(); if (isLinkProcessed && link) { router.replace({ pathname: link.pathname, params: link.params }); clearLink(); } ``` ### 4. Choose `linkProcessingMode` [Section titled “4. Choose linkProcessingMode”](#4-choose-linkprocessingmode) * `'all'` (default) — deferred + Universal/App + scheme links * `'web-only'` — deferred + Universal/App links, no scheme links * `'deferred-only'` — deferred only (recommended if Expo Router `+native-intent.tsx` handles runtime links) ## From 1.0.1 [Section titled “From 1.0.1”](#from-101) ### Migration checklist (`1.0.1` -> `2.0.0`) [Section titled “Migration checklist (1.0.1 -> 2.0.0)”](#migration-checklist-101---200) | Area | In `1.0.1` | In `2.0.0` | | ------------------------------- | ----------------------------------- | --------------------------------------------------------------------- | | Device fingerprint dependencies | `react-native-device-info` | `expo-constants` + `expo-device` | | Peer dependency install command | Included `react-native-device-info` | Uses Expo modules only (no `react-native-device-info`) | | Expo Web behavior | Not explicitly guarded | `DetourProvider` is disabled on web (no-op context + one dev warning) | ### Update dependencies [Section titled “Update dependencies”](#update-dependencies) 1. Remove old dependency: ```bash npm uninstall react-native-device-info ``` 2. Install current peer dependencies: ```bash npm install expo-localization expo-clipboard expo-constants expo-device @react-native-async-storage/async-storage expo-application ``` If you use Yarn/PNPM/Bun, use equivalent commands from [Installation](/docs/sdk/react-native/2.1.x/sdk-installation). ### Web behavior update [Section titled “Web behavior update”](#web-behavior-update) In `2.0.0`, `DetourProvider` does not initialize on Expo Web. Returned context on web: * `isLinkProcessed: true` * `link: null` * `clearLink: () => {}` This prevents partial/unsupported initialization on web while keeping app tree stable. ## Shared validation checklist [Section titled “Shared validation checklist”](#shared-validation-checklist) 1. iOS/Android deferred flow still resolves route and parameters. 2. Runtime Universal/App Links still resolve according to `linkProcessingMode`. 3. Expo Web no longer attempts Detour processing and app still renders without provider runtime errors. 4. Exactly one `DetourProvider` is mounted in app root. ## Related pages [Section titled “Related pages”](#related-pages) * [Installation](/docs/sdk/react-native/2.1.x/sdk-installation) * [Usage](/docs/sdk/react-native/2.1.x/sdk-usage) * [API Reference](/docs/sdk/react-native/2.1.x/api-reference) * [Examples](/docs/sdk/react-native/2.1.x/examples) # Installation > Install the Detour React Native SDK in Expo or bare React Native projects. Detour provides a React Native SDK (`@swmansion/react-native-detour`) compatible with Expo and bare React Native projects. Before installation, make sure your app is created in the Detour Dashboard and you have: * `appID` from **API configuration** * publishable `apiKey` from **API configuration** * platform integration snippets from **App configuration** ## Install package [Section titled “Install package”](#install-package) * npm ```bash npm install @swmansion/react-native-detour ``` * yarn ```bash yarn add @swmansion/react-native-detour ``` * pnpm ```bash pnpm add @swmansion/react-native-detour ``` * bun ```bash bun add @swmansion/react-native-detour ``` ## Install peer dependencies (Expo projects) [Section titled “Install peer dependencies (Expo projects)”](#install-peer-dependencies-expo-projects) For Expo projects (including prebuild), install peer dependencies with Expo CLI so versions are matched to your Expo SDK: ```bash npx expo install expo-application expo-clipboard expo-constants expo-device expo-localization @react-native-async-storage/async-storage ``` ## Bare React Native projects [Section titled “Bare React Native projects”](#bare-react-native-projects) If you are not using Expo CLI, install peer dependencies with your package manager and pin versions compatible with your React Native/Expo Modules setup. * npm ```bash npm install expo-application expo-clipboard expo-constants expo-device expo-localization @react-native-async-storage/async-storage ``` * yarn ```bash yarn add expo-application expo-clipboard expo-constants expo-device expo-localization @react-native-async-storage/async-storage ``` * pnpm ```bash pnpm add expo-application expo-clipboard expo-constants expo-device expo-localization @react-native-async-storage/async-storage ``` * bun ```bash bun add expo-application expo-clipboard expo-constants expo-device expo-localization @react-native-async-storage/async-storage ``` Device info provider `expo-device` is a required peer dependency. Unlike storage, the device info source is not configurable. The SDK imports it directly to read the device model, manufacturer, and OS version for the deferred-link fingerprint. There is no adapter to plug in, and `react-native-device-info` is not accepted as an alternative until `2.3.0`. Custom storage `@react-native-async-storage/async-storage` is optional if you provide a custom storage adapter in config. See [Custom Storage](/docs/sdk/react-native/2.1.x/custom-storage). ### Expo Modules setup (React Native CLI) [Section titled “Expo Modules setup (React Native CLI)”](#expo-modules-setup-react-native-cli) The peer dependencies above (`expo-application`, `expo-clipboard`, `expo-constants`, `expo-localization`) are [Expo Modules](https://docs.expo.dev/bare/installing-expo-modules/), and they ship native code that has to be autolinked into your `android/` and `ios/` projects. In an Expo project this happens automatically. In a bare React Native CLI project you add Expo Modules once: ```bash npx install-expo-modules@latest ``` This sets up Expo Modules autolinking (and on iOS raises the deployment target to the Expo Modules minimum). `install-expo-modules` also switches the native JS bundling step to Expo CLI (`export:embed`), which requires `metro.config.js` to extend `expo/metro-config` (a superset of `@react-native/metro-config`, safe in bare projects). The tool usually migrates this for you. Verify it, especially if you had a customized `metro.config.js`: metro.config.js ```js const { getDefaultConfig } = require("expo/metro-config"); const { mergeConfig } = require("@react-native/metro-config"); module.exports = mergeConfig(getDefaultConfig(__dirname), {}); ``` Then align the Expo package versions to your setup: ```bash npx expo install --fix ``` Build errors after adding Expo Modules If the app throws at startup or a release build fails after adding Expo Modules, see [bare React Native CLI build issues](/docs/sdk/react-native/2.1.x/testing#bare-react-native-cli-build-issues). ## Credentials in app config [Section titled “Credentials in app config”](#credentials-in-app-config) ```ts import { type Config } from '@swmansion/react-native-detour'; export const detourConfig: Config = { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, shouldUseClipboard: true, }; ``` Keep `apiKey` and `appID` in environment variables or another secure configuration layer. ## Next steps [Section titled “Next steps”](#next-steps) 1. Implement the provider and link handling in [Usage](/docs/sdk/react-native/2.1.x/sdk-usage). 2. Pick a runnable app pattern in [Examples](/docs/sdk/react-native/2.1.x/examples). 3. If upgrading existing code, follow [Migration to v2](/docs/sdk/react-native/2.1.x/migration-2-0-0) and start from [From 0.x](/docs/sdk/react-native/2.1.x/migration-2-0-0#from-0x) or [From 1.0.1](/docs/sdk/react-native/2.1.x/migration-2-0-0#from-101). # Usage > Initialize DetourProvider, handle resolved links, and integrate Detour with Expo Router or React Navigation. ## Initialize provider in app root [Section titled “Initialize provider in app root”](#initialize-provider-in-app-root) ```ts import { DetourProvider, type Config } from '@swmansion/react-native-detour'; const config: Config = { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, shouldUseClipboard: true, // linkProcessingMode: 'all' // default }; export default function RootLayout() { return ( ); } ``` One DetourProvider per app Use exactly one `DetourProvider` in your app tree. On Expo Web, `DetourProvider` is disabled: it returns a no-op context (`isLinkProcessed: true`, `link: null`) and does not process links. If you want complete, runnable integrations for each navigation style, go to [Examples](/docs/sdk/react-native/2.1.x/examples). ## Handle resolved links with `useDetourContext()` [Section titled “Handle resolved links with useDetourContext()”](#handle-resolved-links-with-usedetourcontext) ```ts import { useDetourContext } from '@swmansion/react-native-detour'; import { useEffect } from 'react'; import { useRouter } from 'expo-router'; export function RootNavigator() { const { isLinkProcessed, link, clearLink } = useDetourContext(); const router = useRouter(); useEffect(() => { if (!isLinkProcessed || !link) return; router.replace({ pathname: link.pathname, params: link.params, }); clearLink(); }, [clearLink, isLinkProcessed, link, router]); if (!isLinkProcessed) { return null; } return ; } ``` ### `link` structure [Section titled “link structure”](#link-structure) `useDetourContext()` returns a `link` object (or `null`): * `link.route` Full in-app route with query string (e.g. `/details?utm=1`). * `link.pathname` Path without query (e.g. `/details`). * `link.params` Parsed query params record. * `link.url` Original URL or raw value. * `link.type` Type `'deferred' | 'verified' | 'scheme'` Use `clearLink()` after handling navigation to avoid repeated redirects. ## Choose `linkProcessingMode` [Section titled “Choose linkProcessingMode”](#choose-linkprocessingmode) `Config.linkProcessingMode` controls which link sources are handled by the SDK: | Value | Deferred | Universal / App | Custom scheme | | ----------------- | -------- | --------------- | ------------- | | `'all'` (default) | Yes | Yes | Yes | | `'web-only'` | Yes | Yes | No | | `'deferred-only'` | Yes | No | No | Use `'deferred-only'` when Expo Router `+native-intent.tsx` already handles runtime and initial URLs. ## Expo Router integration [Section titled “Expo Router integration”](#expo-router-integration) Recommended for Expo apps If you use Expo Router, we **strongly recommend** using our dedicated `createDetourNativeIntentHandler` API in a [`+native-intent.tsx`](https://docs.expo.dev/router/advanced/native-intent/) file. This intercepts URLs at the router level before any screen renders, avoiding flash-of-wrong-screen issues and providing the smoothest deep-linking experience. Expo Router’s `+native-intent.tsx` file lets you intercept and transform incoming system URLs before the router processes them. The Detour SDK provides `createDetourNativeIntentHandler`, a factory that creates a handler pre-configured for Detour link resolution. ### Basic setup [Section titled “Basic setup”](#basic-setup) Create a `+native-intent.tsx` file in your app directory (e.g. `src/app/+native-intent.tsx`): ```ts import { createDetourNativeIntentHandler } from "@swmansion/react-native-detour/expo-router"; export const redirectSystemPath = createDetourNativeIntentHandler(); ``` This is intercept mode Any URL matching a Detour host is caught and the user is routed to the home screen (default `fallbackPath`). Deferred links are still handled separately by `DetourProvider`. ### Resolve mode [Section titled “Resolve mode”](#resolve-mode) To resolve Detour short links to their full destination URLs at the router level, pass a `config` object: ```ts import { createDetourNativeIntentHandler } from "@swmansion/react-native-detour/expo-router"; export const redirectSystemPath = createDetourNativeIntentHandler({ config: { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, }, }); ``` In resolve mode, when a user taps a Detour short link (e.g. `https://abc123.godetour.link/xyz`), the handler resolves it to the full destination URL and routes directly to the correct screen, all before the first render. See [Expo Router helper](/docs/sdk/react-native/2.1.x/api-reference#expo-router-helper) for the full list of options (custom hosts, fallback path, timeout, route mapping, etc.). ### Configure `DetourProvider` for native intent [Section titled “Configure DetourProvider for native intent”](#configure-detourprovider-for-native-intent) When using `+native-intent.tsx` to handle Universal Links and App Links, configure the provider to only process deferred links: ```ts const config: Config = { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, linkProcessingMode: "deferred-only", }; ``` This avoids double-processing: `+native-intent.tsx` handles runtime and initial URLs, while `DetourProvider` handles deferred deep links only. ### Advanced examples [Section titled “Advanced examples”](#advanced-examples) The SDK repository includes examples for common native intent scenarios: * **`expo-router-native-intent`** — Full resolve mode setup with a custom `mapToRoute` function and scheme URL handling. Use this as a starting point when you need to transform resolved URLs before routing (e.g. stripping path prefixes) or when you want to handle custom scheme links alongside Detour links. * **`expo-router-advanced`** — Demonstrates Detour inside an auth- and onboarding-gated flow. Deferred and runtime links are preserved through sign-in and one-time onboarding before navigating to their destination. Use this when your app requires the user to be signed in (and optionally onboarded) before a deep link should take effect. For all runnable examples, see [Examples](/docs/sdk/react-native/2.1.x/examples). ### Alternative: catch-all route without `+native-intent.tsx` [Section titled “Alternative: catch-all route without +native-intent.tsx”](#alternative-catch-all-route-without-native-intenttsx) If you prefer not to use `+native-intent.tsx`, you can handle all incoming links through `DetourProvider` with `linkProcessingMode: 'all'` (the default) and implement a catch-all route in your navigation stack. In this approach, `DetourProvider` processes all link types (deferred, Universal/App Links, and custom scheme). You consume the resolved link via `useDetourContext()` in a top-level component and navigate programmatically: ```ts const { isLinkProcessed, link, clearLink } = useDetourContext(); useEffect(() => { if (!isLinkProcessed || !link) return; router.replace({ pathname: link.pathname, params: link.params }); clearLink(); }, [isLinkProcessed, link]); ``` This was the primary integration pattern before the SDK introduced native intent support (pre-1.x). It still works but the `+native-intent.tsx` approach provides better UX by resolving links before the first screen renders. ## Analytics module [Section titled “Analytics module”](#analytics-module) `DetourAnalytics` emits events that are sent by the active `DetourProvider`. ```ts import { DetourAnalytics, DetourEventNames, } from "@swmansion/react-native-detour"; DetourAnalytics.logEvent(DetourEventNames.Purchase, { item: "subscription_premium", currency: "USD", }); DetourAnalytics.logRetention("home_screen_opened"); ``` ### Automatic events [Section titled “Automatic events”](#automatic-events) Besides the events you log manually, `DetourProvider` reports some events on its own, with no wiring needed: * `app_open` — a retention event logged once per session when the provider first mounts (cold start). * `opened_via_universal_link` — logged whenever the app is opened through a Universal Link (iOS) or App Link (Android). Custom scheme links and deferred links do not trigger this event. SDK version header Every request the SDK sends to the Detour backend carries an `X-SDK: react-native/` header. It is used for diagnostics and SDK-usage analysis and requires no configuration. ## Related pages [Section titled “Related pages”](#related-pages) * [Examples](/docs/sdk/react-native/2.1.x/examples) * [Migration to v2](/docs/sdk/react-native/2.1.x/migration-2-0-0) * [API Reference](/docs/sdk/react-native/2.1.x/api-reference) * [Custom Storage](/docs/sdk/react-native/2.1.x/custom-storage) * [Testing & Troubleshooting](/docs/sdk/react-native/2.1.x/testing) # Testing & Troubleshooting > Test deferred linking on iOS and Android and troubleshoot common Detour React Native SDK issues. Testing deferred linking has platform constraints. The complete deferred flow requires the store redirect chain, that is the App Store or Google Play. ## Testing iOS [Section titled “Testing iOS”](#testing-ios) If your app is not yet released: 1. Remove any existing install from the device so the next open counts as a first launch. 2. Open a Detour link in a mobile browser. 3. Expect the App Store redirect to fail. With no public listing yet, the store has no listing to open, which does not affect the test. 4. Install your development build. 5. Launch the app and confirm the link resolves within the matching window (currently 15 minutes). ## Testing Android [Section titled “Testing Android”](#testing-android) For non-production builds, the install referrer often does not work in store test tracks, so deterministic matching can be unavailable. You can still verify the probabilistic flow: 1. Uninstall the app from the device. 2. Open a Detour link in a mobile browser. 3. Expect the Play Store redirect to fail when the production listing is unavailable. 4. Install your development build. 5. Launch the app and confirm the link resolves within the matching window (currently 15 minutes). For local install referrer testing workarounds, see [How to test the Play Store Install Referrer API](https://medium.com/@madicdjordje/how-to-test-the-play-store-install-referrer-api-78a63d59945b). Missing `click_id` If you encounter click\_id missing in your build see limitations section [Play Store referrer missing](/docs/platform/architecture/architecture-limitations#play-store-referrer-missing) ## Testing Android App Links [Section titled “Testing Android App Links”](#testing-android-app-links) Whether an installed app opens a Detour link depends on the Dashboard SHA-256 matching the key that signed **the build on your device**: | Installation method | SHA-256 to configure | Where to find it | | :------------------------------------------------------ | :------------------------------------- | :------------------------------------------------------------------------------------------------------- | | Google Play, any track | Play App Signing key | Play Console → **Protected with Play** → **Manage Play app signing** → *App signing key certificate* | | `eas build`, any profile, including `development` | Keystore in your EAS account | `eas credentials -p android` → profile → **Keystore** → `SHA256 Fingerprint` | | `npx expo run:android` / `npx react-native run-android` | Project’s `android/app/debug.keystore` | `keytool -list -v -keystore android/app/debug.keystore -storepass android`, or `./gradlew signingReport` | To check any build directly: `apksigner verify --print-certs app-debug.apk`. The complete procedure is described in [Finding your Android SHA-256 fingerprint](/docs/platform/fundamentals/universal-app-links#finding-your-android-sha-256-fingerprint). ## Common issues [Section titled “Common issues”](#common-issues) * No match found (`404`) * click is older than matching window * click was not recorded in Dashboard logs * for deterministic Android flow: missing `click_id` in referrer * `429 Too Many Requests` * add retry/backoff and avoid heavy testing with one key across many devices * iOS clipboard prompt not appearing * check `shouldUseClipboard: true` * verify copy-link flow is enabled in Dashboard * iOS Universal Link opens the browser instead of the app during development * before your domain and App Store listing have fully propagated, iOS resolves the `apple-app-site-association` file through Apple’s CDN, which may not have it yet * append `?mode=developer` to the matching entry in your iOS `associatedDomains` (e.g. `applinks:yourorg.godetour.link?mode=developer`) so the device fetches the association file directly from your server, bypassing the CDN * this also requires enabling **Developer Mode** on the test device (Settings → Privacy & Security → Developer Mode). Remove the `?mode=developer` suffix before shipping * Link processed twice * confirm you call `clearLink()` after navigation * confirm exactly one `DetourProvider` instance is mounted * Link is not processed on Expo Web * web runtime is intentionally unsupported * provider on web returns a no-op context and skips SDK initialization ## Bare React Native CLI build issues [Section titled “Bare React Native CLI build issues”](#bare-react-native-cli-build-issues) These apply only to bare React Native CLI projects that added Expo Modules with `install-expo-modules` (see [Bare React Native projects](/docs/sdk/react-native/2.1.x/sdk-installation#bare-react-native-projects)). They are bundler/environment issues, not SDK bugs. The SDK itself is plain JavaScript. | Symptom | Cause | Fix | | :--------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | | iOS: `TypeError: Cannot read property '…' of undefined` at startup | Expo Modules native code is not linked | Run `npx install-expo-modules@latest`, then reinstall CocoaPods | | Android release: `Serializer did not return expected format … Unexpected token 'v', "var __BUND"... is not valid JSON` | `metro.config.js` uses `@react-native/metro-config` while the build bundles with Expo CLI (`export:embed`) | Make `metro.config.js` extend `expo/metro-config`. See [Expo Modules setup](/docs/sdk/react-native/2.1.x/sdk-installation#expo-modules-setup-react-native-cli) | | `The NODE_ENV environment variable is required but was not specified` | `export:embed` ran without an environment set | Set `NODE_ENV=production` for the release build | | `cliFile … is not a file … it was a directory` | `@expo/cli` is not hoisted to the top-level `node_modules` | Install it at the project root (`npm install @expo/cli@`) or do a clean reinstall | | Bundling fails because `node` is not found | `node` is not on the PATH of the Gradle process (common with `nvm`) | Make sure `node` is on PATH for the build and restart the Gradle daemon (`./gradlew --stop`) | For integration patterns that are easy to test end-to-end, use [Examples](/docs/sdk/react-native/2.1.x/examples). # API Reference > Complete API reference for the Detour React Native SDK. ## DetourProvider [Section titled “DetourProvider”](#detourprovider) Top-level React component that initializes the Detour SDK. It manages the full link processing lifecycle: deferred link matching, Universal/App Link listening, initial URL handling, and analytics event transport. On mount, the provider: 1. Checks for an initial URL that launched the app (Universal/App Link on cold start) 2. If no initial link is found, queries the Detour API for a deferred link (first install only) 3. Attaches a runtime listener for Universal/App Links received while the app is running 4. Subscribes to analytics events and forwards them to the Detour backend Use exactly one `DetourProvider` at the root of your app tree. Multiple providers are detected at runtime and will cause analytics events to be dropped. ### Signature [Section titled “Signature”](#signature) ```ts function DetourProvider(props: { config: Config; children: React.ReactNode; }): JSX.Element; ``` ### Web behavior [Section titled “Web behavior”](#web-behavior) On `Platform.OS === 'web'`, provider initialization is skipped. No link processing hooks run, no analytics listeners attach, and no storage is accessed. `DetourProvider` returns a no-op context value: * `isLinkProcessed: true` * `link: null` * `clearLink: () => {}` In development builds, the SDK logs a single warning: `[Detour:WEB_UNSUPPORTED]`. This is safe to use in universal code shared between web and native. ### Config fields [Section titled “Config fields”](#config-fields) * `appID` required * Type `string` * Unique app identifier from the Detour Dashboard. Used to authenticate API requests and route links to the correct app. * `apiKey` required * Type `string` * Publishable API key from the Detour Dashboard. Used as a Bearer token for all API calls (deferred link matching, short-link resolution, analytics) * `shouldUseClipboard` * Type `boolean` Default `true` * iOS only. When `true`, the SDK reads the device clipboard as part of probabilistic fingerprinting for deferred link matching. Set to `false` if your app lacks clipboard permissions or you want to skip clipboard access. * `linkProcessingMode` * Type [`LinkProcessingMode` ](#linkprocessingmode)Default `'all'` * Controls which link sources the SDK processes. See [LinkProcessingMode](#linkprocessingmode) for details on each value. * `storage` * Type [`DetourStorage` ](#detourstorage)Default `AsyncStorage` * Custom storage adapter for SDK persistence. The SDK stores a first-entrance flag (for deferred link matching) and a device ID (for analytics). Defaults to `@react-native-async-storage/async-storage` if installed. ## useDetourContext [Section titled “useDetourContext”](#usedetourcontext) Hook that returns the current link processing state and resolved link payload from the nearest `DetourProvider`. Throws an error if called outside of a `DetourProvider` tree. ### Signature [Section titled “Signature”](#signature-1) ```ts function useDetourContext(): DetourContextType; ``` ### Returned object [Section titled “Returned object”](#returned-object) * `isLinkProcessed` * Type `boolean` * `true` once the SDK has finished all link detection attempts, regardless of whether a link was found. Use this to conditionally show a loading/splash screen during cold start. * `link` * Type [`DetourLink`](#detourlink) * Resolved link data, or `null` if no link was detected. Persists until `clearLink()` is called. * `clearLink` * Type `() => void` * Resets `link` to `null`. Call after handling navigation to prevent repeated redirects. ## DetourAnalytics [Section titled “DetourAnalytics”](#detouranalytics) Analytics module exported from the package root. Requires an active `DetourProvider` in the component tree. Events are silently dropped if no provider is mounted. ### Methods [Section titled “Methods”](#methods) #### `logEvent` [Section titled “logEvent”](#logevent) ```ts DetourAnalytics.logEvent( eventName: DetourEventNames | `${DetourEventNames}`, data?: any, ): void; ``` Records a user action or custom event and sends it to the Detour backend. Pass one of the predefined [`DetourEventNames`](#detoureventnames) or a custom string. The optional `data` parameter accepts arbitrary metadata (e.g. `{ itemId: '123', price: 49.99 }`). #### `logRetention` [Section titled “logRetention”](#logretention) ```ts DetourAnalytics.logRetention(retentionEventName: string): void; ``` Records a retention/lifecycle event for user engagement tracking. The SDK automatically logs an `app_open` retention event when `DetourProvider` mounts for the first time in a session. ## Expo Router helper [Section titled “Expo Router helper”](#expo-router-helper) Available under subpath import: ```ts import { createDetourNativeIntentHandler } from '@swmansion/react-native-detour/expo-router'; ``` ### Signature [Section titled “Signature”](#signature-2) ```ts function createDetourNativeIntentHandler( options?: DetourNativeIntentOptions, ): DetourNativeIntentHandler; ``` ### Options [Section titled “Options”](#options) #### `fallbackPath` [Section titled “fallbackPath”](#fallbackpath) | | | | ----------- | -------- | | **Type** | `string` | | **Default** | `''` | The in-app route path returned when an incoming URL matches a Detour host but resolve mode is not enabled or resolution fails. Set this to the route you want users to land on as a safe default, for example `''` for the home screen or `'/welcome'` for an onboarding screen. #### `hosts` [Section titled “hosts”](#hosts) | | | | ----------- | ------------------------- | | **Type** | `Array` | | **Default** | `[/\.godetour\.link$/i]` | Host-matching patterns that determine which incoming URLs the handler should process. Only URLs whose hostname matches at least one entry are intercepted. All other URLs pass through unchanged to Expo Router’s default routing. Use plain strings for exact domain matches, strings beginning with `*.` for suffix wildcards (`*.example.com` matches any subdomain of `example.com`), or regular expressions for anything else. The default pattern matches any subdomain of `godetour.link` (e.g. `abc123.godetour.link`). `hosts` replaces the default The default value is `[/\.godetour\.link$/i]`. Passing `hosts` replaces that value, it does not add to it. An app that passes only its [custom domain](/docs/platform/advanced/custom-domain) stops matching `.godetour.link` links. Keep the default pattern in the list as well: if the custom domain is removed, or the paid plan lapses, Detour serves that app’s links from `.godetour.link` again, and a build that dropped the pattern ignores them. ```ts hosts: ["links.yourdomain.com", /\.godetour\.link$/i], ``` #### `config` [Section titled “config”](#config) | | | | ----------- | --------------------------------- | | **Type** | `DetourNativeIntentResolveConfig` | | **Default** | `undefined` | Providing this object **enables resolve mode**. In resolve mode the handler resolves Detour short links (e.g. `https://abc123.godetour.link/xyz`) to their full destination URLs before routing to the matched in-app screen. Without `config`, the handler operates in **intercept mode**, which matches Detour hosts and returns `fallbackPath` without resolving the link. * `apiKey` required * Type `string` * Publishable API key from the Detour Dashboard (same value used in `DetourProvider` config) * `appID` required * Type `string` * App identifier from the Detour Dashboard (same value used in `DetourProvider` config) * `timeoutMs` * Type `number` Default `1200` * Maximum time in milliseconds to wait for short-link resolution. If exceeded, `fallbackPath` is returned. * `onResolveError` * Type `(error, context) => void` * Callback invoked when resolution fails (e.g. network error, invalid link). Useful for logging or error reporting. DetourProvider configuration When using resolve mode, set `linkProcessingMode: 'deferred-only'` in your `DetourProvider` config so the provider only handles deferred links while `+native-intent.tsx` handles Universal Links and App Links. See [Configure DetourProvider for native intent](/docs/sdk/react-native/2.0.0/sdk-usage#configure-detourprovider-for-native-intent). #### `mapToRoute` [Section titled “mapToRoute”](#maptoroute) | | | | ----------- | ----------------------------------------------------------------------------------------- | | **Type** | `(value: DetourNativeIntentResolvedValue) => string` | | **Default** | Built-in mapper that strips the first path segment (app hash) from multi-segment web URLs | Custom function that transforms a resolved URL into an in-app route string. The function receives a `DetourNativeIntentResolvedValue` object containing the `originalPath`, `initial` flag, and the `resolvedUrl`. Use this when your Detour link destinations do not map directly to your app’s route structure, for example if your link URLs contain a prefix segment that does not correspond to an in-app screen. ## Types [Section titled “Types”](#types) ### LinkProcessingMode [Section titled “LinkProcessingMode”](#linkprocessingmode) ```ts type LinkProcessingMode = 'all' | 'web-only' | 'deferred-only'; ``` Controls which link sources `DetourProvider` processes: | Value | Behavior | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `'all'` (default) | Processes all link sources: deferred links from the Detour API, runtime Universal Links (iOS) / App Links (Android), the initial URL that launched the app, and custom scheme deep links (e.g. `myapp://path`) | | `'web-only'` | Same as `'all'` but filters out custom scheme links. Only HTTP/HTTPS URLs (Universal Links and App Links) and deferred links are processed | | `'deferred-only'` | Only processes deferred links obtained from the Detour API. Disables the runtime link listener and skips the initial URL check. Recommended when Expo Router’s `+native-intent.tsx` already handles runtime and initial URLs | ### LinkType [Section titled “LinkType”](#linktype) ```ts type LinkType = 'deferred' | 'verified' | 'scheme'; ``` Describes how the link arrived in the app: | Value | Meaning | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `'deferred'` | The link was obtained from the Detour deferred-link API. This happens when a user who does not have the app installed clicks a Detour link, installs the app, and opens it. The SDK retrieves the original link from the backend on first launch | | `'verified'` | The link arrived as a Universal Link (iOS) or App Link (Android), an HTTP/HTTPS URL that the OS verified belongs to your app and delivered directly to it | | `'scheme'` | The link arrived via a custom URL scheme (e.g. `myapp://path`). These are non-HTTP URLs that the OS routes to your app based on the registered scheme | ### DetourLink [Section titled “DetourLink”](#detourlink) ```ts type DetourLink = { url: string | URL; route: string; pathname: string; params: Record; type: LinkType; } | null; ``` Resolved link data, or `null` if no link was detected. * `url` * Type `string | URL` * Original URL as parsed by the `URL` constructor, or raw string if parsing failed. * `route` * Type `string` * Full in-app route including query string (e.g. `/details?utm=1`). For web URLs, the first path segment (app hash) is stripped. * `pathname` * Type `string` * Path without query string (e.g. `/details`). For web URLs, the first path segment (app hash) is stripped. * `params` * Type `Record` * Query string parameters as a key-value record. All values are strings. * `type` * Type [`LinkType`](#linktype) * How the link arrived in the app. ### DetourContextType [Section titled “DetourContextType”](#detourcontexttype) ```ts type DetourContextType = { isLinkProcessed: boolean; link: DetourLink; clearLink: () => void; }; ``` Shape of the value returned by [`useDetourContext`](#usedetourcontext). * `isLinkProcessed` * Type `boolean` * `true` once the SDK has finished all link detection attempts. `false` during cold start processing. * `link` * Type [`DetourLink`](#detourlink) * Resolved link data or `null`. Persists until `clearLink()` is called. * `clearLink` * Type `() => void` * Resets `link` to `null`. Call after navigating to prevent repeated redirects. ### DetourStorage [Section titled “DetourStorage”](#detourstorage) ```ts interface DetourStorage { getItem(key: string): Promise | string | null; setItem(key: string, value: string): Promise | void; removeItem?(key: string): Promise | void; } ``` Interface for custom storage adapters. The SDK persists a first-entrance flag (used for deferred link matching, which only runs on first app install) and a stable device ID (used for analytics). Methods can be synchronous or asynchronous. By default, `@react-native-async-storage/async-storage` is used. Provide a custom adapter if you use a different storage solution (e.g. MMKV, Expo SecureStore). See [Custom Storage](/docs/sdk/react-native/2.0.0/custom-storage) for examples. ### DetourNativeIntentArgs [Section titled “DetourNativeIntentArgs”](#detournativeintentargs) ```ts type DetourNativeIntentArgs = { path: string; initial: boolean; }; ``` Arguments passed by Expo Router to the `redirectSystemPath` handler. * `path` * Type `string` * Incoming system path or URL from Expo Router. * `initial` * Type `boolean` * `true` when this is the initial URL used to launch the app (cold start), `false` for runtime link events. ### DetourNativeIntentHandler [Section titled “DetourNativeIntentHandler”](#detournativeintenthandler) ```ts type DetourNativeIntentHandler = ( args: DetourNativeIntentArgs, ) => string | Promise; ``` Function signature returned by `createDetourNativeIntentHandler`. Takes the incoming path and returns the resolved in-app route (synchronously or as a promise). ### DetourNativeIntentResolveConfig [Section titled “DetourNativeIntentResolveConfig”](#detournativeintentresolveconfig) ```ts type DetourNativeIntentResolveConfig = { apiKey: string; appID: string; timeoutMs?: number; onResolveError?: (error: unknown, context: DetourNativeIntentArgs) => void; }; ``` Configuration that enables resolve mode in `createDetourNativeIntentHandler`. See [`config` option](#config) for field descriptions. ### DetourNativeIntentOptions [Section titled “DetourNativeIntentOptions”](#detournativeintentoptions) ```ts type DetourNativeIntentOptions = { fallbackPath?: string; hosts?: Array; config?: DetourNativeIntentResolveConfig; mapToRoute?: (value: DetourNativeIntentResolvedValue) => string; }; ``` Options for `createDetourNativeIntentHandler`. See [Options](#options) for detailed field descriptions. ### DetourNativeIntentResolvedValue [Section titled “DetourNativeIntentResolvedValue”](#detournativeintentresolvedvalue) ```ts type DetourNativeIntentResolvedValue = { originalPath: string; initial: boolean; resolvedUrl: URL; }; ``` Object passed to the `mapToRoute` callback after short-link resolution. * `originalPath` * Type `string` * The original incoming system path before resolution. * `initial` * Type `boolean` * `true` if this is the initial URL that launched the app. * `resolvedUrl` * Type `URL` * The resolved destination URL after short-link resolution. ### DetourEventNames [Section titled “DetourEventNames”](#detoureventnames) | Category | Event Name | String Value | | :---------- | :--------------------------- | :------------------------------ | | **General** | `Login` | `login` | | | `Search` | `search` | | | `Share` | `share` | | | `SignUp` | `sign_up` | | | `TutorialBegin` | `tutorial_begin` | | | `TutorialComplete` | `tutorial_complete` | | | `ReEngage` | `re_engage` | | | `Invite` | `invite` | | | `OpenedFromPushNotification` | `opened_from_push_notification` | | **Sales** | `AddPaymentInfo` | `add_payment_info` | | | `AddShippingInfo` | `add_shipping_info` | | | `AddToCart` | `add_to_cart` | | | `RemoveFromCart` | `remove_from_cart` | | | `Refund` | `refund` | | | `ViewItem` | `view_item` | | | `BeginCheckout` | `begin_checkout` | | | `Purchase` | `purchase` | | | `AdImpression` | `ad_impression` | # Custom Storage > Provide a custom storage adapter (MMKV, Expo Secure Store, or your own) for the Detour React Native SDK. By default, the SDK uses `@react-native-async-storage/async-storage` for internal persistence keys. If you prefer another storage engine, pass a custom adapter in `config.storage`. ## Storage interface [Section titled “Storage interface”](#storage-interface) ```ts interface DetourStorage { getItem(key: string): Promise | string | null; setItem(key: string, value: string): Promise | void; removeItem?(key: string): Promise | void; } ``` ## Examples [Section titled “Examples”](#examples) The two adapters below cover the storage engines React Native apps use most often. Any object that matches the `DetourStorage` interface works, so use these as starting points for other engines. ### React Native MMKV [Section titled “React Native MMKV”](#react-native-mmkv) ```ts import { createMMKV } from 'react-native-mmkv'; import { DetourProvider } from '@swmansion/react-native-detour'; const storage = createMMKV(); const mmkvAdapter = { getItem: (key: string) => storage.getString(key) ?? null, setItem: (key: string, value: string) => storage.set(key, value), }; export default function App() { return ( {/* Your app */} ); } ``` ### Expo Secure Store [Section titled “Expo Secure Store”](#expo-secure-store) ```ts import * as SecureStore from 'expo-secure-store'; import { DetourProvider } from '@swmansion/react-native-detour'; const secureStoreAdapter = { getItem: SecureStore.getItemAsync, setItem: SecureStore.setItemAsync, }; export default function App() { return ( {/* Your app */} ); } ``` ## Related pages [Section titled “Related pages”](#related-pages) * [Usage](/docs/sdk/react-native/2.0.0/sdk-usage) * [API Reference](/docs/sdk/react-native/2.0.0/api-reference) # Examples > Runnable Detour React Native examples for Expo Router and React Navigation. The RN SDK repository ships six runnable apps, one per integration style. Clone the one closest to your setup, run it, and copy what you need. Each card below links to the example’s `README.md` on GitHub, with the project structure, setup steps, and full source. ## Example catalog [Section titled “Example catalog”](#example-catalog) [expo-bare ](https://github.com/software-mansion-labs/react-native-detour/blob/v2.0.0/examples/expo-bare/README.md)The SDK on its own, without a router. Use it to check that your configuration works. [expo-router ](https://github.com/software-mansion-labs/react-native-detour/blob/v2.0.0/examples/expo-router/README.md)The smallest Expo Router setup. The app opens the link's screen as soon as a link arrives. [expo-router-native-intent ](https://github.com/software-mansion-labs/react-native-detour/blob/v2.0.0/examples/expo-router-native-intent/README.md)Expo Router resolves short links in its native intent handler, so the app opens the final screen directly. [expo-router-advanced ](https://github.com/software-mansion-labs/react-native-detour/blob/v2.0.0/examples/expo-router-advanced/README.md)Expo Router with sign-in. A link that arrives while the user is signed out opens after they sign in. [react-navigation ](https://github.com/software-mansion-labs/react-native-detour/blob/v2.0.0/examples/react-navigation/README.md)The smallest React Navigation setup. The app maps the link's path to a screen. [react-navigation-advanced ](https://github.com/software-mansion-labs/react-native-detour/blob/v2.0.0/examples/react-navigation-advanced/README.md)React Navigation with sign-in. Links to protected screens open after the user signs in. ## Recommended user path [Section titled “Recommended user path”](#recommended-user-path) 1. Complete [Installation](/docs/sdk/react-native/2.0.0/sdk-installation). 2. Read [Usage](/docs/sdk/react-native/2.0.0/sdk-usage) for API model. 3. Start from the closest example and adapt route mapping/auth flow. 4. Validate behavior with [Testing & Troubleshooting](/docs/sdk/react-native/2.0.0/testing). # Migration to v2 > Migrate to @swmansion/react-native-detour@2.0.0 from 0.x or 1.0.1. This guide covers both migration paths to `@swmansion/react-native-detour@2.0.0`. Choose the right migration path * If your app is still on `0.x`, start from [From 0.x](#from-0x). * If your app is already on `1.0.1`, start from [From 1.0.1](#from-101). ## From 0.x [Section titled “From 0.x”](#from-0x) ### Migration checklist (`0.x` -> `2.0.0`) [Section titled “Migration checklist (0.x -> 2.0.0)”](#migration-checklist-0x---200) | Area | In `0.x` | In `2.0.0` | | ------------------------------- | ------------------------------------------- | ------------------------------------------------------------ | | Config key | Legacy `API_KEY` form in older integrations | `apiKey` | | Link data in context | `linkRoute` / legacy fields | `link` object (`route`, `pathname`, `params`, `url`, `type`) | | Link-source control | No explicit mode in older flows | `linkProcessingMode: 'all' \| 'web-only' \| 'deferred-only'` | | Device fingerprint dependencies | `react-native-device-info` | `expo-constants` + `expo-device` | | Expo Web behavior | Not explicitly guarded | `DetourProvider` disabled on web (no-op context) | ### 1. Update dependencies [Section titled “1. Update dependencies”](#1-update-dependencies) Remove old dependency: ```bash npm uninstall react-native-device-info ``` Install current peer dependencies: ```bash npm install expo-localization expo-clipboard expo-constants expo-device @react-native-async-storage/async-storage expo-application ``` If you use Yarn/PNPM/Bun, use equivalent commands from [Installation](/docs/sdk/react-native/2.0.0/sdk-installation). ### 2. Update config shape [Section titled “2. Update config shape”](#2-update-config-shape) ```ts // before (legacy) const config = { API_KEY: process.env.EXPO_PUBLIC_DETOUR_API_KEY, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID, }; // after const config = { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID, linkProcessingMode: 'all', }; ``` ### 3. Update context usage [Section titled “3. Update context usage”](#3-update-context-usage) ```ts // before (legacy route string) const { isLinkProcessed, linkRoute, clearLink } = useDetourContext(); if (isLinkProcessed && linkRoute) { router.replace(linkRoute); clearLink(); } // after const { isLinkProcessed, link, clearLink } = useDetourContext(); if (isLinkProcessed && link) { router.replace({ pathname: link.pathname, params: link.params }); clearLink(); } ``` ### 4. Choose `linkProcessingMode` [Section titled “4. Choose linkProcessingMode”](#4-choose-linkprocessingmode) * `'all'` (default) — deferred + Universal/App + scheme links * `'web-only'` — deferred + Universal/App links, no scheme links * `'deferred-only'` — deferred only (recommended if Expo Router `+native-intent.tsx` handles runtime links) ## From 1.0.1 [Section titled “From 1.0.1”](#from-101) ### Migration checklist (`1.0.1` -> `2.0.0`) [Section titled “Migration checklist (1.0.1 -> 2.0.0)”](#migration-checklist-101---200) | Area | In `1.0.1` | In `2.0.0` | | ------------------------------- | ----------------------------------- | --------------------------------------------------------------------- | | Device fingerprint dependencies | `react-native-device-info` | `expo-constants` + `expo-device` | | Peer dependency install command | Included `react-native-device-info` | Uses Expo modules only (no `react-native-device-info`) | | Expo Web behavior | Not explicitly guarded | `DetourProvider` is disabled on web (no-op context + one dev warning) | ### Update dependencies [Section titled “Update dependencies”](#update-dependencies) 1. Remove old dependency: ```bash npm uninstall react-native-device-info ``` 2. Install current peer dependencies: ```bash npm install expo-localization expo-clipboard expo-constants expo-device @react-native-async-storage/async-storage expo-application ``` If you use Yarn/PNPM/Bun, use equivalent commands from [Installation](/docs/sdk/react-native/2.0.0/sdk-installation). ### Web behavior update [Section titled “Web behavior update”](#web-behavior-update) In `2.0.0`, `DetourProvider` does not initialize on Expo Web. Returned context on web: * `isLinkProcessed: true` * `link: null` * `clearLink: () => {}` This prevents partial/unsupported initialization on web while keeping app tree stable. ## Shared validation checklist [Section titled “Shared validation checklist”](#shared-validation-checklist) 1. iOS/Android deferred flow still resolves route and parameters. 2. Runtime Universal/App Links still resolve according to `linkProcessingMode`. 3. Expo Web no longer attempts Detour processing and app still renders without provider runtime errors. 4. Exactly one `DetourProvider` is mounted in app root. ## Related pages [Section titled “Related pages”](#related-pages) * [Installation](/docs/sdk/react-native/2.0.0/sdk-installation) * [Usage](/docs/sdk/react-native/2.0.0/sdk-usage) * [API Reference](/docs/sdk/react-native/2.0.0/api-reference) * [Examples](/docs/sdk/react-native/2.0.0/examples) # Installation > Install the Detour React Native SDK in Expo or bare React Native projects. Detour provides a React Native SDK (`@swmansion/react-native-detour`) compatible with Expo and bare React Native projects. Before installation, make sure your app is created in the Detour Dashboard and you have: * `appID` from **API configuration** * publishable `apiKey` from **API configuration** * platform integration snippets from **App configuration** ## Install package [Section titled “Install package”](#install-package) * npm ```bash npm install @swmansion/react-native-detour ``` * yarn ```bash yarn add @swmansion/react-native-detour ``` * pnpm ```bash pnpm add @swmansion/react-native-detour ``` * bun ```bash bun add @swmansion/react-native-detour ``` ## Install peer dependencies (Expo projects) [Section titled “Install peer dependencies (Expo projects)”](#install-peer-dependencies-expo-projects) For Expo projects (including prebuild), install peer dependencies with Expo CLI so versions are matched to your Expo SDK: ```bash npx expo install expo-application expo-clipboard expo-constants expo-device expo-localization @react-native-async-storage/async-storage ``` ## Bare React Native projects [Section titled “Bare React Native projects”](#bare-react-native-projects) If you are not using Expo CLI, install peer dependencies with your package manager and pin versions compatible with your React Native/Expo Modules setup. * npm ```bash npm install expo-application expo-clipboard expo-constants expo-device expo-localization @react-native-async-storage/async-storage ``` * yarn ```bash yarn add expo-application expo-clipboard expo-constants expo-device expo-localization @react-native-async-storage/async-storage ``` * pnpm ```bash pnpm add expo-application expo-clipboard expo-constants expo-device expo-localization @react-native-async-storage/async-storage ``` * bun ```bash bun add expo-application expo-clipboard expo-constants expo-device expo-localization @react-native-async-storage/async-storage ``` Device info provider `expo-device` is a required peer dependency. Unlike storage, the device info source is not configurable. The SDK imports it directly to read the device model, manufacturer, and OS version for the deferred-link fingerprint. There is no adapter to plug in, and `react-native-device-info` is not accepted as an alternative until `2.3.0`. Custom storage `@react-native-async-storage/async-storage` is optional if you provide a custom storage adapter in config. See [Custom Storage](/docs/sdk/react-native/2.0.0/custom-storage). ### Expo Modules setup (React Native CLI) [Section titled “Expo Modules setup (React Native CLI)”](#expo-modules-setup-react-native-cli) The peer dependencies above (`expo-application`, `expo-clipboard`, `expo-constants`, `expo-localization`) are [Expo Modules](https://docs.expo.dev/bare/installing-expo-modules/), and they ship native code that has to be autolinked into your `android/` and `ios/` projects. In an Expo project this happens automatically. In a bare React Native CLI project you add Expo Modules once: ```bash npx install-expo-modules@latest ``` This sets up Expo Modules autolinking (and on iOS raises the deployment target to the Expo Modules minimum). `install-expo-modules` also switches the native JS bundling step to Expo CLI (`export:embed`), which requires `metro.config.js` to extend `expo/metro-config` (a superset of `@react-native/metro-config`, safe in bare projects). The tool usually migrates this for you. Verify it, especially if you had a customized `metro.config.js`: metro.config.js ```js const { getDefaultConfig } = require("expo/metro-config"); const { mergeConfig } = require("@react-native/metro-config"); module.exports = mergeConfig(getDefaultConfig(__dirname), {}); ``` Then align the Expo package versions to your setup: ```bash npx expo install --fix ``` Build errors after adding Expo Modules If the app throws at startup or a release build fails after adding Expo Modules, see [bare React Native CLI build issues](/docs/sdk/react-native/2.0.0/testing#bare-react-native-cli-build-issues). ## Credentials in app config [Section titled “Credentials in app config”](#credentials-in-app-config) ```ts import { type Config } from '@swmansion/react-native-detour'; export const detourConfig: Config = { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, shouldUseClipboard: true, }; ``` Keep `apiKey` and `appID` in environment variables or another secure configuration layer. ## Next steps [Section titled “Next steps”](#next-steps) 1. Implement the provider and link handling in [Usage](/docs/sdk/react-native/2.0.0/sdk-usage). 2. Pick a runnable app pattern in [Examples](/docs/sdk/react-native/2.0.0/examples). 3. If upgrading existing code, follow [Migration to v2](/docs/sdk/react-native/2.0.0/migration-2-0-0) and start from [From 0.x](/docs/sdk/react-native/2.0.0/migration-2-0-0#from-0x) or [From 1.0.1](/docs/sdk/react-native/2.0.0/migration-2-0-0#from-101). # Usage > Initialize DetourProvider, handle resolved links, and integrate Detour with Expo Router or React Navigation. ## Initialize provider in app root [Section titled “Initialize provider in app root”](#initialize-provider-in-app-root) ```ts import { DetourProvider, type Config } from '@swmansion/react-native-detour'; const config: Config = { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, shouldUseClipboard: true, // linkProcessingMode: 'all' // default }; export default function RootLayout() { return ( ); } ``` One DetourProvider per app Use exactly one `DetourProvider` in your app tree. On Expo Web, `DetourProvider` is disabled: it returns a no-op context (`isLinkProcessed: true`, `link: null`) and does not process links. If you want complete, runnable integrations for each navigation style, go to [Examples](/docs/sdk/react-native/2.0.0/examples). ## Handle resolved links with `useDetourContext()` [Section titled “Handle resolved links with useDetourContext()”](#handle-resolved-links-with-usedetourcontext) ```ts import { useDetourContext } from '@swmansion/react-native-detour'; import { useEffect } from 'react'; import { useRouter } from 'expo-router'; export function RootNavigator() { const { isLinkProcessed, link, clearLink } = useDetourContext(); const router = useRouter(); useEffect(() => { if (!isLinkProcessed || !link) return; router.replace({ pathname: link.pathname, params: link.params, }); clearLink(); }, [clearLink, isLinkProcessed, link, router]); if (!isLinkProcessed) { return null; } return ; } ``` ### `link` structure [Section titled “link structure”](#link-structure) `useDetourContext()` returns a `link` object (or `null`): * `link.route` Full in-app route with query string (e.g. `/details?utm=1`). * `link.pathname` Path without query (e.g. `/details`). * `link.params` Parsed query params record. * `link.url` Original URL or raw value. * `link.type` Type `'deferred' | 'verified' | 'scheme'` Use `clearLink()` after handling navigation to avoid repeated redirects. ## Choose `linkProcessingMode` [Section titled “Choose linkProcessingMode”](#choose-linkprocessingmode) `Config.linkProcessingMode` controls which link sources are handled by the SDK: | Value | Deferred | Universal / App | Custom scheme | | ----------------- | -------- | --------------- | ------------- | | `'all'` (default) | Yes | Yes | Yes | | `'web-only'` | Yes | Yes | No | | `'deferred-only'` | Yes | No | No | Use `'deferred-only'` when Expo Router `+native-intent.tsx` already handles runtime and initial URLs. ## Expo Router integration [Section titled “Expo Router integration”](#expo-router-integration) Recommended for Expo apps If you use Expo Router, we **strongly recommend** using our dedicated `createDetourNativeIntentHandler` API in a [`+native-intent.tsx`](https://docs.expo.dev/router/advanced/native-intent/) file. This intercepts URLs at the router level before any screen renders, avoiding flash-of-wrong-screen issues and providing the smoothest deep-linking experience. Expo Router’s `+native-intent.tsx` file lets you intercept and transform incoming system URLs before the router processes them. The Detour SDK provides `createDetourNativeIntentHandler`, a factory that creates a handler pre-configured for Detour link resolution. ### Basic setup [Section titled “Basic setup”](#basic-setup) Create a `+native-intent.tsx` file in your app directory (e.g. `src/app/+native-intent.tsx`): ```ts import { createDetourNativeIntentHandler } from "@swmansion/react-native-detour/expo-router"; export const redirectSystemPath = createDetourNativeIntentHandler(); ``` This is intercept mode Any URL matching a Detour host is caught and the user is routed to the home screen (default `fallbackPath`). Deferred links are still handled separately by `DetourProvider`. ### Resolve mode [Section titled “Resolve mode”](#resolve-mode) To resolve Detour short links to their full destination URLs at the router level, pass a `config` object: ```ts import { createDetourNativeIntentHandler } from "@swmansion/react-native-detour/expo-router"; export const redirectSystemPath = createDetourNativeIntentHandler({ config: { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, }, }); ``` In resolve mode, when a user taps a Detour short link (e.g. `https://abc123.godetour.link/xyz`), the handler resolves it to the full destination URL and routes directly to the correct screen, all before the first render. See [Expo Router helper](/docs/sdk/react-native/2.0.0/api-reference#expo-router-helper) for the full list of options (custom hosts, fallback path, timeout, route mapping, etc.). ### Configure `DetourProvider` for native intent [Section titled “Configure DetourProvider for native intent”](#configure-detourprovider-for-native-intent) When using `+native-intent.tsx` to handle Universal Links and App Links, configure the provider to only process deferred links: ```ts const config: Config = { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, linkProcessingMode: "deferred-only", }; ``` This avoids double-processing: `+native-intent.tsx` handles runtime and initial URLs, while `DetourProvider` handles deferred deep links only. ### Advanced examples [Section titled “Advanced examples”](#advanced-examples) The SDK repository includes examples for common native intent scenarios: * **`expo-router-native-intent`** — Full resolve mode setup with a custom `mapToRoute` function and scheme URL handling. Use this as a starting point when you need to transform resolved URLs before routing (e.g. stripping path prefixes) or when you want to handle custom scheme links alongside Detour links. * **`expo-router-advanced`** — Demonstrates Detour inside an auth- and onboarding-gated flow. Deferred and runtime links are preserved through sign-in and one-time onboarding before navigating to their destination. Use this when your app requires the user to be signed in (and optionally onboarded) before a deep link should take effect. For all runnable examples, see [Examples](/docs/sdk/react-native/2.0.0/examples). ### Alternative: catch-all route without `+native-intent.tsx` [Section titled “Alternative: catch-all route without +native-intent.tsx”](#alternative-catch-all-route-without-native-intenttsx) If you prefer not to use `+native-intent.tsx`, you can handle all incoming links through `DetourProvider` with `linkProcessingMode: 'all'` (the default) and implement a catch-all route in your navigation stack. In this approach, `DetourProvider` processes all link types (deferred, Universal/App Links, and custom scheme). You consume the resolved link via `useDetourContext()` in a top-level component and navigate programmatically: ```ts const { isLinkProcessed, link, clearLink } = useDetourContext(); useEffect(() => { if (!isLinkProcessed || !link) return; router.replace({ pathname: link.pathname, params: link.params }); clearLink(); }, [isLinkProcessed, link]); ``` This was the primary integration pattern before the SDK introduced native intent support (pre-1.x). It still works but the `+native-intent.tsx` approach provides better UX by resolving links before the first screen renders. ## Analytics module [Section titled “Analytics module”](#analytics-module) `DetourAnalytics` emits events that are sent by the active `DetourProvider`. ```ts import { DetourAnalytics, DetourEventNames, } from "@swmansion/react-native-detour"; DetourAnalytics.logEvent(DetourEventNames.Purchase, { item: "subscription_premium", currency: "USD", }); DetourAnalytics.logRetention("home_screen_opened"); ``` ## Related pages [Section titled “Related pages”](#related-pages) * [Examples](/docs/sdk/react-native/2.0.0/examples) * [Migration to v2](/docs/sdk/react-native/2.0.0/migration-2-0-0) * [API Reference](/docs/sdk/react-native/2.0.0/api-reference) * [Custom Storage](/docs/sdk/react-native/2.0.0/custom-storage) * [Testing & Troubleshooting](/docs/sdk/react-native/2.0.0/testing) # Testing & Troubleshooting > Test deferred linking on iOS and Android and troubleshoot common Detour React Native SDK issues. Testing deferred linking has platform constraints. The complete deferred flow requires the store redirect chain, that is the App Store or Google Play. ## Testing iOS [Section titled “Testing iOS”](#testing-ios) If your app is not yet released: 1. Remove any existing install from the device so the next open counts as a first launch. 2. Open a Detour link in a mobile browser. 3. Expect the App Store redirect to fail. With no public listing yet, the store has no listing to open, which does not affect the test. 4. Install your development build. 5. Launch the app and confirm the link resolves within the matching window (currently 15 minutes). ## Testing Android [Section titled “Testing Android”](#testing-android) For non-production builds, the install referrer often does not work in store test tracks, so deterministic matching can be unavailable. You can still verify the probabilistic flow: 1. Uninstall the app from the device. 2. Open a Detour link in a mobile browser. 3. Expect the Play Store redirect to fail when the production listing is unavailable. 4. Install your development build. 5. Launch the app and confirm the link resolves within the matching window (currently 15 minutes). For local install referrer testing workarounds, see [How to test the Play Store Install Referrer API](https://medium.com/@madicdjordje/how-to-test-the-play-store-install-referrer-api-78a63d59945b). Missing `click_id` If you encounter click\_id missing in your build see limitations section [Play Store referrer missing](/docs/platform/architecture/architecture-limitations#play-store-referrer-missing) ## Testing Android App Links [Section titled “Testing Android App Links”](#testing-android-app-links) Whether an installed app opens a Detour link depends on the Dashboard SHA-256 matching the key that signed **the build on your device**: | Installation method | SHA-256 to configure | Where to find it | | :------------------------------------------------------ | :------------------------------------- | :------------------------------------------------------------------------------------------------------- | | Google Play, any track | Play App Signing key | Play Console → **Protected with Play** → **Manage Play app signing** → *App signing key certificate* | | `eas build`, any profile, including `development` | Keystore in your EAS account | `eas credentials -p android` → profile → **Keystore** → `SHA256 Fingerprint` | | `npx expo run:android` / `npx react-native run-android` | Project’s `android/app/debug.keystore` | `keytool -list -v -keystore android/app/debug.keystore -storepass android`, or `./gradlew signingReport` | To check any build directly: `apksigner verify --print-certs app-debug.apk`. The complete procedure is described in [Finding your Android SHA-256 fingerprint](/docs/platform/fundamentals/universal-app-links#finding-your-android-sha-256-fingerprint). ## Common issues [Section titled “Common issues”](#common-issues) * No match found (`404`) * click is older than matching window * click was not recorded in Dashboard logs * for deterministic Android flow: missing `click_id` in referrer * `429 Too Many Requests` * add retry/backoff and avoid heavy testing with one key across many devices * iOS clipboard prompt not appearing * check `shouldUseClipboard: true` * verify copy-link flow is enabled in Dashboard * iOS Universal Link opens the browser instead of the app during development * before your domain and App Store listing have fully propagated, iOS resolves the `apple-app-site-association` file through Apple’s CDN, which may not have it yet * append `?mode=developer` to the matching entry in your iOS `associatedDomains` (e.g. `applinks:yourorg.godetour.link?mode=developer`) so the device fetches the association file directly from your server, bypassing the CDN * this also requires enabling **Developer Mode** on the test device (Settings → Privacy & Security → Developer Mode). Remove the `?mode=developer` suffix before shipping * Link processed twice * confirm you call `clearLink()` after navigation * confirm exactly one `DetourProvider` instance is mounted * Link is not processed on Expo Web * web runtime is intentionally unsupported * provider on web returns a no-op context and skips SDK initialization ## Bare React Native CLI build issues [Section titled “Bare React Native CLI build issues”](#bare-react-native-cli-build-issues) These apply only to bare React Native CLI projects that added Expo Modules with `install-expo-modules` (see [Bare React Native projects](/docs/sdk/react-native/2.0.0/sdk-installation#bare-react-native-projects)). They are bundler/environment issues, not SDK bugs. The SDK itself is plain JavaScript. | Symptom | Cause | Fix | | :--------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | | iOS: `TypeError: Cannot read property '…' of undefined` at startup | Expo Modules native code is not linked | Run `npx install-expo-modules@latest`, then reinstall CocoaPods | | Android release: `Serializer did not return expected format … Unexpected token 'v', "var __BUND"... is not valid JSON` | `metro.config.js` uses `@react-native/metro-config` while the build bundles with Expo CLI (`export:embed`) | Make `metro.config.js` extend `expo/metro-config`. See [Expo Modules setup](/docs/sdk/react-native/2.0.0/sdk-installation#expo-modules-setup-react-native-cli) | | `The NODE_ENV environment variable is required but was not specified` | `export:embed` ran without an environment set | Set `NODE_ENV=production` for the release build | | `cliFile … is not a file … it was a directory` | `@expo/cli` is not hoisted to the top-level `node_modules` | Install it at the project root (`npm install @expo/cli@`) or do a clean reinstall | | Bundling fails because `node` is not found | `node` is not on the PATH of the Gradle process (common with `nvm`) | Make sure `node` is on PATH for the build and restart the Gradle daemon (`./gradlew --stop`) | For integration patterns that are easy to test end-to-end, use [Examples](/docs/sdk/react-native/2.0.0/examples). # API Reference > Complete API reference for the Detour React Native SDK. ## DetourProvider [Section titled “DetourProvider”](#detourprovider) Top-level React component that initializes the Detour SDK. It manages the full link processing lifecycle: deferred link matching, Universal/App Link listening, initial URL handling, and analytics event transport. On mount, the provider: 1. Checks for an initial URL that launched the app (Universal/App Link on cold start) 2. If no initial link is found, queries the Detour API for a deferred link (first install only) 3. Attaches a runtime listener for Universal/App Links received while the app is running 4. Subscribes to analytics events and forwards them to the Detour backend Use exactly one `DetourProvider` at the root of your app tree. Multiple providers are detected at runtime and will cause analytics events to be dropped. ### Signature [Section titled “Signature”](#signature) ```ts function DetourProvider(props: { config: Config; children: React.ReactNode; }): JSX.Element; ``` ### Config fields [Section titled “Config fields”](#config-fields) * `appID` required * Type `string` * Unique app identifier from the Detour Dashboard. Used to authenticate API requests and route links to the correct app. * `apiKey` required * Type `string` * Publishable API key from the Detour Dashboard. Used as a Bearer token for all API calls (deferred link matching, short-link resolution, analytics) * `shouldUseClipboard` * Type `boolean` Default `true` * iOS only. When `true`, the SDK reads the device clipboard as part of probabilistic fingerprinting for deferred link matching. Set to `false` if your app lacks clipboard permissions or you want to skip clipboard access. * `linkProcessingMode` * Type [`LinkProcessingMode` ](#linkprocessingmode)Default `'all'` * Controls which link sources the SDK processes. See [LinkProcessingMode](#linkprocessingmode) for details on each value. * `storage` * Type [`DetourStorage` ](#detourstorage)Default `AsyncStorage` * Custom storage adapter for SDK persistence. The SDK stores a first-entrance flag (for deferred link matching) and a device ID (for analytics). Defaults to `@react-native-async-storage/async-storage` if installed. ## useDetourContext [Section titled “useDetourContext”](#usedetourcontext) Hook that returns the current link processing state and resolved link payload from the nearest `DetourProvider`. Throws an error if called outside of a `DetourProvider` tree. ### Signature [Section titled “Signature”](#signature-1) ```ts function useDetourContext(): DetourContextType; ``` ### Returned object [Section titled “Returned object”](#returned-object) * `isLinkProcessed` * Type `boolean` * `true` once the SDK has finished all link detection attempts, regardless of whether a link was found. Use this to conditionally show a loading/splash screen during cold start. * `link` * Type [`DetourLink`](#detourlink) * Resolved link data, or `null` if no link was detected. Persists until `clearLink()` is called. * `clearLink` * Type `() => void` * Resets `link` to `null`. Call after handling navigation to prevent repeated redirects. ## DetourAnalytics [Section titled “DetourAnalytics”](#detouranalytics) Analytics module exported from the package root. Requires an active `DetourProvider` in the component tree. Events are silently dropped if no provider is mounted. ### Methods [Section titled “Methods”](#methods) #### `logEvent` [Section titled “logEvent”](#logevent) ```ts DetourAnalytics.logEvent( eventName: DetourEventNames | `${DetourEventNames}`, data?: any, ): void; ``` Records a user action or custom event and sends it to the Detour backend. Pass one of the predefined [`DetourEventNames`](#detoureventnames) or a custom string. The optional `data` parameter accepts arbitrary metadata (e.g. `{ itemId: '123', price: 49.99 }`). #### `logRetention` [Section titled “logRetention”](#logretention) ```ts DetourAnalytics.logRetention(retentionEventName: string): void; ``` Records a retention/lifecycle event for user engagement tracking. The SDK automatically logs an `app_open` retention event when `DetourProvider` mounts for the first time in a session. ## Expo Router helper [Section titled “Expo Router helper”](#expo-router-helper) Available under subpath import: ```ts import { createDetourNativeIntentHandler } from '@swmansion/react-native-detour/expo-router'; ``` ### Signature [Section titled “Signature”](#signature-2) ```ts function createDetourNativeIntentHandler( options?: DetourNativeIntentOptions, ): DetourNativeIntentHandler; ``` ### Options [Section titled “Options”](#options) #### `fallbackPath` [Section titled “fallbackPath”](#fallbackpath) | | | | ----------- | -------- | | **Type** | `string` | | **Default** | `''` | The in-app route path returned when an incoming URL matches a Detour host but resolve mode is not enabled or resolution fails. Set this to the route you want users to land on as a safe default, for example `''` for the home screen or `'/welcome'` for an onboarding screen. #### `hosts` [Section titled “hosts”](#hosts) | | | | ----------- | ------------------------- | | **Type** | `Array` | | **Default** | `[/\.godetour\.link$/i]` | Host-matching patterns that determine which incoming URLs the handler should process. Only URLs whose hostname matches at least one entry are intercepted. All other URLs pass through unchanged to Expo Router’s default routing. Use plain strings for exact domain matches, strings beginning with `*.` for suffix wildcards (`*.example.com` matches any subdomain of `example.com`), or regular expressions for anything else. The default pattern matches any subdomain of `godetour.link` (e.g. `abc123.godetour.link`). `hosts` replaces the default The default value is `[/\.godetour\.link$/i]`. Passing `hosts` replaces that value, it does not add to it. An app that passes only its [custom domain](/docs/platform/advanced/custom-domain) stops matching `.godetour.link` links. Keep the default pattern in the list as well: if the custom domain is removed, or the paid plan lapses, Detour serves that app’s links from `.godetour.link` again, and a build that dropped the pattern ignores them. ```ts hosts: ["links.yourdomain.com", /\.godetour\.link$/i], ``` #### `config` [Section titled “config”](#config) | | | | ----------- | --------------------------------- | | **Type** | `DetourNativeIntentResolveConfig` | | **Default** | `undefined` | Providing this object **enables resolve mode**. In resolve mode the handler resolves Detour short links (e.g. `https://abc123.godetour.link/xyz`) to their full destination URLs before routing to the matched in-app screen. Without `config`, the handler operates in **intercept mode**, which matches Detour hosts and returns `fallbackPath` without resolving the link. * `apiKey` required * Type `string` * Publishable API key from the Detour Dashboard (same value used in `DetourProvider` config) * `appID` required * Type `string` * App identifier from the Detour Dashboard (same value used in `DetourProvider` config) * `timeoutMs` * Type `number` Default `1200` * Maximum time in milliseconds to wait for short-link resolution. If exceeded, `fallbackPath` is returned. * `onResolveError` * Type `(error, context) => void` * Callback invoked when resolution fails (e.g. network error, invalid link). Useful for logging or error reporting. DetourProvider configuration When using resolve mode, set `linkProcessingMode: 'deferred-only'` in your `DetourProvider` config so the provider only handles deferred links while `+native-intent.tsx` handles Universal Links and App Links. See [Configure DetourProvider for native intent](/docs/sdk/react-native/1.0.1/sdk-usage#configure-detourprovider-for-native-intent). #### `mapToRoute` [Section titled “mapToRoute”](#maptoroute) | | | | ----------- | ----------------------------------------------------------------------------------------- | | **Type** | `(value: DetourNativeIntentResolvedValue) => string` | | **Default** | Built-in mapper that strips the first path segment (app hash) from multi-segment web URLs | Custom function that transforms a resolved URL into an in-app route string. The function receives a `DetourNativeIntentResolvedValue` object containing the `originalPath`, `initial` flag, and the `resolvedUrl`. Use this when your Detour link destinations do not map directly to your app’s route structure, for example if your link URLs contain a prefix segment that does not correspond to an in-app screen. ## Types [Section titled “Types”](#types) ### LinkProcessingMode [Section titled “LinkProcessingMode”](#linkprocessingmode) ```ts type LinkProcessingMode = 'all' | 'web-only' | 'deferred-only'; ``` Controls which link sources `DetourProvider` processes: | Value | Behavior | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `'all'` (default) | Processes all link sources: deferred links from the Detour API, runtime Universal Links (iOS) / App Links (Android), the initial URL that launched the app, and custom scheme deep links (e.g. `myapp://path`) | | `'web-only'` | Same as `'all'` but filters out custom scheme links. Only HTTP/HTTPS URLs (Universal Links and App Links) and deferred links are processed | | `'deferred-only'` | Only processes deferred links obtained from the Detour API. Disables the runtime link listener and skips the initial URL check. Recommended when Expo Router’s `+native-intent.tsx` already handles runtime and initial URLs | ### LinkType [Section titled “LinkType”](#linktype) ```ts type LinkType = 'deferred' | 'verified' | 'scheme'; ``` Describes how the link arrived in the app: | Value | Meaning | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `'deferred'` | The link was obtained from the Detour deferred-link API. This happens when a user who does not have the app installed clicks a Detour link, installs the app, and opens it. The SDK retrieves the original link from the backend on first launch | | `'verified'` | The link arrived as a Universal Link (iOS) or App Link (Android), an HTTP/HTTPS URL that the OS verified belongs to your app and delivered directly to it | | `'scheme'` | The link arrived via a custom URL scheme (e.g. `myapp://path`). These are non-HTTP URLs that the OS routes to your app based on the registered scheme | ### DetourLink [Section titled “DetourLink”](#detourlink) ```ts type DetourLink = { url: string | URL; route: string; pathname: string; params: Record; type: LinkType; } | null; ``` Resolved link data, or `null` if no link was detected. * `url` * Type `string | URL` * Original URL as parsed by the `URL` constructor, or raw string if parsing failed. * `route` * Type `string` * Full in-app route including query string (e.g. `/details?utm=1`). For web URLs, the first path segment (app hash) is stripped. * `pathname` * Type `string` * Path without query string (e.g. `/details`). For web URLs, the first path segment (app hash) is stripped. * `params` * Type `Record` * Query string parameters as a key-value record. All values are strings. * `type` * Type [`LinkType`](#linktype) * How the link arrived in the app. ### DetourContextType [Section titled “DetourContextType”](#detourcontexttype) ```ts type DetourContextType = { isLinkProcessed: boolean; link: DetourLink; clearLink: () => void; }; ``` Shape of the value returned by [`useDetourContext`](#usedetourcontext). * `isLinkProcessed` * Type `boolean` * `true` once the SDK has finished all link detection attempts. `false` during cold start processing. * `link` * Type [`DetourLink`](#detourlink) * Resolved link data or `null`. Persists until `clearLink()` is called. * `clearLink` * Type `() => void` * Resets `link` to `null`. Call after navigating to prevent repeated redirects. ### DetourStorage [Section titled “DetourStorage”](#detourstorage) ```ts interface DetourStorage { getItem(key: string): Promise | string | null; setItem(key: string, value: string): Promise | void; removeItem?(key: string): Promise | void; } ``` Interface for custom storage adapters. The SDK persists a first-entrance flag (used for deferred link matching, which only runs on first app install) and a stable device ID (used for analytics). Methods can be synchronous or asynchronous. By default, `@react-native-async-storage/async-storage` is used. Provide a custom adapter if you use a different storage solution (e.g. MMKV, Expo SecureStore). See [Custom Storage](/docs/sdk/react-native/1.0.1/custom-storage) for examples. ### DetourNativeIntentArgs [Section titled “DetourNativeIntentArgs”](#detournativeintentargs) ```ts type DetourNativeIntentArgs = { path: string; initial: boolean; }; ``` Arguments passed by Expo Router to the `redirectSystemPath` handler. * `path` * Type `string` * Incoming system path or URL from Expo Router. * `initial` * Type `boolean` * `true` when this is the initial URL used to launch the app (cold start), `false` for runtime link events. ### DetourNativeIntentHandler [Section titled “DetourNativeIntentHandler”](#detournativeintenthandler) ```ts type DetourNativeIntentHandler = ( args: DetourNativeIntentArgs, ) => string | Promise; ``` Function signature returned by `createDetourNativeIntentHandler`. Takes the incoming path and returns the resolved in-app route (synchronously or as a promise). ### DetourNativeIntentResolveConfig [Section titled “DetourNativeIntentResolveConfig”](#detournativeintentresolveconfig) ```ts type DetourNativeIntentResolveConfig = { apiKey: string; appID: string; timeoutMs?: number; onResolveError?: (error: unknown, context: DetourNativeIntentArgs) => void; }; ``` Configuration that enables resolve mode in `createDetourNativeIntentHandler`. See [`config` option](#config) for field descriptions. ### DetourNativeIntentOptions [Section titled “DetourNativeIntentOptions”](#detournativeintentoptions) ```ts type DetourNativeIntentOptions = { fallbackPath?: string; hosts?: Array; config?: DetourNativeIntentResolveConfig; mapToRoute?: (value: DetourNativeIntentResolvedValue) => string; }; ``` Options for `createDetourNativeIntentHandler`. See [Options](#options) for detailed field descriptions. ### DetourNativeIntentResolvedValue [Section titled “DetourNativeIntentResolvedValue”](#detournativeintentresolvedvalue) ```ts type DetourNativeIntentResolvedValue = { originalPath: string; initial: boolean; resolvedUrl: URL; }; ``` Object passed to the `mapToRoute` callback after short-link resolution. * `originalPath` * Type `string` * The original incoming system path before resolution. * `initial` * Type `boolean` * `true` if this is the initial URL that launched the app. * `resolvedUrl` * Type `URL` * The resolved destination URL after short-link resolution. ### DetourEventNames [Section titled “DetourEventNames”](#detoureventnames) | Category | Event Name | String Value | | :---------- | :--------------------------- | :------------------------------ | | **General** | `Login` | `login` | | | `Search` | `search` | | | `Share` | `share` | | | `SignUp` | `sign_up` | | | `TutorialBegin` | `tutorial_begin` | | | `TutorialComplete` | `tutorial_complete` | | | `ReEngage` | `re_engage` | | | `Invite` | `invite` | | | `OpenedFromPushNotification` | `opened_from_push_notification` | | **Sales** | `AddPaymentInfo` | `add_payment_info` | | | `AddShippingInfo` | `add_shipping_info` | | | `AddToCart` | `add_to_cart` | | | `RemoveFromCart` | `remove_from_cart` | | | `Refund` | `refund` | | | `ViewItem` | `view_item` | | | `BeginCheckout` | `begin_checkout` | | | `Purchase` | `purchase` | | | `AdImpression` | `ad_impression` | # Custom Storage > Provide a custom storage adapter (MMKV, Expo Secure Store, or your own) for the Detour React Native SDK. By default, the SDK uses `@react-native-async-storage/async-storage` for internal persistence keys. If you prefer another storage engine, pass a custom adapter in `config.storage`. ## Storage interface [Section titled “Storage interface”](#storage-interface) ```ts interface DetourStorage { getItem(key: string): Promise | string | null; setItem(key: string, value: string): Promise | void; removeItem?(key: string): Promise | void; } ``` ## Examples [Section titled “Examples”](#examples) The two adapters below cover the storage engines React Native apps use most often. Any object that matches the `DetourStorage` interface works, so use these as starting points for other engines. ### React Native MMKV [Section titled “React Native MMKV”](#react-native-mmkv) ```ts import { createMMKV } from 'react-native-mmkv'; import { DetourProvider } from '@swmansion/react-native-detour'; const storage = createMMKV(); const mmkvAdapter = { getItem: (key: string) => storage.getString(key) ?? null, setItem: (key: string, value: string) => storage.set(key, value), }; export default function App() { return ( {/* Your app */} ); } ``` ### Expo Secure Store [Section titled “Expo Secure Store”](#expo-secure-store) ```ts import * as SecureStore from 'expo-secure-store'; import { DetourProvider } from '@swmansion/react-native-detour'; const secureStoreAdapter = { getItem: SecureStore.getItemAsync, setItem: SecureStore.setItemAsync, }; export default function App() { return ( {/* Your app */} ); } ``` ## Related pages [Section titled “Related pages”](#related-pages) * [Usage](/docs/sdk/react-native/1.0.1/sdk-usage) * [API Reference](/docs/sdk/react-native/1.0.1/api-reference) # Examples > Runnable Detour React Native examples for Expo Router and React Navigation. The RN SDK repository ships six runnable apps, one per integration style. Clone the one closest to your setup, run it, and copy what you need. Each card below links to the example’s `README.md` on GitHub, with the project structure, setup steps, and full source. ## Example catalog [Section titled “Example catalog”](#example-catalog) [expo-bare ](https://github.com/software-mansion-labs/react-native-detour/blob/v1.0.1/examples/expo-bare/README.md)The SDK on its own, without a router. Use it to check that your configuration works. [expo-router ](https://github.com/software-mansion-labs/react-native-detour/blob/v1.0.1/examples/expo-router/README.md)The smallest Expo Router setup. The app opens the link's screen as soon as a link arrives. [expo-router-native-intent ](https://github.com/software-mansion-labs/react-native-detour/blob/v1.0.1/examples/expo-router-native-intent/README.md)Expo Router resolves short links in its native intent handler, so the app opens the final screen directly. [expo-router-advanced ](https://github.com/software-mansion-labs/react-native-detour/blob/v1.0.1/examples/expo-router-advanced/README.md)Expo Router with sign-in. A link that arrives while the user is signed out opens after they sign in. [react-navigation ](https://github.com/software-mansion-labs/react-native-detour/blob/v1.0.1/examples/react-navigation/README.md)The smallest React Navigation setup. The app maps the link's path to a screen. [react-navigation-advanced ](https://github.com/software-mansion-labs/react-native-detour/blob/v1.0.1/examples/react-navigation-advanced/README.md)React Navigation with sign-in. Links to protected screens open after the user signs in. ## Recommended user path [Section titled “Recommended user path”](#recommended-user-path) 1. Complete [Installation](/docs/sdk/react-native/1.0.1/sdk-installation). 2. Read [Usage](/docs/sdk/react-native/1.0.1/sdk-usage) for API model. 3. Start from the closest example and adapt route mapping/auth flow. 4. Validate behavior with [Testing & Troubleshooting](/docs/sdk/react-native/1.0.1/testing). # Migration to v1 > Migrate the Detour React Native SDK to v1.0.1. This guide helps migrate from older RN SDK integration patterns to `@swmansion/react-native-detour@1.0.1`. ## Migration checklist [Section titled “Migration checklist”](#migration-checklist) | Area | Before | In `1.0.1` | | ------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------- | | Config key | `API_KEY` | `apiKey` | | Context shape | `linkRoute`, `linkUrl`, `linkType` exposed directly | `link` object (`route`, `pathname`, `params`, `url`, `type`) | | Navigation mapping | Parse route string manually | Prefer `link.pathname` and `link.params` | | Link-source control | No explicit mode | `linkProcessingMode: 'all' \| 'web-only' \| 'deferred-only'` | | Expo Router native intent | Custom handler only | Optional `createDetourNativeIntentHandler` from `@swmansion/react-native-detour/expo-router` | | Analytics import | legacy package/import variants | `DetourAnalytics` from `@swmansion/react-native-detour` | ## Config update [Section titled “Config update”](#config-update) ```ts // before const config = { API_KEY: process.env.EXPO_PUBLIC_DETOUR_API_KEY, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID, }; // after const config = { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID, }; ``` ## Context update (`linkRoute` -> `link`) [Section titled “Context update (linkRoute -> link)”](#context-update-linkroute---link) ```ts // before const { isLinkProcessed, linkRoute, clearLink } = useDetourContext(); if (isLinkProcessed && linkRoute) { router.replace(linkRoute); clearLink(); } // after const { isLinkProcessed, link, clearLink } = useDetourContext(); if (isLinkProcessed && link) { router.replace({ pathname: link.pathname, params: link.params }); clearLink(); } ``` ## Decide `linkProcessingMode` [Section titled “Decide linkProcessingMode”](#decide-linkprocessingmode) * Keep default `'all'` when SDK should process deferred + web links + custom schemes. * Use `'web-only'` when your app handles custom schemes separately. * Use `'deferred-only'` when Expo Router native-intent flow already handles runtime/initial URLs. ## Expo Router native-intent migration (optional) [Section titled “Expo Router native-intent migration (optional)”](#expo-router-native-intent-migration-optional) If your app uses Expo Router `+native-intent.tsx`, there are two potential link-processing layers: * Expo Router `redirectSystemPath` (before route resolution) * Detour runtime processing in `DetourProvider` / `useDetourContext()` Without explicit coordination, the same URL can be processed twice, or users can briefly land on an intermediate/unknown route before the final Detour route is resolved. `createDetourNativeIntentHandler` is a helper that gives you a deterministic pre-routing step for Detour links. It can: * match Detour hosts * resolve short links using `apiKey` + `appID` * map the resolved URL to a final Expo Router route * return a safe `fallbackPath` when resolution fails/timeouts ### Recommended setup [Section titled “Recommended setup”](#recommended-setup) 1. Add `createDetourNativeIntentHandler` in `app/+native-intent.tsx`. 2. Set `linkProcessingMode: 'deferred-only'` in `DetourProvider` so runtime/initial web links are not processed twice. 3. Keep `DetourProvider` mounted for deferred matching and analytics. app/+native-intent.tsx ```ts import { createDetourNativeIntentHandler } from '@swmansion/react-native-detour/expo-router'; export const redirectSystemPath = createDetourNativeIntentHandler({ fallbackPath: '', config: { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, timeoutMs: 1200, }, }); // app/_layout.tsx const config = { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, linkProcessingMode: 'deferred-only', }; ``` If you need custom path shaping (for example dropping app-hash segment), use the handler `mapToRoute` callback. ### Related resources [Section titled “Related resources”](#related-resources) * [Usage: Expo Router native intent helper](/docs/sdk/react-native/1.0.1/sdk-usage#expo-router-integration) * [Examples](/docs/sdk/react-native/1.0.1/examples) (`expo-router-native-intent`, `expo-router-advanced`) * Expo Router docs: [Customizing links with +native-intent](https://docs.expo.dev/router/advanced/native-intent/) ## Validate after migration [Section titled “Validate after migration”](#validate-after-migration) 1. Trigger deferred link on fresh install and confirm `link?.type === 'deferred'`. 2. Trigger runtime Universal/App Link and confirm `link?.type === 'verified'`. 3. Trigger custom scheme link and confirm expected behavior for selected `linkProcessingMode`. 4. Confirm only one `DetourProvider` is mounted. # Installation > Install the Detour React Native SDK in Expo or bare React Native projects. Detour provides a React Native SDK (`@swmansion/react-native-detour`) compatible with Expo and bare React Native projects. Before installation, make sure your app is created in the Detour Dashboard and you have: * `appID` from **API configuration** * publishable `apiKey` from **API configuration** * platform integration snippets from **App configuration** ## Install package [Section titled “Install package”](#install-package) * npm ```bash npm install @swmansion/react-native-detour ``` * yarn ```bash yarn add @swmansion/react-native-detour ``` * pnpm ```bash pnpm add @swmansion/react-native-detour ``` * bun ```bash bun add @swmansion/react-native-detour ``` ## Install peer dependencies [Section titled “Install peer dependencies”](#install-peer-dependencies) * npm ```bash npm install expo-localization react-native-device-info expo-clipboard @react-native-async-storage/async-storage expo-application ``` * yarn ```bash yarn add expo-localization react-native-device-info expo-clipboard @react-native-async-storage/async-storage expo-application ``` * pnpm ```bash pnpm add expo-localization react-native-device-info expo-clipboard @react-native-async-storage/async-storage expo-application ``` * bun ```bash bun add expo-localization react-native-device-info expo-clipboard @react-native-async-storage/async-storage expo-application ``` Device info provider `react-native-device-info` is a required peer dependency. Unlike storage, the device info source is not configurable. The SDK imports the library directly to read the device model, manufacturer, OS version, and user agent for the deferred-link fingerprint. There is no adapter to plug in. Custom storage `@react-native-async-storage/async-storage` is optional if you provide a custom storage adapter in config. See [Custom Storage](/docs/sdk/react-native/1.0.1/custom-storage). Expo managed workflow After installing `react-native-device-info` in Expo managed workflow, create a custom development build: ## Credentials in app config [Section titled “Credentials in app config”](#credentials-in-app-config) ```ts import { type Config } from '@swmansion/react-native-detour'; export const detourConfig: Config = { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, shouldUseClipboard: true, }; ``` Keep `apiKey` and `appID` in environment variables or another secure configuration layer. ## Next steps [Section titled “Next steps”](#next-steps) 1. Implement the provider and link handling in [Usage](/docs/sdk/react-native/1.0.1/sdk-usage). 2. Pick a runnable app pattern in [Examples](/docs/sdk/react-native/1.0.1/examples). 3. If upgrading existing code, follow [Migration to v1](/docs/sdk/react-native/1.0.1/migration-1-0-1). # Usage > Initialize DetourProvider, handle resolved links, and integrate Detour with Expo Router or React Navigation. ## Initialize provider in app root [Section titled “Initialize provider in app root”](#initialize-provider-in-app-root) ```ts import { DetourProvider, type Config } from '@swmansion/react-native-detour'; const config: Config = { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, shouldUseClipboard: true, // linkProcessingMode: 'all' // default }; export default function RootLayout() { return ( ); } ``` One DetourProvider per app Use exactly one `DetourProvider` in your app tree. If you want complete, runnable integrations for each navigation style, go to [Examples](/docs/sdk/react-native/1.0.1/examples). ## Handle resolved links with `useDetourContext()` [Section titled “Handle resolved links with useDetourContext()”](#handle-resolved-links-with-usedetourcontext) ```ts import { useDetourContext } from '@swmansion/react-native-detour'; import { useEffect } from 'react'; import { useRouter } from 'expo-router'; export function RootNavigator() { const { isLinkProcessed, link, clearLink } = useDetourContext(); const router = useRouter(); useEffect(() => { if (!isLinkProcessed || !link) return; router.replace({ pathname: link.pathname, params: link.params, }); clearLink(); }, [clearLink, isLinkProcessed, link, router]); if (!isLinkProcessed) { return null; } return ; } ``` ### `link` structure [Section titled “link structure”](#link-structure) `useDetourContext()` returns a `link` object (or `null`): * `link.route` Full in-app route with query string (e.g. `/details?utm=1`). * `link.pathname` Path without query (e.g. `/details`). * `link.params` Parsed query params record. * `link.url` Original URL or raw value. * `link.type` Type `'deferred' | 'verified' | 'scheme'` Use `clearLink()` after handling navigation to avoid repeated redirects. ## Choose `linkProcessingMode` [Section titled “Choose linkProcessingMode”](#choose-linkprocessingmode) `Config.linkProcessingMode` controls which link sources are handled by the SDK: | Value | Deferred | Universal / App | Custom scheme | | ----------------- | -------- | --------------- | ------------- | | `'all'` (default) | Yes | Yes | Yes | | `'web-only'` | Yes | Yes | No | | `'deferred-only'` | Yes | No | No | Use `'deferred-only'` when Expo Router `+native-intent.tsx` already handles runtime and initial URLs. ## Expo Router integration [Section titled “Expo Router integration”](#expo-router-integration) Recommended for Expo apps If you use Expo Router, we **strongly recommend** using our dedicated `createDetourNativeIntentHandler` API in a [`+native-intent.tsx`](https://docs.expo.dev/router/advanced/native-intent/) file. This intercepts URLs at the router level before any screen renders, avoiding flash-of-wrong-screen issues and providing the smoothest deep-linking experience. Expo Router’s `+native-intent.tsx` file lets you intercept and transform incoming system URLs before the router processes them. The Detour SDK provides `createDetourNativeIntentHandler`, a factory that creates a handler pre-configured for Detour link resolution. ### Basic setup [Section titled “Basic setup”](#basic-setup) Create a `+native-intent.tsx` file in your app directory (e.g. `src/app/+native-intent.tsx`): ```ts import { createDetourNativeIntentHandler } from "@swmansion/react-native-detour/expo-router"; export const redirectSystemPath = createDetourNativeIntentHandler(); ``` This is intercept mode Any URL matching a Detour host is caught and the user is routed to the home screen (default `fallbackPath`). Deferred links are still handled separately by `DetourProvider`. ### Resolve mode [Section titled “Resolve mode”](#resolve-mode) To resolve Detour short links to their full destination URLs at the router level, pass a `config` object: ```ts import { createDetourNativeIntentHandler } from "@swmansion/react-native-detour/expo-router"; export const redirectSystemPath = createDetourNativeIntentHandler({ config: { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, }, }); ``` In resolve mode, when a user taps a Detour short link (e.g. `https://abc123.godetour.link/xyz`), the handler resolves it to the full destination URL and routes directly to the correct screen, all before the first render. See [Expo Router helper](/docs/sdk/react-native/1.0.1/api-reference#expo-router-helper) for the full list of options (custom hosts, fallback path, timeout, route mapping, etc.). ### Configure `DetourProvider` for native intent [Section titled “Configure DetourProvider for native intent”](#configure-detourprovider-for-native-intent) When using `+native-intent.tsx` to handle Universal Links and App Links, configure the provider to only process deferred links: ```ts const config: Config = { apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!, appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!, linkProcessingMode: "deferred-only", }; ``` This avoids double-processing: `+native-intent.tsx` handles runtime and initial URLs, while `DetourProvider` handles deferred deep links only. ### Advanced examples [Section titled “Advanced examples”](#advanced-examples) The SDK repository includes examples for common native intent scenarios: * **`expo-router-native-intent`** — Full resolve mode setup with a custom `mapToRoute` function and scheme URL handling. Use this as a starting point when you need to transform resolved URLs before routing (e.g. stripping path prefixes) or when you want to handle custom scheme links alongside Detour links. * **`expo-router-advanced`** — Demonstrates Detour inside an auth- and onboarding-gated flow. Deferred and runtime links are preserved through sign-in and one-time onboarding before navigating to their destination. Use this when your app requires the user to be signed in (and optionally onboarded) before a deep link should take effect. For all runnable examples, see [Examples](/docs/sdk/react-native/1.0.1/examples). ### Alternative: catch-all route without `+native-intent.tsx` [Section titled “Alternative: catch-all route without +native-intent.tsx”](#alternative-catch-all-route-without-native-intenttsx) If you prefer not to use `+native-intent.tsx`, you can handle all incoming links through `DetourProvider` with `linkProcessingMode: 'all'` (the default) and implement a catch-all route in your navigation stack. In this approach, `DetourProvider` processes all link types (deferred, Universal/App Links, and custom scheme). You consume the resolved link via `useDetourContext()` in a top-level component and navigate programmatically: ```ts const { isLinkProcessed, link, clearLink } = useDetourContext(); useEffect(() => { if (!isLinkProcessed || !link) return; router.replace({ pathname: link.pathname, params: link.params }); clearLink(); }, [isLinkProcessed, link]); ``` This was the primary integration pattern before the SDK introduced native intent support (pre-1.x). It still works but the `+native-intent.tsx` approach provides better UX by resolving links before the first screen renders. ## Analytics module [Section titled “Analytics module”](#analytics-module) `DetourAnalytics` emits events that are sent by the active `DetourProvider`. ```ts import { DetourAnalytics, DetourEventNames, } from "@swmansion/react-native-detour"; DetourAnalytics.logEvent(DetourEventNames.Purchase, { item: "subscription_premium", currency: "USD", }); DetourAnalytics.logRetention("home_screen_opened"); ``` ## Related pages [Section titled “Related pages”](#related-pages) * [Examples](/docs/sdk/react-native/1.0.1/examples) * [Migration to v1](/docs/sdk/react-native/1.0.1/migration-1-0-1) * [API Reference](/docs/sdk/react-native/1.0.1/api-reference) * [Custom Storage](/docs/sdk/react-native/1.0.1/custom-storage) * [Testing & Troubleshooting](/docs/sdk/react-native/1.0.1/testing) # Testing & Troubleshooting > Test deferred linking on iOS and Android and troubleshoot common Detour React Native SDK issues. Testing deferred linking has platform constraints. The complete deferred flow requires the store redirect chain, that is the App Store or Google Play. ## Testing iOS [Section titled “Testing iOS”](#testing-ios) If your app is not yet released: 1. Remove any existing install from the device so the next open counts as a first launch. 2. Open a Detour link in a mobile browser. 3. Expect the App Store redirect to fail. With no public listing yet, the store has no listing to open, which does not affect the test. 4. Install your development build. 5. Launch the app and confirm the link resolves within the matching window (currently 15 minutes). ## Testing Android [Section titled “Testing Android”](#testing-android) For non-production builds, the install referrer often does not work in store test tracks, so deterministic matching can be unavailable. You can still verify the probabilistic flow: 1. Uninstall the app from the device. 2. Open a Detour link in a mobile browser. 3. Expect the Play Store redirect to fail when the production listing is unavailable. 4. Install your development build. 5. Launch the app and confirm the link resolves within the matching window (currently 15 minutes). For local install referrer testing workarounds, see [How to test the Play Store Install Referrer API](https://medium.com/@madicdjordje/how-to-test-the-play-store-install-referrer-api-78a63d59945b). Missing `click_id` If you encounter click\_id missing in your build see limitations section [Play Store referrer missing](/docs/platform/architecture/architecture-limitations#play-store-referrer-missing) ## Testing Android App Links [Section titled “Testing Android App Links”](#testing-android-app-links) Whether an installed app opens a Detour link depends on the Dashboard SHA-256 matching the key that signed **the build on your device**: | Installation method | SHA-256 to configure | Where to find it | | :------------------------------------------------------ | :------------------------------------- | :------------------------------------------------------------------------------------------------------- | | Google Play, any track | Play App Signing key | Play Console → **Protected with Play** → **Manage Play app signing** → *App signing key certificate* | | `eas build`, any profile, including `development` | Keystore in your EAS account | `eas credentials -p android` → profile → **Keystore** → `SHA256 Fingerprint` | | `npx expo run:android` / `npx react-native run-android` | Project’s `android/app/debug.keystore` | `keytool -list -v -keystore android/app/debug.keystore -storepass android`, or `./gradlew signingReport` | To check any build directly: `apksigner verify --print-certs app-debug.apk`. The complete procedure is described in [Finding your Android SHA-256 fingerprint](/docs/platform/fundamentals/universal-app-links#finding-your-android-sha-256-fingerprint). ## Common issues [Section titled “Common issues”](#common-issues) * No match found (`404`) * click is older than matching window * click was not recorded in Dashboard logs * for deterministic Android flow: missing `click_id` in referrer * `429 Too Many Requests` * add retry/backoff and avoid heavy testing with one key across many devices * iOS clipboard prompt not appearing * check `shouldUseClipboard: true` * verify copy-link flow is enabled in Dashboard * iOS Universal Link opens the browser instead of the app during development * before your domain and App Store listing have fully propagated, iOS resolves the `apple-app-site-association` file through Apple’s CDN, which may not have it yet * append `?mode=developer` to the matching entry in your iOS `associatedDomains` (e.g. `applinks:yourorg.godetour.link?mode=developer`) so the device fetches the association file directly from your server, bypassing the CDN * this also requires enabling **Developer Mode** on the test device (Settings → Privacy & Security → Developer Mode). Remove the `?mode=developer` suffix before shipping * Link processed twice * confirm you call `clearLink()` after navigation * confirm exactly one `DetourProvider` instance is mounted For integration patterns that are easy to test end-to-end, use [Examples](/docs/sdk/react-native/1.0.1/examples). # API Reference > Complete API reference for the Detour React Native SDK. ## DetourProvider [Section titled “DetourProvider”](#detourprovider) Top-level React component that initializes the Detour SDK. It manages the full link processing lifecycle: deferred link matching, Universal/App Link listening, custom scheme handling, short-link resolution, and analytics event transport. On mount, the provider: 1. Attaches a runtime listener for Universal Links (iOS) / App Links (Android) received while the app is running 2. Checks for an initial URL that launched the app (Universal/App Link on cold start) 3. If the URL is a short link (single-segment web URL), resolves it to the full destination URL 4. If no initial link is found, checks if this is the first app entrance and queries the Detour API for a matching deferred link 5. Subscribes to analytics events and forwards them to the Detour backend 6. Updates context state with the result via [`useDetourContext()`](#usedetourcontext) Use exactly one `DetourProvider` at the root of your app tree. The deferred link matching runs only once per app install, and subsequent app opens skip the API call entirely. Universal/App Links and custom scheme links are handled on every app open. ### Signature [Section titled “Signature”](#signature) ```ts function DetourProvider(props: { config: Config; children: React.ReactNode; }): JSX.Element; ``` ### Config fields [Section titled “Config fields”](#config-fields) * `API_KEY` required * Type `string` * Publishable API key from the Detour Dashboard. Used as a Bearer token for API calls (deferred link matching, short-link resolution, analytics) * `appID` required * Type `string` * Unique app identifier from the Detour Dashboard. Used to route requests to the correct app. * `shouldUseClipboard` * Type `boolean` Default `true` * iOS only. When `true`, the SDK reads the device clipboard and includes it in the probabilistic fingerprint for deferred link matching. May trigger the iOS system paste permission prompt. Set to `false` to skip clipboard access. * `storage` * Type [`DetourStorage` ](#detourstorage)Default `AsyncStorage` * Custom storage adapter for SDK persistence. The SDK stores a first-entrance flag (for deferred link matching) and a device ID (for analytics). Defaults to `@react-native-async-storage/async-storage` if installed. ### Error handling [Section titled “Error handling”](#error-handling) If the match API returns an error or network calls fail, the provider sets `isLinkProcessed` to `true` and keeps `linkUrl`, `linkRoute`, and `linkType` as `null`, allowing your app to continue normally. Errors are logged to the console. ## useDetourContext [Section titled “useDetourContext”](#usedetourcontext) Hook that returns the current link processing state from the nearest `DetourProvider`. Throws an error if called outside of a `DetourProvider` tree. ### Signature [Section titled “Signature”](#signature-1) ```ts function useDetourContext(): DetourContextType; ``` ### Returned object [Section titled “Returned object”](#returned-object) * `isLinkProcessed` * Type `boolean` * `true` once the SDK has finished all link detection attempts, regardless of whether a link was found. Use this to conditionally show a loading/splash screen during cold start. * `linkUrl` * Type `string | URL | null` * The matched link, a `URL` object for full URLs or a pathname string. Can be a deferred link, Universal/App Link, or custom scheme link. `null` if no match was found or an error occurred. * `linkRoute` * Type `string | null` * Normalized in-app route extracted from the matched link. For web URLs, the first path segment (app hash) is stripped and query string is preserved (e.g. `/promo/123?utm=1`). For custom scheme links, the host and path are combined into a route. `null` if no link was matched. * `linkType` * Type `LinkType | null` * How the link arrived in the app. See [LinkType](#linktype) for values. `null` if no link was found. * `clearLink` * Type `() => void` * Resets `linkUrl`, `linkRoute`, and `linkType` to `null`. Call after handling navigation to prevent repeated redirects. ## DetourAnalytics [Section titled “DetourAnalytics”](#detouranalytics) Analytics module exported from the package root. Requires an active `DetourProvider` in the component tree. Events are silently dropped if no provider is mounted. ### Methods [Section titled “Methods”](#methods) #### `logEvent` [Section titled “logEvent”](#logevent) ```ts DetourAnalytics.logEvent( eventName: DetourEventNames | `${DetourEventNames}`, data?: any, ): void; ``` Records a user action or custom event and sends it to the Detour backend. Pass one of the predefined [`DetourEventNames`](#detoureventnames) or a custom string. The optional `data` parameter accepts arbitrary metadata (e.g. `{ itemId: '123', price: 49.99 }`). ## Types [Section titled “Types”](#types) ### Config [Section titled “Config”](#config) ```ts type Config = { appID: string; API_KEY: string; shouldUseClipboard?: boolean; storage?: DetourStorage; }; ``` Configuration object passed to `DetourProvider`. See [Config fields](#config-fields) for detailed descriptions. ### DetourContextType [Section titled “DetourContextType”](#detourcontexttype) ```ts type DetourContextType = { isLinkProcessed: boolean; linkUrl: string | URL | null; linkType: LinkType | null; linkRoute: string | null; clearLink: () => void; }; ``` Shape of the value returned by [`useDetourContext`](#usedetourcontext). * `isLinkProcessed` * Type `boolean` * `true` once the SDK has finished all link detection attempts. `false` during cold start processing. * `linkUrl` * Type `string | URL | null` * Matched link data or `null` if no match found. * `linkRoute` * Type `string | null` * Extracted in-app route or `null`. For web URLs, the first path segment (app hash) is stripped. * `linkType` * Type `LinkType | null` * How the link arrived: `'deferred'`, `'verified'`, or `'scheme'`. `null` if no link found. * `clearLink` * Type `() => void` * Resets link state to `null`. Call after navigating to prevent repeated redirects. ### LinkType [Section titled “LinkType”](#linktype) ```ts type LinkType = 'deferred' | 'verified' | 'scheme'; ``` Describes how the link arrived in the app: | Value | Meaning | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `'deferred'` | The link was obtained from the Detour deferred-link API. This happens when a user who does not have the app installed clicks a Detour link, installs the app, and opens it. The SDK retrieves the original link from the backend on first launch | | `'verified'` | The link arrived as a Universal Link (iOS) or App Link (Android), an HTTP/HTTPS URL that the OS verified belongs to your app and delivered directly to it | | `'scheme'` | The link arrived via a custom URL scheme (e.g. `myapp://path`). These are non-HTTP URLs that the OS routes to your app based on the registered scheme | ### DetourStorage [Section titled “DetourStorage”](#detourstorage) ```ts interface DetourStorage { getItem(key: string): Promise | string | null; setItem(key: string, value: string): Promise | void; removeItem?(key: string): Promise | void; } ``` Interface for custom storage adapters. The SDK persists a first-entrance flag (used for deferred link matching, which only runs on first app install) and a stable device ID (used for analytics). Methods can be synchronous or asynchronous. By default, `@react-native-async-storage/async-storage` is used. Provide a custom adapter if you use a different storage solution (e.g. MMKV, Expo SecureStore). ### DetourEventNames [Section titled “DetourEventNames”](#detoureventnames) | Category | Event Name | String Value | | :---------- | :--------------------------- | :------------------------------ | | **General** | `Login` | `login` | | | `Search` | `search` | | | `Share` | `share` | | | `SignUp` | `sign_up` | | | `TutorialBegin` | `tutorial_begin` | | | `TutorialComplete` | `tutorial_complete` | | | `ReEngage` | `re_engage` | | | `Invite` | `invite` | | | `OpenedFromPushNotification` | `opened_from_push_notification` | | **Sales** | `AddPaymentInfo` | `add_payment_info` | | | `AddShippingInfo` | `add_shipping_info` | | | `AddToCart` | `add_to_cart` | | | `RemoveFromCart` | `remove_from_cart` | | | `Refund` | `refund` | | | `ViewItem` | `view_item` | | | `BeginCheckout` | `begin_checkout` | | | `Purchase` | `purchase` | | | `AdImpression` | `ad_impression` | # Custom Storage > Provide a custom storage adapter (MMKV, Expo Secure Store, or your own) for the Detour React Native SDK. By default, this library attempts to use `@react-native-async-storage/async-storage` to persist the “first entrance” flag. However, you can inject your own storage implementation. This is useful if you: * Use **MMKV** for performance. * Use **Expo Secure Store** for sensitive data. ## Usage [Section titled “Usage”](#usage) To use a custom storage, pass an object implementing the `DetourStorage` interface to the `storage` property in the `config`. The interface is simple: ```ts interface DetourStorage { getItem(key: string): Promise | string | null; setItem(key: string, value: string): Promise | void; removeItem?(key: string): Promise | void; } ``` ### Example: React Native MMKV [Section titled “Example: React Native MMKV”](#example-react-native-mmkv) ```ts import { createMMKV } from "react-native-mmkv"; import { DetourProvider } from "@swmansion/react-native-detour"; const storage = createMMKV(); const mmkvAdapter = { // MMKV returns undefined if missing, but we expect null getItem: (key: string) => storage.getString(key) ?? null, setItem: (key: string, value: string) => storage.set(key, value), }; export default function App() { return ( {/* Your App */} ); } ``` ### Example: Expo Secure Store [Section titled “Example: Expo Secure Store”](#example-expo-secure-store) ```ts import * as SecureStore from "expo-secure-store"; import { DetourProvider } from "@swmansion/react-native-detour"; const secureStoreAdapter = { getItem: SecureStore.getItemAsync, setItem: SecureStore.setItemAsync, }; export default function App() { return ( {/* Your App */} ); } ``` # Installation > Install the Detour React Native SDK in Expo or bare React Native projects. Detour provides a React Native SDK (`@swmansion/react-native-detour`) compatible with Expo and bare React Native projects. ## Install [Section titled “Install”](#install) * npm ```bash npm install @swmansion/react-native-detour ``` * yarn ```bash yarn add @swmansion/react-native-detour ``` * pnpm ```bash pnpm add @swmansion/react-native-detour ``` * bun ```bash bun add @swmansion/react-native-detour ``` ### Additional dependencies [Section titled “Additional dependencies”](#additional-dependencies) * npm ```bash npm install expo-localization react-native-device-info expo-clipboard @react-native-async-storage/async-storage expo-application ``` * yarn ```bash yarn add expo-localization react-native-device-info expo-clipboard @react-native-async-storage/async-storage expo-application ``` * pnpm ```bash pnpm add expo-localization react-native-device-info expo-clipboard @react-native-async-storage/async-storage expo-application ``` * bun ```bash bun add expo-localization react-native-device-info expo-clipboard @react-native-async-storage/async-storage expo-application ``` Device info provider `react-native-device-info` is a required peer dependency. Unlike storage, the device info source is not configurable. The SDK imports the library directly to read the device model, manufacturer, OS version, and user agent for the deferred-link fingerprint. There is no adapter to plug in. Expo managed workflow After installing react-native-device-info in expo managed workflow you need to create custom development build Custom storage You can override the default persistent storage (@react-native-async-storage/async-storage) by providing an alternative storage implementation. Pass your custom storage object via the configuration settings. [See more](/docs/sdk/react-native/0.6.x/custom-storage) # Usage > Initialize DetourProvider, handle resolved links, and integrate Detour with Expo Router or React Navigation. ## Initialize provider in root of your app [Section titled “Initialize provider in root of your app”](#initialize-provider-in-root-of-your-app) ```ts import { DetourProvider, type Config } from '@swmansion/react-native-detour'; const config: Config = { API_KEY: '', appID: '', shouldUseClipboard: true, }; export default function RootLayout() { return ( ); } ``` Keep credentials in .env Keep your API\_KEY and appID in .env ## Links module [Section titled “Links module”](#links-module) ### Use values from context (Expo Router) [Section titled “Use values from context (Expo Router)”](#use-values-from-context-expo-router) SDK exposes `useDetourContext()` hook that returns object containing information about link and processing status. Our recommendation is to use the hook before you application renders navigation container. The SDK only looks for a deferred link during the application’s first launch after installation. Subsequent launches should be handled by Universal Links for iOS and App Links for Android. Deep links based on uri-scheme are also supported. ```ts import { useDetourContext } from '@swmansion/react-native-detour'; import * as SplashScreen from 'expo-splash-screen'; import { Stack, usePathname, useRouter } from 'expo-router'; SplashScreen.preventAutoHideAsync(); const normalizePath = (route: string) => route.split('?')[0] || '/'; export function RootNavigator() { const { isLinkProcessed, linkRoute, clearLink } = useDetourContext(); const pathname = usePathname(); const router = useRouter(); useEffect(() => { if (isLinkProcessed) { SplashScreen.hide(); } }, [isLinkProcessed]); useEffect(() => { if (!isLinkProcessed || !linkRoute) return; if (pathname !== normalizePath(linkRoute)) { router.replace(linkRoute); return; } clearLink(); // avoid redirecting again when returning to this screen }, [clearLink, isLinkProcessed, linkRoute, pathname, router]); if (!isLinkProcessed) { return null; } return ; } ``` **linkRoute format** `linkRoute` is a ready-to-use path (including query string if present). Example: ```plaintext URL: https://example-organization.godetour.link/Ar8UqUjGzG/segment1?param1=value1 route: /segment1?param1=value1 ``` **Short links** If the incoming link path has a single segment (for example `https://example-organization.godetour.link/Ar8UqUjGzG`), the SDK resolves it to the full link before computing `linkRoute`. **Custom schemes** For custom scheme links, the route is computed from the host + pathname + query. Example: `myapp://promo/123?utm=1` → `/promo/123?utm=1`. ### Implementation & lifecycle details [Section titled “Implementation & lifecycle details”](#implementation--lifecycle-details) * On first app launch after install, the SDK checks whether this is the first entrance and attempts deferred matching once per session. * If the app is opened via a Universal/App Link on first launch, the SDK handles that link immediately and skips deferred matching. * `useDetourContext()` exposes the final state. Wait for `isLinkProcessed` before routing. * Matching is asynchronous. Show a loading state while it runs. * Clipboard matching (iOS) can be controlled via `Config.shouldUseClipboard`. * After handling a route, call `clearLink()` to prevent repeated redirects. * On subsequent launches the SDK checks only for Universal/App Links and processes them. ### Universal and App Links with Expo Router [Section titled “Universal and App Links with Expo Router”](#universal-and-app-links-with-expo-router) Expo Router handles Universal/App Links on its own and may redirect to a route before the SDK finishes processing the link. The detected route can include the hash fragment Detour uses for link identification, so either match such routes in your navigation or add a catch-all. Links resolve asynchronously, so without a catch-all Expo Router may show *Not Found* route for unknown paths while Detour resolves the final destination. Use a catch-all route to keep the user on a loading screen until Detour provides `linkRoute`: app/\[...link].tsx ```ts import { useDetourContext } from '@swmansion/react-native-detour'; import { useEffect } from 'react'; import { useRouter } from 'expo-router'; export default function CatchAllRoute() { const { isLinkProcessed, linkRoute } = useDetourContext(); const router = useRouter(); useEffect(() => { if (!isLinkProcessed) return; if (linkRoute) { router.replace(linkRoute); } else { router.replace('/'); } }, [isLinkProcessed, linkRoute, router]); return null; } ``` This keeps the UX consistent and avoids a flash of the Not Found screen. ## Analytics module [Section titled “Analytics module”](#analytics-module) You can log specific user behaviors using the `DetourAnalytics` module. This allows you to track the user journey through your conversion funnel. Events are emitted via a global internal system and handled by the `DetourProvider`. ### Example usage [Section titled “Example usage”](#example-usage) ```ts import { DetourAnalytics } from '@software-mansion-labs/react-native-detour'; // Simple event logging DetourAnalytics.logEvent("login"); // Event logging with custom metadata DetourAnalytics.logEvent('purchase', { item: 'subscription_premium', currency: 'USD', }); ``` Use exactly one DetourProvider To ensure accurate event tracking and avoid duplicate logs, make sure your application is wrapped in exactly one DetourProvider. Check the list of defined events [here](/docs/sdk/react-native/0.6.x/api-reference#detoureventnames) # Testing & Troubleshooting > Test deferred linking on iOS and Android and troubleshoot common Detour React Native SDK issues. There are known limitations for testing flow during implementations. It’s not possible to test the full flow of deferred links and universal app links unless your app is already in Play Store / App Store. ## Testing iOS [Section titled “Testing iOS”](#testing-ios) If you are still in development mode and your app is not released to App Store, you can test SDK behavior by following steps: 1. Ensure the app is uninstalled on the device. 2. Visit the link on your device by pasting or typing its URL into your device’s browser. 3. Platform will try to redirect to App Store page based on `App Store ID` you put in configuration. Your app might not exist at that moment. 4. Install you app in development mode like you normally do. 5. See the result. SDK should return the link you clicked if you managed to do it within the time window (currently 15 minutes). ## Testing Android [Section titled “Testing Android”](#testing-android) In most cases `referrer` param is not passed to Play Store unless your production app is released. That means even though your app is displayed in Play Store for internal/external test, `referrer` probably won’t work and so SDK won’t use deterministic flow. However, still you can test probabilistic flow by following steps: 1. Ensure the app is uninstalled on the device. 2. Visit the link on your device by pasting or typing its URL into your device’s browser. 3. Platform will try to redirect to Play Store based on `package name`. Your app might not exist at that moment. 4. Install you app in development mode like you normally do. 5. See the result. SDK should return the link you clicked if you managed to do it within the time window (currently 15 minutes). You want to test `referrer` api in development mode, there are some workarounds for that.\ Take a look on that medium article: [link](https://medium.com/@madicdjordje/how-to-test-the-play-store-install-referrer-api-78a63d59945b) Missing `click_id` If you encounter click\_id missing in your build see limitations section [Play Store referrer missing](/docs/platform/architecture/architecture-limitations#play-store-referrer-missing) ### Testing Android App Links [Section titled “Testing Android App Links”](#testing-android-app-links) Whether an installed app opens a Detour link depends on the Dashboard SHA-256 matching the key that signed **the build on your device**: | Installation method | SHA-256 to configure | Where to find it | | :------------------------------------------------------ | :------------------------------------- | :------------------------------------------------------------------------------------------------------- | | Google Play, any track | Play App Signing key | Play Console → **Protected with Play** → **Manage Play app signing** → *App signing key certificate* | | `eas build`, any profile, including `development` | Keystore in your EAS account | `eas credentials -p android` → profile → **Keystore** → `SHA256 Fingerprint` | | `npx expo run:android` / `npx react-native run-android` | Project’s `android/app/debug.keystore` | `keytool -list -v -keystore android/app/debug.keystore -storepass android`, or `./gradlew signingReport` | To check any build directly: `apksigner verify --print-certs app-debug.apk`. The complete procedure is described in [Finding your Android SHA-256 fingerprint](/docs/platform/fundamentals/universal-app-links#finding-your-android-sha-256-fingerprint). ## Common issues [Section titled “Common issues”](#common-issues) * No match found (404): * Check timestamps. The click may be older than the match window. * Confirm the click was recorded (Dashboard click logs). * For Android deterministic: confirm click\_id is present in the referrer. * 429 Too Many Requests: * Implement retry/backoff and ensure you are not using the same appID/key for heavy testing from many devices. Use staging keys. * iOS clipboard prompt not appearing: * ensure `shouldUseClipboard` is enabled and that your app actually attempts to read the clipboard on first open. * check that the copy-link feature is enabled in the iOS configuration in the Dashboard, and that you copied the link to the clipboard on the screen you were redirected to # API Reference > Complete API reference for the Detour React Native SDK. ## DetourProvider [Section titled “DetourProvider”](#detourprovider) Top-level React component that initializes the Detour SDK. It manages the full link processing lifecycle: deferred link matching, Universal/App Link listening, custom scheme handling, and short-link resolution. It exposes the result via React context. On mount, the provider: 1. Attaches a runtime listener for Universal Links (iOS) / App Links (Android) received while the app is running 2. Checks for an initial URL that launched the app (Universal/App Link on cold start) 3. If the URL is a short link (single-segment web URL), resolves it to the full destination URL 4. If no initial link is found, checks if this is the first app entrance and queries the Detour API for a matching deferred link 5. Updates context state with the result via [`useDetourContext()`](#usedetourcontext) Use exactly one `DetourProvider` at the root of your app tree. The deferred link matching runs only once per app install, and subsequent app opens skip the API call entirely. Universal/App Links and custom scheme links are handled on every app open. ### Signature [Section titled “Signature”](#signature) ```ts function DetourProvider(props: { config: Config; children: React.ReactNode; }): JSX.Element; ``` ### Config fields [Section titled “Config fields”](#config-fields) * `API_KEY` required * Type `string` * Publishable API key from the Detour Dashboard. Used as a Bearer token for API calls (deferred link matching, short-link resolution) * `appID` required * Type `string` * Unique app identifier from the Detour Dashboard. Used to route requests to the correct app. * `shouldUseClipboard` * Type `boolean` Default `true` * iOS only. When `true`, the SDK reads the device clipboard and includes it in the probabilistic fingerprint for deferred link matching. May trigger the iOS system paste permission prompt. Set to `false` to skip clipboard access. * `storage` * Type [`DetourStorage` ](#detourstorage)Default `AsyncStorage` * Custom storage adapter for SDK persistence. The SDK stores a first-entrance flag used for deferred link matching. Defaults to `@react-native-async-storage/async-storage` if installed. ### Error handling [Section titled “Error handling”](#error-handling) If the match API returns an error or network calls fail, the provider sets `isLinkProcessed` to `true` and keeps `linkUrl`, `linkRoute`, and `linkType` as `null`, allowing your app to continue normally. Errors are logged to the console. ## useDetourContext [Section titled “useDetourContext”](#usedetourcontext) Hook that returns the current link processing state from the nearest `DetourProvider`. Throws an error if called outside of a `DetourProvider` tree. ### Signature [Section titled “Signature”](#signature-1) ```ts function useDetourContext(): DetourContextType; ``` ### Returned object [Section titled “Returned object”](#returned-object) * `isLinkProcessed` * Type `boolean` * `true` once the SDK has finished all link detection attempts, regardless of whether a link was found. Use this to conditionally show a loading/splash screen during cold start. * `linkUrl` * Type `string | URL | null` * The matched link, a `URL` object for full URLs or a pathname string. Can be a deferred link, Universal/App Link, or custom scheme link. `null` if no match was found or an error occurred. * `linkRoute` * Type `string | null` * Normalized in-app route extracted from the matched link. For web URLs, the first path segment (app hash) is stripped and query string is preserved (e.g. `/promo/123?utm=1`). For custom scheme links, the host and path are combined into a route. `null` if no link was matched. * `linkType` * Type `LinkType | null` * How the link arrived in the app. See [LinkType](#linktype) for values. `null` if no link was found. ## Types [Section titled “Types”](#types) ### Config [Section titled “Config”](#config) ```ts type Config = { appID: string; API_KEY: string; shouldUseClipboard?: boolean; storage?: DetourStorage; }; ``` Configuration object passed to `DetourProvider`. See [Config fields](#config-fields) for detailed descriptions. ### DetourContextType [Section titled “DetourContextType”](#detourcontexttype) ```ts type DetourContextType = { isLinkProcessed: boolean; linkUrl: string | URL | null; linkType: LinkType | null; linkRoute: string | null; }; ``` Shape of the value returned by [`useDetourContext`](#usedetourcontext). * `isLinkProcessed` * Type `boolean` * `true` once the SDK has finished all link detection attempts. `false` during cold start processing. * `linkUrl` * Type `string | URL | null` * Matched link data or `null` if no match found. * `linkRoute` * Type `string | null` * Extracted in-app route or `null`. For web URLs, the first path segment (app hash) is stripped. * `linkType` * Type `LinkType | null` * How the link arrived: `'deferred'`, `'verified'`, or `'scheme'`. `null` if no link found. ### LinkType [Section titled “LinkType”](#linktype) ```ts type LinkType = 'deferred' | 'verified' | 'scheme'; ``` Describes how the link arrived in the app: | Value | Meaning | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `'deferred'` | The link was obtained from the Detour deferred-link API. This happens when a user who does not have the app installed clicks a Detour link, installs the app, and opens it. The SDK retrieves the original link from the backend on first launch | | `'verified'` | The link arrived as a Universal Link (iOS) or App Link (Android), an HTTP/HTTPS URL that the OS verified belongs to your app and delivered directly to it | | `'scheme'` | The link arrived via a custom URL scheme (e.g. `myapp://path`). These are non-HTTP URLs that the OS routes to your app based on the registered scheme | ### DetourStorage [Section titled “DetourStorage”](#detourstorage) ```ts interface DetourStorage { getItem(key: string): Promise | string | null; setItem(key: string, value: string): Promise | void; removeItem?(key: string): Promise | void; } ``` Interface for custom storage adapters. The SDK persists a first-entrance flag used for deferred link matching (only triggers on first app install). Methods can be synchronous or asynchronous. By default, `@react-native-async-storage/async-storage` is used. Provide a custom adapter if you use a different storage solution (e.g. MMKV, Expo SecureStore). # Custom Storage > Provide a custom storage adapter (MMKV, Expo Secure Store, or your own) for the Detour React Native SDK. By default, this library attempts to use `@react-native-async-storage/async-storage` to persist the “first entrance” flag. However, you can inject your own storage implementation. This is useful if you: * Use **MMKV** for performance. * Use **Expo Secure Store** for sensitive data. ## Usage [Section titled “Usage”](#usage) To use a custom storage, pass an object implementing the `DetourStorage` interface to the `storage` property in the `config`. The interface is simple: ```ts interface DetourStorage { getItem(key: string): Promise | string | null; setItem(key: string, value: string): Promise | void; removeItem?(key: string): Promise | void; } ``` ### Example: React Native MMKV [Section titled “Example: React Native MMKV”](#example-react-native-mmkv) ```ts import { createMMKV } from "react-native-mmkv"; import { DetourProvider } from "@swmansion/react-native-detour"; const storage = createMMKV(); const mmkvAdapter = { // MMKV returns undefined if missing, but we expect null getItem: (key: string) => storage.getString(key) ?? null, setItem: (key: string, value: string) => storage.set(key, value), }; export default function App() { return ( {/* Your App */} ); } ``` ### Example: Expo Secure Store [Section titled “Example: Expo Secure Store”](#example-expo-secure-store) ```ts import * as SecureStore from "expo-secure-store"; import { DetourProvider } from "@swmansion/react-native-detour"; const secureStoreAdapter = { getItem: SecureStore.getItemAsync, setItem: SecureStore.setItemAsync, }; export default function App() { return ( {/* Your App */} ); } ``` # Installation > Install the Detour React Native SDK in Expo or bare React Native projects. Detour provides a React Native SDK (`@swmansion/react-native-detour`) compatible with Expo and bare React Native projects. ## Install [Section titled “Install”](#install) * npm ```bash npm install @swmansion/react-native-detour ``` * yarn ```bash yarn add @swmansion/react-native-detour ``` * pnpm ```bash pnpm add @swmansion/react-native-detour ``` * bun ```bash bun add @swmansion/react-native-detour ``` ### Additional dependencies [Section titled “Additional dependencies”](#additional-dependencies) * npm ```bash npm install expo-localization react-native-device-info expo-clipboard @react-native-async-storage/async-storage expo-application ``` * yarn ```bash yarn add expo-localization react-native-device-info expo-clipboard @react-native-async-storage/async-storage expo-application ``` * pnpm ```bash pnpm add expo-localization react-native-device-info expo-clipboard @react-native-async-storage/async-storage expo-application ``` * bun ```bash bun add expo-localization react-native-device-info expo-clipboard @react-native-async-storage/async-storage expo-application ``` Device info provider `react-native-device-info` is a required peer dependency. Unlike storage, the device info source is not configurable. The SDK imports the library directly to read the device model, manufacturer, OS version, and user agent for the deferred-link fingerprint. There is no adapter to plug in. Expo managed workflow After installing react-native-device-info in expo managed workflow you need to create custom development build Custom storage You can override the default persistent storage (@react-native-async-storage/async-storage) by providing an alternative storage implementation. Pass your custom storage object via the configuration settings. [See more](/docs/sdk/react-native/0.5.x/custom-storage) # Usage > Initialize DetourProvider, handle resolved links, and integrate Detour with Expo Router or React Navigation. ### Initialize provider in root of your app [Section titled “Initialize provider in root of your app”](#initialize-provider-in-root-of-your-app) ```js import { DetourProvider, type Config } from "@swmansion/react-native-detour"; const config: Config = { API_KEY: "", appID: "", shouldUseClipboard: true, }; export default function RootLayout() { return ( ); } ``` Keep credentials in .env Keep your API\_KEY and appID in .env ### Use values from context - Example with Expo Router [Section titled “Use values from context - Example with Expo Router”](#use-values-from-context---example-with-expo-router) SDK exposes `useDetourContext()` hook that returns object containing information about link and processing status. Our recommendation is to use the hook before you application renders navigation container. The SDK only looks for a deferred link during the application’s first launch after installation. Subsequent launches should be handled by Universal Links for iOS and App Links for Android. Deep links based on uri-scheme are also supported. ```js import { useDetourContext } from "@swmansion/react-native-detour"; import * as SplashScreen from "expo-splash-screen"; import { Redirect, Stack } from "expo-router"; SplashScreen.preventAutoHideAsync(); export function RootNavigator() { const { isLinkProcessed, linkRoute, linkType, linkUrl } = useDetourContext(); useEffect(() => { if (isLinkProcessed) { SplashScreen.hide(); } }, [isLinkProcessed]); if (!isLinkProcessed) { return null; } if (linkRoute) { return ; } return ; } ``` `route` is ready to use path with params for your redirection. It contains everything you added to our generated link. You can use it to redirect to other screen in you app with any navigation solution you prefer. **Example of route generation:**\ URL: `https://example-organization.godetour.link/Ar8UqUjGzG/segment1?param1=value1`\ route: `/segment1?param1=value1` ### Implementation & lifecycle details (important for integrators) [Section titled “Implementation & lifecycle details (important for integrators)”](#implementation--lifecycle-details-important-for-integrators) * On first app launch after install: * provider checks `AsyncStorage` / persisted flag to determine “first entrance”. * provider sets an in-memory flag (so remounts in the same process won’t trigger extra runs). * provider will either: * use `clickId` (if Android install referrer is present) → deterministic match * or build `ProbabilisticFingerprint` → probabilistic match * The hook (`useDetourContext()`) exposes the final state. Wait for `isLinkProcessed` before you attempt to route the user. * Matching is asynchronous. Show a loading state while it runs, then route to the matched content. * Clipboard: including `pastedLink` improves match accuracy but may surface a paste permission prompt (iOS). Make this configurable in `Config.shouldUseClipboard` for UX control. * On subsequent launches SDK checks only for universal/app links and process them ### Universal and App Links with Expo Router [Section titled “Universal and App Links with Expo Router”](#universal-and-app-links-with-expo-router) Expo Router handles Universal Links and App Links natively. When it detects that the app was opened by such a link, it redirects to the corresponding route before the SDK can process the link. **Important**: Remember that the route detected by Expo Router will contain the necessary **hash** that Detour uses for link identification. You must either match this route within your navigation structure or implement a catch-all mechanism for unassigned routes in your application. Example implementation of `app/[...unmatched].tsx`: ```js export default function UnmatchedRoute() { // Redirect immediately to place where you handle Detour response return ; } ``` This way you can rely on `linkRoute` from `useDetourContext()` which does not contain hash # Testing & Troubleshooting > Test deferred linking on iOS and Android and troubleshoot common Detour React Native SDK issues. There are known limitations for testing flow during implementations. It’s not possible to test the full flow of deferred links and universal app links unless your app is already in Play Store / App Store. ## Testing iOS [Section titled “Testing iOS”](#testing-ios) If you are still in development mode and your app is not released to App Store, you can test SDK behavior by following steps: 1. Ensure the app is uninstalled on the device. 2. Visit the link on your device by pasting or typing its URL into your device’s browser. 3. Platform will try to redirect to App Store page based on `App Store ID` you put in configuration. Your app might not exist at that moment. 4. Install you app in development mode like you normally do. 5. See the result. SDK should return the link you clicked if you managed to do it within the time window (currently 15 minutes). ## Testing Android [Section titled “Testing Android”](#testing-android) In most cases `referrer` param is not passed to Play Store unless your production app is released. That means even though your app is displayed in Play Store for internal/external test, `referrer` probably won’t work and so SDK won’t use deterministic flow. However, still you can test probabilistic flow by following steps: 1. Ensure the app is uninstalled on the device. 2. Visit the link on your device by pasting or typing its URL into your device’s browser. 3. Platform will try to redirect to Play Store based on `package name`. Your app might not exist at that moment. 4. Install you app in development mode like you normally do. 5. See the result. SDK should return the link you clicked if you managed to do it within the time window (currently 15 minutes). You want to test `referrer` api in development mode, there are some workarounds for that.\ Take a look on that medium article: [link](https://medium.com/@madicdjordje/how-to-test-the-play-store-install-referrer-api-78a63d59945b) Missing `click_id` If you encounter click\_id missing in your build see limitations section [Play Store referrer missing](/docs/platform/architecture/architecture-limitations#play-store-referrer-missing) ### Testing Android App Links [Section titled “Testing Android App Links”](#testing-android-app-links) Whether an installed app opens a Detour link depends on the Dashboard SHA-256 matching the key that signed **the build on your device**: | Installation method | SHA-256 to configure | Where to find it | | :------------------------------------------------------ | :------------------------------------- | :------------------------------------------------------------------------------------------------------- | | Google Play, any track | Play App Signing key | Play Console → **Protected with Play** → **Manage Play app signing** → *App signing key certificate* | | `eas build`, any profile, including `development` | Keystore in your EAS account | `eas credentials -p android` → profile → **Keystore** → `SHA256 Fingerprint` | | `npx expo run:android` / `npx react-native run-android` | Project’s `android/app/debug.keystore` | `keytool -list -v -keystore android/app/debug.keystore -storepass android`, or `./gradlew signingReport` | To check any build directly: `apksigner verify --print-certs app-debug.apk`. The complete procedure is described in [Finding your Android SHA-256 fingerprint](/docs/platform/fundamentals/universal-app-links#finding-your-android-sha-256-fingerprint). ## Common issues [Section titled “Common issues”](#common-issues) * No match found (404): * Check timestamps. The click may be older than the match window. * Confirm the click was recorded (Dashboard click logs). * For Android deterministic: confirm click\_id is present in the referrer. * 429 Too Many Requests: * Implement retry/backoff and ensure you are not using the same appID/key for heavy testing from many devices. Use staging keys. * iOS clipboard prompt not appearing: * ensure `shouldUseClipboard` is enabled and that your app actually attempts to read the clipboard on first open. * check that the copy-link feature is enabled in the iOS configuration in the Dashboard, and that you copied the link to the clipboard on the screen you were redirected to # API Reference > Complete API reference for the Detour React Native SDK. ## DetourProvider [Section titled “DetourProvider”](#detourprovider) Top-level React component that initializes the Detour SDK for deferred link matching and Universal/App Link handling. It manages the link processing lifecycle and exposes the result via React context. On mount, the provider: 1. Attaches a runtime listener for Universal Links (iOS) / App Links (Android) received while the app is running 2. Checks for an initial URL that launched the app (Universal/App Link on cold start) 3. If no initial link is found, checks if this is the first app entrance and queries the Detour API for a matching deferred link 4. Updates context state with the result via [`useDetourContext()`](#usedetourcontext) Use exactly one `DetourProvider` at the root of your app tree. The deferred link matching runs only once per app install, and subsequent app opens skip the API call entirely. Universal/App Links are handled on every app open. ### Signature [Section titled “Signature”](#signature) ```ts function DetourProvider(props: { config: Config; children: React.ReactNode; }): JSX.Element; ``` ### Config fields [Section titled “Config fields”](#config-fields) * `API_KEY` required * Type `string` * Publishable API key from the Detour Dashboard. Used as a Bearer token for API calls. * `appID` required * Type `string` * Unique app identifier from the Detour Dashboard. Used to route matching requests to the correct app. * `shouldUseClipboard` * Type `boolean` Default `true` * iOS only. When `true`, the SDK reads the device clipboard and includes it in the probabilistic fingerprint for deferred link matching. May trigger the iOS system paste permission prompt. Set to `false` to skip clipboard access. * `storage` * Type [`DetourStorage` ](#detourstorage)Default `AsyncStorage` * Custom storage adapter for SDK persistence. The SDK stores a first-entrance flag used for deferred link matching. Defaults to `@react-native-async-storage/async-storage` if installed. ### Error handling [Section titled “Error handling”](#error-handling) If the match API returns an error or network calls fail, the provider sets `deferredLinkProcessed` to `true` and keeps `deferredLink` and `route` as `null`, allowing your app to continue normally. Errors are logged to the console. ## useDetourContext [Section titled “useDetourContext”](#usedetourcontext) Hook that returns the current link processing state from the nearest `DetourProvider`. Throws an error if called outside of a `DetourProvider` tree. ### Signature [Section titled “Signature”](#signature-1) ```ts function useDetourContext(): DetourContextType; ``` ### Returned object [Section titled “Returned object”](#returned-object) * `deferredLinkProcessed` * Type `boolean` * `true` once the SDK has finished all link detection attempts (deferred link matching and Universal/App Link check), regardless of whether a link was found. Use this to conditionally show a loading/splash screen during cold start. * `deferredLink` * Type `string | URL | null` * The matched link, a `URL` object for full URLs (e.g. `https://yourorg.godetour.link/app-hash/promo`) or a pathname string. Can be a deferred link or a Universal/App Link. `null` if no match was found or an error occurred. * `route` * Type `string | null` * Normalized in-app route extracted from the matched link. For full URLs, the first path segment (app hash) is stripped and query string is preserved (e.g. `/promo/123?utm=1`). `null` if no link was matched. ## Types [Section titled “Types”](#types) ### Config [Section titled “Config”](#config) ```ts type Config = { appID: string; API_KEY: string; shouldUseClipboard?: boolean; storage?: DetourStorage; }; ``` Configuration object passed to `DetourProvider`. See [Config fields](#config-fields) for detailed descriptions. ### DetourContextType [Section titled “DetourContextType”](#detourcontexttype) ```ts type DetourContextType = { deferredLinkProcessed: boolean; deferredLink: string | URL | null; route: string | null; }; ``` Shape of the value returned by [`useDetourContext`](#usedetourcontext). * `deferredLinkProcessed` * Type `boolean` * `true` once the SDK has finished all link detection attempts. `false` during cold start processing. * `deferredLink` * Type `string | URL | null` * Matched link data or `null` if no match found. * `route` * Type `string | null` * Extracted in-app route or `null`. For full URLs, the first path segment (app hash) is stripped. ### DetourStorage [Section titled “DetourStorage”](#detourstorage) ```ts interface DetourStorage { getItem(key: string): Promise | string | null; setItem(key: string, value: string): Promise | void; removeItem?(key: string): Promise | void; } ``` Interface for custom storage adapters. The SDK persists a first-entrance flag used for deferred link matching (only triggers on first app install). Methods can be synchronous or asynchronous. By default, `@react-native-async-storage/async-storage` is used. Provide a custom adapter if you use a different storage solution (e.g. MMKV, Expo SecureStore). # Custom Storage > Provide a custom storage adapter (MMKV, Expo Secure Store, or your own) for the Detour React Native SDK. By default, this library attempts to use `@react-native-async-storage/async-storage` to persist the “first entrance” flag. However, you can inject your own storage implementation. This is useful if you: * Use **MMKV** for performance. * Use **Expo Secure Store** for sensitive data. ## Usage [Section titled “Usage”](#usage) To use a custom storage, pass an object implementing the `DetourStorage` interface to the `storage` property in the `config`. The interface is simple: ```ts interface DetourStorage { getItem(key: string): Promise | string | null; setItem(key: string, value: string): Promise | void; removeItem?(key: string): Promise | void; } ``` ### Example: React Native MMKV [Section titled “Example: React Native MMKV”](#example-react-native-mmkv) ```ts import { createMMKV } from "react-native-mmkv"; import { DetourProvider } from "@swmansion/react-native-detour"; const storage = createMMKV(); const mmkvAdapter = { // MMKV returns undefined if missing, but we expect null getItem: (key: string) => storage.getString(key) ?? null, setItem: (key: string, value: string) => storage.set(key, value), }; export default function App() { return ( {/* Your App */} ); } ``` ### Example: Expo Secure Store [Section titled “Example: Expo Secure Store”](#example-expo-secure-store) ```ts import * as SecureStore from "expo-secure-store"; import { DetourProvider } from "@swmansion/react-native-detour"; const secureStoreAdapter = { getItem: SecureStore.getItemAsync, setItem: SecureStore.setItemAsync, }; export default function App() { return ( {/* Your App */} ); } ``` # Installation > Install the Detour React Native SDK in Expo or bare React Native projects. Detour provides a React Native SDK (`@swmansion/react-native-detour`) compatible with Expo and bare React Native projects. ## Install [Section titled “Install”](#install) * npm ```bash npm install @swmansion/react-native-detour ``` * yarn ```bash yarn add @swmansion/react-native-detour ``` * pnpm ```bash pnpm add @swmansion/react-native-detour ``` * bun ```bash bun add @swmansion/react-native-detour ``` ### Additional dependencies [Section titled “Additional dependencies”](#additional-dependencies) * npm ```bash npm install expo-localization react-native-device-info expo-clipboard @react-native-async-storage/async-storage expo-application ``` * yarn ```bash yarn add expo-localization react-native-device-info expo-clipboard @react-native-async-storage/async-storage expo-application ``` * pnpm ```bash pnpm add expo-localization react-native-device-info expo-clipboard @react-native-async-storage/async-storage expo-application ``` * bun ```bash bun add expo-localization react-native-device-info expo-clipboard @react-native-async-storage/async-storage expo-application ``` Device info provider `react-native-device-info` is a required peer dependency. Unlike storage, the device info source is not configurable. The SDK imports the library directly to read the device model, manufacturer, OS version, and user agent for the deferred-link fingerprint. There is no adapter to plug in. Expo managed workflow After installing react-native-device-info in expo managed workflow you need to create custom development build Custom storage You can override the default persistent storage (@react-native-async-storage/async-storage) by providing an alternative storage implementation. Pass your custom storage object via the configuration settings. [See more](/docs/sdk/react-native/0.4.x/custom-storage) # Usage > Initialize DetourProvider, handle resolved links, and integrate Detour with Expo Router or React Navigation. ### Initialize provider in root of your app [Section titled “Initialize provider in root of your app”](#initialize-provider-in-root-of-your-app) ```js import { DetourProvider, type Config } from "@swmansion/react-native-detour"; const config: Config = { API_KEY: "", appID: "", shouldUseClipboard: true, }; export default function RootLayout() { return ( ); } ``` Keep credentials in .env Keep your API\_KEY and appID in .env ### Use values from context - Example with Expo Router [Section titled “Use values from context - Example with Expo Router”](#use-values-from-context---example-with-expo-router) SDK exposes `useDetourContext()` hook that returns object containing information about link and processing status. Our recommendation is to use the hook before you application renders navigation container. The SDK only looks for a deferred link during the application’s first launch after installation. Subsequent launches should be handled by Universal Links for iOS and App Links for Android. Deep links based on uri-scheme are also supported. ```js import { useDetourContext } from "@swmansion/react-native-detour"; import * as SplashScreen from "expo-splash-screen"; import { Redirect, Stack } from "expo-router"; SplashScreen.preventAutoHideAsync(); export function RootNavigator() { const { deferredLinkProcessed, deferredLink, route } = useDetourContext(); useEffect(() => { if (deferredLinkProcessed) { SplashScreen.hide(); } }, [deferredLinkProcessed]); if (!deferredLinkProcessed) { return null; } if (route) { return ; } return ; } ``` `route` is ready to use path with params for your redirection. It contains everything you added to our generated link. You can use it to redirect to other screen in you app with any navigation solution you prefer. **Example of route generation:**\ URL: `https://example-organization.godetour.link/Ar8UqUjGzG/segment1?param1=value1`\ route: `/segment1?param1=value1` ### Implementation & lifecycle details (important for integrators) [Section titled “Implementation & lifecycle details (important for integrators)”](#implementation--lifecycle-details-important-for-integrators) * On first app launch after install: * provider checks `AsyncStorage` / persisted flag to determine “first entrance”. * provider sets an in-memory flag (so remounts in the same process won’t trigger extra runs). * provider will either: * use `clickId` (if Android install referrer is present) → deterministic match * or build `ProbabilisticFingerprint` → probabilistic match * The hook (`useDetourContext()`) exposes the final state. Wait for `deferredLinkProcessed` before you attempt to route the user. * Matching is asynchronous. Show a loading state while it runs, then route to the matched content. * Clipboard: including `pastedLink` improves match accuracy but may surface a paste permission prompt (iOS). Make this configurable in `Config.shouldUseClipboard` for UX control. * On subsequent launches SDK checks only for universal/app links and process them ### Universal and App Links with Expo Router [Section titled “Universal and App Links with Expo Router”](#universal-and-app-links-with-expo-router) Expo Router handles Universal Links and App Links natively. When it detects that the app was opened by such a link, it redirects to the corresponding route before the SDK can process the link. **Important**: Remember that the route detected by Expo Router will contain the necessary **hash** that Detour uses for link identification. You must either match this route within your navigation structure or implement a catch-all mechanism for unassigned routes in your application. Example implementation of `app/[...unmatched].tsx`: ```js export default function UnmatchedRoute() { // Redirect immediately to place where you handle Detour response return ; } ``` This way you can rely on `route` from `useDetourContext()` which does not contain hash # Testing & Troubleshooting > Test deferred linking on iOS and Android and troubleshoot common Detour React Native SDK issues. There are known limitations for testing flow during implementations. It’s not possible to test the full flow of deferred links and universal app links unless your app is already in Play Store / App Store. ## Testing iOS [Section titled “Testing iOS”](#testing-ios) If you are still in development mode and your app is not released to App Store, you can test SDK behavior by following steps: 1. Ensure the app is uninstalled on the device. 2. Visit the link on your device by pasting or typing its URL into your device’s browser. 3. Platform will try to redirect to App Store page based on `App Store ID` you put in configuration. Your app might not exist at that moment. 4. Install you app in development mode like you normally do. 5. See the result. SDK should return the link you clicked if you managed to do it within the time window (currently 15 minutes). ## Testing Android [Section titled “Testing Android”](#testing-android) In most cases `referrer` param is not passed to Play Store unless your production app is released. That means even though your app is displayed in Play Store for internal/external test, `referrer` probably won’t work and so SDK won’t use deterministic flow. However, still you can test probabilistic flow by following steps: 1. Ensure the app is uninstalled on the device. 2. Visit the link on your device by pasting or typing its URL into your device’s browser. 3. Platform will try to redirect to Play Store based on `package name`. Your app might not exist at that moment. 4. Install you app in development mode like you normally do. 5. See the result. SDK should return the link you clicked if you managed to do it within the time window (currently 15 minutes). You want to test `referrer` api in development mode, there are some workarounds for that.\ Take a look on that medium article: [link](https://medium.com/@madicdjordje/how-to-test-the-play-store-install-referrer-api-78a63d59945b) Missing `click_id` If you encounter click\_id missing in your build see limitations section [Play Store referrer missing](/docs/platform/architecture/architecture-limitations#play-store-referrer-missing) ### Testing Android App Links [Section titled “Testing Android App Links”](#testing-android-app-links) Whether an installed app opens a Detour link depends on the Dashboard SHA-256 matching the key that signed **the build on your device**: | Installation method | SHA-256 to configure | Where to find it | | :------------------------------------------------------ | :------------------------------------- | :------------------------------------------------------------------------------------------------------- | | Google Play, any track | Play App Signing key | Play Console → **Protected with Play** → **Manage Play app signing** → *App signing key certificate* | | `eas build`, any profile, including `development` | Keystore in your EAS account | `eas credentials -p android` → profile → **Keystore** → `SHA256 Fingerprint` | | `npx expo run:android` / `npx react-native run-android` | Project’s `android/app/debug.keystore` | `keytool -list -v -keystore android/app/debug.keystore -storepass android`, or `./gradlew signingReport` | To check any build directly: `apksigner verify --print-certs app-debug.apk`. The complete procedure is described in [Finding your Android SHA-256 fingerprint](/docs/platform/fundamentals/universal-app-links#finding-your-android-sha-256-fingerprint). ## Common issues [Section titled “Common issues”](#common-issues) * No match found (404): * Check timestamps. The click may be older than the match window. * Confirm the click was recorded (Dashboard click logs). * For Android deterministic: confirm click\_id is present in the referrer. * 429 Too Many Requests: * Implement retry/backoff and ensure you are not using the same appID/key for heavy testing from many devices. Use staging keys. * iOS clipboard prompt not appearing: * ensure `shouldUseClipboard` is enabled and that your app actually attempts to read the clipboard on first open. * check that the copy-link feature is enabled in the iOS configuration in the Dashboard, and that you copied the link to the clipboard on the screen you were redirected to # API Reference > Complete API reference for the Detour React Native SDK. ## DetourProvider [Section titled “DetourProvider”](#detourprovider) Top-level React component that initializes the Detour SDK for deferred link matching. It manages the matching lifecycle and exposes the result via React context. On mount, the provider: 1. Checks if this is the first app entrance using persisted state in AsyncStorage 2. If first entrance, collects a device fingerprint and queries the Detour API for a matching deferred link 3. On Android, attempts deterministic matching using the install referrer `click_id` before falling back to probabilistic fingerprinting 4. Updates context state with the result via [`useDetourContext()`](#usedetourcontext) Use exactly one `DetourProvider` at the root of your app tree. The matching logic runs only once per app install, and subsequent app opens skip the API call entirely. ### Signature [Section titled “Signature”](#signature) ```ts function DetourProvider(props: { config: Config; children: React.ReactNode; }): JSX.Element; ``` ### Config fields [Section titled “Config fields”](#config-fields) * `API_KEY` required * Type `string` * Publishable API key from the Detour Dashboard. Used as a Bearer token for API calls. * `appID` required * Type `string` * Unique app identifier from the Detour Dashboard. Used to route matching requests to the correct app. * `shouldUseClipboard` * Type `boolean` Default `true` * iOS only. When `true`, the SDK reads the device clipboard and includes it in the probabilistic fingerprint for deferred link matching. May trigger the iOS system paste permission prompt. Set to `false` to skip clipboard access. ### Error handling [Section titled “Error handling”](#error-handling) If the match API returns an error or network calls fail, the provider sets `deferredLinkProcessed` to `true` and keeps `deferredLink` and `route` as `null`, allowing your app to continue normally. Errors are logged to the console. ## useDetourContext [Section titled “useDetourContext”](#usedetourcontext) Hook that returns the current deferred link matching state from the nearest `DetourProvider`. Throws an error if called outside of a `DetourProvider` tree. ### Signature [Section titled “Signature”](#signature-1) ```ts function useDetourContext(): DeferredLinkContext; ``` ### Returned object [Section titled “Returned object”](#returned-object) * `deferredLinkProcessed` * Type `boolean` * `true` once the SDK has finished the deferred link matching attempt, regardless of whether a link was found. Use this to conditionally show a loading/splash screen during cold start. * `deferredLink` * Type `string | URL | null` * The matched link, a `URL` object for full URLs (e.g. `https://yourorg.godetour.link/app-hash/promo`) or a pathname string. `null` if no match was found or an error occurred. * `route` * Type `string | null` * Normalized in-app route extracted from the matched link. For full URLs, the first path segment (app hash) is stripped and query string is preserved (e.g. `/promo/123?utm=1`). `null` if no link was matched. ## Types [Section titled “Types”](#types) ### Config [Section titled “Config”](#config) ```ts type Config = { appID: string; API_KEY: string; shouldUseClipboard?: boolean; }; ``` Configuration object passed to `DetourProvider`. See [Config fields](#config-fields) for detailed descriptions. ### DeferredLinkContext [Section titled “DeferredLinkContext”](#deferredlinkcontext) ```ts type DeferredLinkContext = { deferredLinkProcessed: boolean; deferredLink: string | URL | null; route: string | null; }; ``` Shape of the value returned by [`useDetourContext`](#usedetourcontext). * `deferredLinkProcessed` * Type `boolean` * `true` once the SDK has finished all link detection attempts. `false` during cold start processing. * `deferredLink` * Type `string | URL | null` * Matched link data or `null` if no match found. * `route` * Type `string | null` * Extracted in-app route or `null`. For full URLs, the first path segment (app hash) is stripped. # Installation > Install the Detour React Native SDK in Expo or bare React Native projects. Detour provides a React Native SDK (`@swmansion/react-native-detour`) compatible with Expo and bare React Native projects. ## Install [Section titled “Install”](#install) * npm ```bash npm install @swmansion/react-native-detour ``` * yarn ```bash yarn add @swmansion/react-native-detour ``` * pnpm ```bash pnpm add @swmansion/react-native-detour ``` * bun ```bash bun add @swmansion/react-native-detour ``` ### Additional dependencies [Section titled “Additional dependencies”](#additional-dependencies) * npm ```bash npm install expo-localization react-native-device-info expo-clipboard @react-native-async-storage/async-storage expo-application ``` * yarn ```bash yarn add expo-localization react-native-device-info expo-clipboard @react-native-async-storage/async-storage expo-application ``` * pnpm ```bash pnpm add expo-localization react-native-device-info expo-clipboard @react-native-async-storage/async-storage expo-application ``` * bun ```bash bun add expo-localization react-native-device-info expo-clipboard @react-native-async-storage/async-storage expo-application ``` Device info provider `react-native-device-info` is a required peer dependency, and the device info source is not configurable. The SDK imports the library directly to read the device model, manufacturer, OS version, and user agent for the deferred-link fingerprint. There is no adapter to plug in. Expo managed workflow After installing react-native-device-info in expo managed workflow you need to create custom development build # Usage > Initialize DetourProvider, handle resolved links, and integrate Detour with Expo Router or React Navigation. ### Initialize provider in root of your app [Section titled “Initialize provider in root of your app”](#initialize-provider-in-root-of-your-app) ```js import { DetourProvider, type Config } from "@swmansion/react-native-detour"; const config: Config = { API_KEY: "", appID: "", shouldUseClipboard: true, }; export default function RootLayout() { return ( ); } ``` Keep credentials in .env Keep your API\_KEY and appID in .env ### Use values from context - Example with Expo Router [Section titled “Use values from context - Example with Expo Router”](#use-values-from-context---example-with-expo-router) SDK exposes `useDetourContext()` hook that returns object containing information about deferred link and processing status. Our recommendation is to use the hook before you application renders navigation container. The SDK only looks for a deferred link during the application’s first launch after installation. Subsequent launches should be handled by Universal Links for iOS and App Links for Android. ```js import { useDetourContext } from "@swmansion/react-native-detour"; import * as SplashScreen from "expo-splash-screen"; import { Redirect, Stack } from "expo-router"; SplashScreen.preventAutoHideAsync(); export function RootNavigator() { const { deferredLinkProcessed, deferredLink, route } = useDetourContext(); useEffect(() => { if (deferredLinkProcessed) { SplashScreen.hide(); } }, [deferredLinkProcessed]); if (!deferredLinkProcessed) { return null; } if (route) { return ; } return ; } ``` ### Implementation & lifecycle details (important for integrators) [Section titled “Implementation & lifecycle details (important for integrators)”](#implementation--lifecycle-details-important-for-integrators) * On first app launch after install: * provider checks `AsyncStorage` / persisted flag to determine “first entrance”. * provider sets an in-memory flag (so remounts in the same process won’t trigger extra runs). * provider will either: * use `clickId` (if Android install referrer is present) → deterministic match * or build `ProbabilisticFingerprint` → probabilistic match * The hook (`useDetourContext()`) exposes the final state. Wait for `deferredLinkProcessed` before you attempt to route the user. * Matching is asynchronous. Show a loading state while it runs, then route to the matched content. * Clipboard: including `pastedLink` improves match accuracy but may surface a paste permission prompt (iOS). Make this configurable in `Config.shouldUseClipboard` for UX control. ### Universal and App Links handling [Section titled “Universal and App Links handling”](#universal-and-app-links-handling) Expo Router handles Universal Links and App Links natively. When it detects that the app was opened by such a link, it redirects to the corresponding route before the SDK can process the link. If your are not using Expo Router you need to process universal/app link which opened your app manually or upgrade to version 0.4.0 which gives you universal/app link value in same response what is used for deferred links. **Important**: Remember that the route detected by Expo Router will contain the necessary **hash** that Detour uses for link identification. You must either match this route within your navigation structure or implement a catch-all mechanism for unassigned routes in your application. Example implementation of `app/[...unmatched].tsx`: ```js export default function UnmatchedRoute() { // 1. Get the current path that Expo Router is trying to render // Example: "/abcd-1234/product/5" const pathname = usePathname(); // 2. Parse it const cleanPath = stripHashAndParseUrl(pathname); // 3. Safety Check: if (cleanPath === pathname || cleanPath === "/") { return ; } // 4. Immediate Redirect to the clean path // e.g. Redirects to "/product/5" return ; } ``` # Testing & Troubleshooting > Test deferred linking on iOS and Android and troubleshoot common Detour React Native SDK issues. There are known limitations for testing flow during implementations. It’s not possible to test the full flow of deferred links and universal app links unless your app is already in Play Store / App Store. ## Testing iOS [Section titled “Testing iOS”](#testing-ios) If you are still in development mode and your app is not released to App Store, you can test SDK behavior by following steps: 1. Ensure the app is uninstalled on the device. 2. Visit the link on your device by pasting or typing its URL into your device’s browser. 3. Platform will try to redirect to App Store page based on `App Store ID` you put in configuration. Your app might not exist at that moment. 4. Install you app in development mode like you normally do. 5. See the result. SDK should return the link you clicked if you managed to do it within the time window (currently 15 minutes). ## Testing Android [Section titled “Testing Android”](#testing-android) In most cases `referrer` param is not passed to Play Store unless your production app is released. That means even though your app is displayed in Play Store for internal/external test, `referrer` probably won’t work and so SDK won’t use deterministic flow. However, still you can test probabilistic flow by following steps: 1. Ensure the app is uninstalled on the device. 2. Visit the link on your device by pasting or typing its URL into your device’s browser. 3. Platform will try to redirect to Play Store based on `package name`. Your app might not exist at that moment. 4. Install you app in development mode like you normally do. 5. See the result. SDK should return the link you clicked if you managed to do it within the time window (currently 15 minutes). You want to test `referrer` api in development mode, there are some workarounds for that.\ Take a look on that medium article: [link](https://medium.com/@madicdjordje/how-to-test-the-play-store-install-referrer-api-78a63d59945b) Missing `click_id` If you encounter click\_id missing in your build see limitations section [Play Store referrer missing](/docs/platform/architecture/architecture-limitations#play-store-referrer-missing) ### Testing Android App Links [Section titled “Testing Android App Links”](#testing-android-app-links) Whether an installed app opens a Detour link depends on the Dashboard SHA-256 matching the key that signed **the build on your device**: | Installation method | SHA-256 to configure | Where to find it | | :------------------------------------------------------ | :------------------------------------- | :------------------------------------------------------------------------------------------------------- | | Google Play, any track | Play App Signing key | Play Console → **Protected with Play** → **Manage Play app signing** → *App signing key certificate* | | `eas build`, any profile, including `development` | Keystore in your EAS account | `eas credentials -p android` → profile → **Keystore** → `SHA256 Fingerprint` | | `npx expo run:android` / `npx react-native run-android` | Project’s `android/app/debug.keystore` | `keytool -list -v -keystore android/app/debug.keystore -storepass android`, or `./gradlew signingReport` | To check any build directly: `apksigner verify --print-certs app-debug.apk`. The complete procedure is described in [Finding your Android SHA-256 fingerprint](/docs/platform/fundamentals/universal-app-links#finding-your-android-sha-256-fingerprint). ## Common issues [Section titled “Common issues”](#common-issues) * No match found (404): * Check timestamps. The click may be older than the match window. * Confirm the click was recorded (Dashboard click logs). * For Android deterministic: confirm click\_id is present in the referrer. * 429 Too Many Requests: * Implement retry/backoff and ensure you are not using the same appID/key for heavy testing from many devices. Use staging keys. * iOS clipboard prompt not appearing: * ensure `shouldUseClipboard` is enabled and that your app actually attempts to read the clipboard on first open. * check that the copy-link feature is enabled in the iOS configuration in the Dashboard, and that you copied the link to the clipboard on the screen you were redirected to # API Reference > Complete API reference for the Detour iOS SDK. ## Detour [Section titled “Detour”](#detour) Main SDK singleton, accessed via `Detour.shared`. All methods are `@MainActor`. ### Link processing methods [Section titled “Link processing methods”](#link-processing-methods) #### `processLink(_:)` (synchronous) [Section titled “processLink(\_:) (synchronous)”](#processlink_-synchronous) ```swift func processLink(_ url: URL) -> DetourResult ``` Converts a URL into a `DetourResult` with parsed route, pathname, params, and link type. Does not resolve short links. Returns an empty result for infrastructure URLs (e.g. Expo dev client URLs). #### `processLink(_:config:)` (async, URL) [Section titled “processLink(\_:config:) (async, URL)”](#processlink_config-async-url) ```swift func processLink(_ url: URL, config: DetourConfig?) async -> DetourResult ``` Processes a URL and optionally resolves short links when `config` is provided. Use this when you receive a URL at runtime (e.g. from `onOpenURL` in SwiftUI or `application(_:open:)` in UIKit). #### `processLink(_:config:)` (async, String) [Section titled “processLink(\_:config:) (async, String)”](#processlink_config-async-string) ```swift func processLink(_ rawLink: String, config: DetourConfig? = nil) async -> DetourResult ``` Same as the URL variant but accepts a raw link string. Handles both full URLs and plain path strings. When `config` is provided and the URL is a single-segment web path (short link), the SDK resolves it to the full destination URL. #### `resolveInitialLink(config:launchOptions:completion:)` (UIKit) [Section titled “resolveInitialLink(config:launchOptions:completion:) (UIKit)”](#resolveinitiallinkconfiglaunchoptionscompletion-uikit) ```swift func resolveInitialLink( config: DetourConfig, launchOptions: [UIApplication.LaunchOptionsKey: Any]?, completion: @escaping @Sendable (DetourResult) -> Void ) ``` Resolves the initial link that launched the app in UIKit-based apps. Call from `application(_:didFinishLaunchingWithOptions:)`. Checks in priority order: 1. **Launch URL** from `launchOptions` 2. **Universal Link** from user activities in `launchOptions` 3. **Deferred link** from the Detour API (first install only) This method is session-guarded, so it processes only once per app session. It also mounts analytics automatically. #### `resolveInitialLink(config:connectionOptions:completion:)` (SceneDelegate / SwiftUI) [Section titled “resolveInitialLink(config:connectionOptions:completion:) (SceneDelegate / SwiftUI)”](#resolveinitiallinkconfigconnectionoptionscompletion-scenedelegate--swiftui) ```swift func resolveInitialLink( config: DetourConfig, connectionOptions: UIScene.ConnectionOptions, completion: @escaping @Sendable (DetourResult) -> Void ) ``` Same as the UIKit variant but uses `UIScene.ConnectionOptions` for scene-based apps. Call from `scene(_:willConnectTo:options:)` or the SwiftUI app lifecycle. ### Analytics methods [Section titled “Analytics methods”](#analytics-methods) #### `mountAnalytics(config:)` [Section titled “mountAnalytics(config:)”](#mountanalyticsconfig) ```swift func mountAnalytics(config: DetourConfig) ``` Mounts the analytics system. Called automatically by `resolveInitialLink`. Only call manually if you use `processLink` without `resolveInitialLink`. #### `unmountAnalytics()` [Section titled “unmountAnalytics()”](#unmountanalytics) ```swift func unmountAnalytics() ``` Unmounts the analytics system and stops listening for events. ### Utility methods [Section titled “Utility methods”](#utility-methods) #### `resetSession(allowDeferredRetry:)` [Section titled “resetSession(allowDeferredRetry:)”](#resetsessionallowdeferredretry) ```swift func resetSession(allowDeferredRetry: Bool = false) ``` Resets the session state so `resolveInitialLink` can run again. When `allowDeferredRetry` is `true`, also resets the first-entrance flag, which is useful for local testing of the deferred link flow. ## DetourConfig [Section titled “DetourConfig”](#detourconfig) SDK configuration. All properties are immutable and the struct conforms to `Sendable`. ```swift public struct DetourConfig: Sendable { public let apiKey: String public let appID: String public let shouldUseClipboard: Bool public let linkProcessingMode: LinkProcessingMode } ``` ### Initializer [Section titled “Initializer”](#initializer) ```swift DetourConfig( apiKey: String, appID: String, shouldUseClipboard: Bool = true, linkProcessingMode: LinkProcessingMode = .all ) ``` * `apiKey` required * Type `String` * Publishable API key from the Detour Dashboard. Used as a Bearer token for all API calls (deferred link matching, short-link resolution, analytics) * `appID` required * Type `String` * Unique app identifier from the Detour Dashboard. Used to route API requests to the correct app. * `shouldUseClipboard` * Type `Bool` Default `true` * When `true`, the SDK reads the device clipboard and includes it in the probabilistic fingerprint for deferred link matching. May trigger the iOS system paste permission prompt. Set to `false` to skip clipboard access. * `linkProcessingMode` * Type [`LinkProcessingMode` ](#linkprocessingmode)Default `.all` * Controls which link sources the SDK processes. See [LinkProcessingMode](#linkprocessingmode) for details. ## DetourResult [Section titled “DetourResult”](#detourresult) Result returned by all link processing methods. Contains a processing flag and an optional parsed link. ```swift public struct DetourResult: Sendable { public let processed: Bool public let link: DetourLink? } ``` * `processed` * Type `Bool` * `true` when the SDK has finished processing the link (regardless of whether a link was found) * `link` * Type [`DetourLink?`](#detourlink) * Parsed link data, or `nil` if no link was matched. ### Convenience computed properties [Section titled “Convenience computed properties”](#convenience-computed-properties) * `route` * Type `String?` * Shorthand for `link?.route`, the full in-app route including query string. * `pathname` * Type `String?` * Shorthand for `link?.pathname`, the path without query string. * `params` * Type `[String: String]` * Shorthand for `link?.params ?? [:]`, the parsed query parameters. * `linkType` * Type [`LinkType?`](#linktype) * Shorthand for `link?.type`, which says how the link arrived. * `linkURL` * Type `URL?` * Shorthand for `link.flatMap { URL(string: $0.url) }`, the original URL as a `URL` object, `nil` when there is no link. ### Factory method [Section titled “Factory method”](#factory-method) ```swift static func empty() -> DetourResult ``` Returns a result with `processed: true` and `link: nil`. Used internally when no link is matched. ## DetourLink [Section titled “DetourLink”](#detourlink) Parsed link data returned inside a `DetourResult`. ```swift public struct DetourLink: Sendable { public let url: String public let route: String public let pathname: String public let params: [String: String] public let type: LinkType } ``` * `url` * Type `String` * Full URL string that was matched or opened. * `route` * Type `String` * In-app route for navigation, as a path with query string, first path segment (app hash) stripped (e.g. `/promo/123?utm=1`) * `pathname` * Type `String` * Route path without query string (e.g. `/promo/123`) * `params` * Type `[String: String]` * Parsed query parameters as key-value pairs. * `type` * Type [`LinkType`](#linktype) * How the link arrived in the app. ## LinkProcessingMode [Section titled “LinkProcessingMode”](#linkprocessingmode) ```swift public enum LinkProcessingMode: String, Sendable { case all case webOnly = "web-only" case deferredOnly = "deferred-only" } ``` Controls which link sources are processed: | Value | Behavior | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `.all` (default) | Processes all link sources: deferred links from the Detour API, Universal Links (verified HTTP/HTTPS URLs), and custom scheme deep links | | `.webOnly` | Same as `.all` but filters out custom scheme links. Only deferred links and Universal Links (HTTP/HTTPS) are processed | | `.deferredOnly` | Only processes deferred links from the Detour API. Skips Universal Link and scheme link detection in `resolveInitialLink`. Recommended when your app handles runtime links separately | ## LinkType [Section titled “LinkType”](#linktype) ```swift public enum LinkType: String, Codable, Sendable { case deferred case verified case scheme } ``` Describes how the link arrived in the app: | Value | Meaning | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `.deferred` | The link was obtained from the Detour deferred-link API. This happens when a user who does not have the app installed clicks a Detour link, installs the app, and opens it. The SDK retrieves the original link from the backend on first launch | | `.verified` | The link arrived as a Universal Link, an HTTP/HTTPS URL that iOS verified belongs to your app via the apple-app-site-association file and delivered directly to it | | `.scheme` | The link arrived via a custom URL scheme (e.g. `myapp://path`). These are non-HTTP URLs that iOS routes to your app based on the registered scheme | ## DetourAnalytics [Section titled “DetourAnalytics”](#detouranalytics) Analytics module for logging events to the Detour backend. Accessed via static methods on the `DetourAnalytics` class. Analytics are automatically mounted when calling `resolveInitialLink`. Mount manually only if you use `processLink` without `resolveInitialLink`. The SDK automatically logs an `app_open` retention event when analytics are first mounted in a session. ### Methods [Section titled “Methods”](#methods) #### `mount(config:)` [Section titled “mount(config:)”](#mountconfig) ```swift static func mount(config: DetourConfig) ``` Mounts the analytics system and subscribes to event emissions. Guards against duplicate mounts. #### `unmount()` [Section titled “unmount()”](#unmount) ```swift static func unmount() ``` Unmounts the analytics system and stops listening for events. #### `logEvent(_:data:)` (DetourEventName) [Section titled “logEvent(\_:data:) (DetourEventName)”](#logevent_data-detoureventname) ```swift static func logEvent(_ eventName: DetourEventName, data: [String: Any]? = nil) ``` Records a user action using a predefined event name. The optional `data` parameter accepts a dictionary of arbitrary metadata. #### `logEvent(_:data:)` (String) [Section titled “logEvent(\_:data:) (String)”](#logevent_data-string) ```swift static func logEvent(_ eventName: String, data: [String: Any]? = nil) ``` Records a user action using a custom event name string. #### `logRetention(_:)` [Section titled “logRetention(\_:)”](#logretention_) ```swift static func logRetention(_ eventName: String) ``` Records a retention/lifecycle event for user engagement tracking. ## DetourEventName [Section titled “DetourEventName”](#detoureventname) | Category | Event Name | Raw value | | :---------- | :---------------------------- | :------------------------------ | | **General** | `.login` | `login` | | | `.search` | `search` | | | `.share` | `share` | | | `.signUp` | `sign_up` | | | `.tutorialBegin` | `tutorial_begin` | | | `.tutorialComplete` | `tutorial_complete` | | | `.reEngage` | `re_engage` | | | `.invite` | `invite` | | | `.openedFromPushNotification` | `opened_from_push_notification` | | **Sales** | `.addPaymentInfo` | `add_payment_info` | | | `.addShippingInfo` | `add_shipping_info` | | | `.addToCart` | `add_to_cart` | | | `.removeFromCart` | `remove_from_cart` | | | `.refund` | `refund` | | | `.viewItem` | `view_item` | | | `.beginCheckout` | `begin_checkout` | | | `.purchase` | `purchase` | | | `.adImpression` | `ad_impression` | ## Related pages [Section titled “Related pages”](#related-pages) * [Usage](/docs/sdk/ios/1.0.x/sdk-usage) * [Examples](/docs/sdk/ios/1.0.x/examples) * [Testing & Troubleshooting](/docs/sdk/ios/1.0.x/testing) # Custom Storage > Provide a custom storage adapter (Keychain or your own) for the Detour iOS SDK. In iOS SDK `1.0.2`, custom storage injection is not exposed in public API. SDK uses internal `UserDefaults` keys for: * first-entrance flag: `DetourFirstEntranceFlag` * analytics device ID: `Detour_deviceId` ## What this means in practice [Section titled “What this means in practice”](#what-this-means-in-practice) * You cannot pass a custom storage adapter in `DetourConfig` in `1.0.2`. * Data persists across app restarts automatically through `UserDefaults`. * For testing deferred flow repeatedly, use: ```swift Detour.shared.resetSession(allowDeferredRetry: true) ``` This resets in-memory session guard and clears first-entrance state used by deferred matching. ## Related pages [Section titled “Related pages”](#related-pages) * [Usage](/docs/sdk/ios/1.0.x/sdk-usage) * [API Reference](/docs/sdk/ios/1.0.x/api-reference) * [Testing & Troubleshooting](/docs/sdk/ios/1.0.x/testing) # Examples > Runnable Detour iOS example apps for SwiftUI and UIKit integration. The iOS SDK repository ships one runnable app that covers all three link types. Open it in Xcode, fill in the placeholders, run it, and copy what you need. The card below links to the example’s `README.md` on GitHub, with the setup steps and full source. ## Example catalog [Section titled “Example catalog”](#example-catalog) [ExampleUsage ](https://github.com/software-mansion-labs/ios-detour/blob/1.0.2/ExampleUsage/README.md)A SwiftUI app that handles links on cold start and at runtime, with analytics calls. Fill in the placeholders and run it. ## Recommended user path [Section titled “Recommended user path”](#recommended-user-path) 1. Complete [Installation](/docs/sdk/ios/1.0.x/sdk-installation). 2. Read [Usage](/docs/sdk/ios/1.0.x/sdk-usage) for lifecycle and link-processing model. 3. Adapt the template files to your bundle ID, domain, and route mapping. 4. Validate behavior with [Testing & Troubleshooting](/docs/sdk/ios/1.0.x/testing). # Installation > Install the Detour iOS SDK via Swift Package Manager or CocoaPods. Detour provides a native iOS SDK (`Detour`) for deferred links, universal links, and custom scheme links. Beta iOS SDK `1.0.2` is currently in beta. APIs and integration details may still change before stable release. Before installation, make sure your app is created in the Detour Dashboard and you have: * `appID` from **API configuration** * publishable `apiKey` from **API configuration** * iOS link integration snippets from **App configuration** ## Requirements [Section titled “Requirements”](#requirements) * iOS `13+` * Swift `5.5+` ## Install with Swift Package Manager [Section titled “Install with Swift Package Manager”](#install-with-swift-package-manager) In Xcode: 1. Open your project. 2. Go to `File > Add Package Dependencies...` 3. Enter repository URL: * `https://github.com/software-mansion-labs/ios-detour` 4. Add product `Detour` to your app target. Or in `Package.swift`: ```swift .package(url: "https://github.com/software-mansion-labs/ios-detour", from: "1.0.2") ``` ## Install with CocoaPods [Section titled “Install with CocoaPods”](#install-with-cocoapods) In your `Podfile`: ```ruby platform :ios, '13.0' target 'YourAppTarget' do use_frameworks! pod 'Detour', '1.0.2' end ``` Then run: ```bash pod install ``` ## Prepare app configuration [Section titled “Prepare app configuration”](#prepare-app-configuration) ```swift import Detour let detourConfig = DetourConfig( apiKey: "", appID: "", shouldUseClipboard: true, linkProcessingMode: .all ) ``` ## Configure link capabilities [Section titled “Configure link capabilities”](#configure-link-capabilities) Add both integration pieces from Dashboard/App config: * custom URL scheme in `Info.plist` (`CFBundleURLTypes`) * associated domains in entitlements (`applinks:`) Without those entries, iOS will not dispatch runtime links to your app. ## Privacy manifest [Section titled “Privacy manifest”](#privacy-manifest) The `1.0.x` line ships no `PrivacyInfo.xcprivacy`. Apple expects the app bundle to declare what every third-party SDK collects, so on this version you add the declarations to your own app’s manifest. `1.1.1` and later ship one with the package. The data types to declare, and the matching App Store Connect answers, are listed in [Privacy](/docs/sdk/ios/privacy). ## Next steps [Section titled “Next steps”](#next-steps) 1. Wire startup/runtime handling in [Usage](/docs/sdk/ios/1.0.x/sdk-usage). 2. Use the runnable template from [Examples](/docs/sdk/ios/1.0.x/examples). # Usage > Initialize Detour, resolve the initial link on cold start, and handle runtime links in your iOS app. ## Define shared config [Section titled “Define shared config”](#define-shared-config) ```swift import Detour let detourConfig = DetourConfig( apiKey: "", appID: "", shouldUseClipboard: true, linkProcessingMode: .all ) ``` ## Resolve initial link at startup [Section titled “Resolve initial link at startup”](#resolve-initial-link-at-startup) Use `resolveInitialLink` once during cold start (`UIApplicationDelegate` flow): ```swift func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { Detour.shared.resolveInitialLink(config: detourConfig, launchOptions: launchOptions) { result in if let link = result.link { // Navigate using link.route / link.pathname / link.params } } return true } ``` If you use scene lifecycle, call the overload with `connectionOptions` in `scene(_:willConnectTo:options:)`. If you want complete, runnable integration with AppDelegate + SwiftUI UI state, go to [Examples](/docs/sdk/ios/1.0.x/examples). ## Handle runtime links [Section titled “Handle runtime links”](#handle-runtime-links) ### Custom scheme links (`myapp://...`) [Section titled “Custom scheme links (myapp://...)”](#custom-scheme-links-myapp) ```swift func application( _ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:] ) -> Bool { Task { @MainActor in let result = await Detour.shared.processLink(url, config: detourConfig) // handle result.link } return true } ``` ### Universal links (`https://...`) [Section titled “Universal links (https://...)”](#universal-links-https) ```swift func application( _ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void ) -> Bool { guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL else { return false } Task { @MainActor in let result = await Detour.shared.processLink(url, config: detourConfig) // handle result.link } return true } ``` ## `DetourResult` shape [Section titled “DetourResult shape”](#detourresult-shape) `processLink` and `resolveInitialLink` return: * `processed` Link processing finished. * `link` Type `DetourLink?` Helpers exposed on `DetourResult`: * `route` * `pathname` * `params` * `linkType` * `linkURL` ## Choose `linkProcessingMode` [Section titled “Choose linkProcessingMode”](#choose-linkprocessingmode) `DetourConfig.linkProcessingMode` controls which link sources are handled: | Value | Universal links | Deferred links | Custom scheme links | | ---------------- | --------------- | -------------- | ------------------- | | `.all` (default) | Yes | Yes | Yes | | `.webOnly` | Yes | Yes | No | | `.deferredOnly` | No | Yes | No | Use `.deferredOnly` when runtime links are handled by your own routing layer and Detour should resolve only deferred links. ## Analytics [Section titled “Analytics”](#analytics) `resolveInitialLink(...)` mounts analytics automatically. You can also mount explicitly: ```swift Detour.shared.mountAnalytics(config: detourConfig) ``` Emit events from app code: ```swift DetourAnalytics.logEvent(.addToCart, data: ["sku": "abc"]) DetourAnalytics.logEvent("promo_banner_tap", data: ["placement": "home_top"]) DetourAnalytics.logRetention("day_7_return") ``` ## Related pages [Section titled “Related pages”](#related-pages) * [Examples](/docs/sdk/ios/1.0.x/examples) * [API Reference](/docs/sdk/ios/1.0.x/api-reference) * [Custom Storage](/docs/sdk/ios/1.0.x/custom-storage) * [Testing & Troubleshooting](/docs/sdk/ios/1.0.x/testing) # Testing & Troubleshooting > Test deferred linking and Universal Links on iOS and troubleshoot common Detour iOS SDK issues. Testing deferred links on iOS has platform constraints (App Store redirect chain, first-launch-only semantics). ## Deferred flow constraints [Section titled “Deferred flow constraints”](#deferred-flow-constraints) * Deferred matching is expected on first launch after install. * SDK stores the first-entrance flag in `UserDefaults`. Later launches return an empty result for the deferred fallback. * Session handling is single-pass. Repeated startup resolution in one session is skipped. ## Recommended deferred test flow [Section titled “Recommended deferred test flow”](#recommended-deferred-test-flow) 1. Uninstall the app from the device or simulator. 2. Open a Detour link. 3. Install and launch the app. 4. Verify `result.link?.type == .deferred` and expected route mapping. 5. Reopen app and confirm deferred link is not consumed again. For local iteration without reinstall every time: ```swift Detour.shared.resetSession(allowDeferredRetry: true) ``` ## Testing runtime links [Section titled “Testing runtime links”](#testing-runtime-links) ### Simulator custom scheme [Section titled “Simulator custom scheme”](#simulator-custom-scheme) ```bash xcrun simctl openurl booted "myapp://products/42" ``` Expected: runtime `processLink` resolves to `.scheme`. ### Simulator universal link [Section titled “Simulator universal link”](#simulator-universal-link) ```bash xcrun simctl openurl booted "https:///products/42" ``` Expected: runtime `processLink` resolves to `.verified` when associated domains are configured. ## Troubleshooting checklist [Section titled “Troubleshooting checklist”](#troubleshooting-checklist) * No link handled on cold start * ensure `resolveInitialLink(config:launchOptions:)` (or `connectionOptions` overload) is called once at startup. * Universal links open Safari instead of app * verify associated domains entitlement and Dashboard domain configuration. * Scheme links do nothing * verify `CFBundleURLTypes` in `Info.plist`. * Expected scheme link is ignored * check `linkProcessingMode`. `.webOnly` and `.deferredOnly` intentionally skip non-web URL-like links. * Analytics events missing in Dashboard * ensure analytics is mounted (`resolveInitialLink` mounts it automatically, and a manual flow can call `mountAnalytics`). ## Debugging [Section titled “Debugging”](#debugging) SDK logs through Apple unified logging under subsystem `com.swmansion.detour`. On simulator/device: ```bash xcrun simctl spawn booted log stream --predicate 'subsystem == "com.swmansion.detour"' ``` For end-to-end setup template, use [Examples](/docs/sdk/ios/1.0.x/examples). # API Reference > Complete API reference for the Detour Android SDK. ## Detour [Section titled “Detour”](#detour) Main SDK singleton. All methods are `@JvmStatic` for Java interoperability. ### Methods [Section titled “Methods”](#methods) #### `initialize` [Section titled “initialize”](#initialize) ```kotlin Detour.initialize(context: Context, config: DetourConfig): Unit ``` Initializes the SDK. Must be called once before any other SDK method, usually in `Application.onCreate()` or at the start of your launcher `Activity`. Sets up the internal API client, fingerprint collector, install referrer helper, and analytics subsystem. If `apiKey` or `appId` is blank, the SDK logs an error and does not initialize, and it does not throw. #### `processLink` [Section titled “processLink”](#processlink) ```kotlin suspend fun Detour.processLink(intent: Intent): LinkResult ``` Main entry point for link processing. Automatically detects the link source from the given `Intent` and returns the appropriate result. Checks in priority order: 1. **App Links**: HTTP/HTTPS URLs from verified domains (returns `LinkType.VERIFIED`) 2. **Scheme links**: custom scheme deep links, if `linkProcessingMode` is `ALL` (returns `LinkType.SCHEME`) 3. **Deferred links**: queries the Detour API on first launch (returns `LinkType.DEFERRED`) If a web URL has a single path segment (short link), the SDK resolves it to the full destination URL before returning. #### `getDeferredLink` [Section titled “getDeferredLink”](#getdeferredlink) ```kotlin suspend fun Detour.getDeferredLink(): LinkResult ``` Resolves deferred links only, ignoring App Links and scheme links. Useful when you handle other link types separately. Returns `LinkResult.NotFirstLaunch` on subsequent calls within the same session, because deferred matching runs only once per app install. #### `resolveShortLink` [Section titled “resolveShortLink”](#resolveshortlink) ```kotlin suspend fun Detour.resolveShortLink(url: String): ShortLinkResponse? ``` Resolves a Detour short link URL to its full destination. Returns `null` if resolution fails or the URL is not a valid short link. ## DetourDelegate [Section titled “DetourDelegate”](#detourdelegate) Lifecycle-aware helper that wraps `Detour.processLink()` for `Activity` / `Fragment` integrations. Manages coroutine scope internally and delivers results via callback. ### Constructor [Section titled “Constructor”](#constructor) ```kotlin DetourDelegate( lifecycleOwner: LifecycleOwner, config: DetourConfig, onLinkResult: (LinkResult) -> Unit, ) ``` * `lifecycleOwner` * Type `LifecycleOwner` * The Activity, AppCompatActivity, or Fragment that owns this delegate. * `config` * Type [`DetourConfig`](#detourconfig) * SDK configuration. Also used to call `Detour.initialize()` if not already initialized. * `onLinkResult` * Type `(LinkResult) -> Unit` * Callback invoked with the link processing result. ### Methods [Section titled “Methods”](#methods-1) #### `onCreate` [Section titled “onCreate”](#oncreate) ```kotlin fun onCreate(intent: Intent): Unit ``` Call from `Activity.onCreate()`. Processes the launching intent for App Links, scheme links, and deferred links. #### `onNewIntent` [Section titled “onNewIntent”](#onnewintent) ```kotlin fun onNewIntent(intent: Intent): Unit ``` Call from `Activity.onNewIntent()`. Processes links received while the app is already running. ## DetourConfig [Section titled “DetourConfig”](#detourconfig) SDK configuration object. Accepts required credentials and optional behavioral settings. ### Data class [Section titled “Data class”](#data-class) ```kotlin data class DetourConfig( val apiKey: String, val appId: String, val linkProcessingMode: LinkProcessingMode = LinkProcessingMode.ALL, val storage: DetourStorage? = null, ) ``` * `apiKey` required * Type `String` * Publishable API key from the Detour Dashboard. Used as a Bearer token for all API calls (deferred link matching, short-link resolution, analytics) * `appId` required * Type `String` * Unique app identifier from the Detour Dashboard. Used to route API requests to the correct app. * `linkProcessingMode` * Type [`LinkProcessingMode` ](#linkprocessingmode)Default `ALL` * Controls which link sources the SDK processes. See [LinkProcessingMode](#linkprocessingmode) for details on each value. * `storage` * Type [`DetourStorage?` ](#detourstorage)Default `null` * Custom storage adapter for SDK persistence. The SDK stores a first-entrance flag (for deferred link matching) and a device ID (for analytics). Left `null`, it falls back to a SharedPreferences-based implementation. ### Java builder [Section titled “Java builder”](#java-builder) For Java callers, a builder pattern is available: ```kotlin DetourConfig.Builder(apiKey: String, appId: String) .linkProcessingMode(LinkProcessingMode) .storage(DetourStorage) .build() ``` ## LinkResult [Section titled “LinkResult”](#linkresult) Sealed class returned by `processLink()` and `getDeferredLink()`. Pattern-match on the subtype to handle each outcome. ```kotlin sealed class LinkResult { data class Success( val url: String, val route: String, val pathname: String, val type: LinkType, val params: Map, ) : LinkResult() data object NotFirstLaunch : LinkResult() data object NoLink : LinkResult() data class Error(val exception: Exception) : LinkResult() } ``` ### Subtypes [Section titled “Subtypes”](#subtypes) | Subtype | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------ | | `Success` | A link was successfully resolved. Contains the parsed link data, listed below | | `NotFirstLaunch` | No deferred link available because this is not the first app launch. Only returned by the deferred link flow | | `NoLink` | Normal app launch without any deep link | | `Error` | An error occurred during link processing. The `exception` field contains the cause | ### Success fields [Section titled “Success fields”](#success-fields) * `url` * Type `String` * Full URL that was matched or opened. * `route` * Type `String` * In-app route for navigation, as a path with query string, first path segment (app hash) stripped (e.g. `/promo/123?utm=1`) * `pathname` * Type `String` * Route path without query string (e.g. `/promo/123`) * `type` * Type [`LinkType`](#linktype) * How the link arrived: `DEFERRED`, `VERIFIED`, or `SCHEME` * `params` * Type `Map` * Parsed query parameters as key-value pairs. ## LinkProcessingMode [Section titled “LinkProcessingMode”](#linkprocessingmode) ```kotlin enum class LinkProcessingMode { ALL, WEB_ONLY, DEFERRED_ONLY, } ``` Controls which link sources `Detour.processLink()` handles: | Value | Behavior | | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ALL` (default) | Processes all link sources: deferred links from the Detour API, App Links (verified HTTP/HTTPS URLs), and custom scheme deep links | | `WEB_ONLY` | Same as `ALL` but filters out custom scheme links. Only deferred links and App Links (HTTP/HTTPS) are processed | | `DEFERRED_ONLY` | Only processes deferred links from the Detour API. Skips App Link and scheme link detection. Recommended when your navigation framework already handles runtime deep links | ## LinkType [Section titled “LinkType”](#linktype) ```kotlin enum class LinkType { DEFERRED, VERIFIED, SCHEME, } ``` Describes how the link arrived in the app: | Value | Meaning | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `DEFERRED` | The link was obtained from the Detour deferred-link API. This happens when a user who does not have the app installed clicks a Detour link, installs the app, and opens it. The SDK retrieves the original link from the backend on first launch | | `VERIFIED` | The link arrived as an Android App Link, an HTTP/HTTPS URL from a verified domain that the OS delivered directly to your app | | `SCHEME` | The link arrived via a custom URL scheme (e.g. `myapp://path`). These are non-HTTP URLs that the OS routes to your app based on the registered intent filter | ## ShortLinkResponse [Section titled “ShortLinkResponse”](#shortlinkresponse) ```kotlin data class ShortLinkResponse( val link: String, val route: String?, val parameters: String?, ) ``` Returned by `Detour.resolveShortLink()`. * `link` * Type `String` * The full destination URL after short-link resolution. * `route` * Type `String?` * Extracted in-app route, or `null` if not available. * `parameters` * Type `String?` * Raw query parameter string, or `null` if none. ## DetourStorage [Section titled “DetourStorage”](#detourstorage) ```kotlin interface DetourStorage { suspend fun getItem(key: String): String? suspend fun setItem(key: String, value: String) suspend fun removeItem(key: String) { } } ``` Interface for custom storage adapters. The SDK persists a first-entrance flag (used for deferred link matching, which only runs on first app install) and a stable device ID (used for analytics). All methods are suspending functions. By default, the SDK uses a SharedPreferences-based implementation. Provide a custom adapter if you need encrypted storage, DataStore, or another persistence layer. See [Custom Storage](/docs/sdk/android/1.0.x/custom-storage) for examples. ## DetourAnalytics [Section titled “DetourAnalytics”](#detouranalytics) Analytics module initialized by `Detour.initialize()`. All calls are fire-and-forget. They run on a background dispatcher and never propagate exceptions. Both methods are `@JvmStatic` for Java interoperability. ### Methods [Section titled “Methods”](#methods-2) #### `logEvent` [Section titled “logEvent”](#logevent) ```kotlin DetourAnalytics.logEvent(eventName: DetourEventNames, data: Any? = null): Unit ``` Records a user action or custom event and sends it to the Detour backend. Pass one of the predefined [`DetourEventNames`](#detoureventnames) values. The optional `data` parameter accepts any Gson-serializable object (maps, primitives, data classes, etc.). #### `logRetention` [Section titled “logRetention”](#logretention) ```kotlin DetourAnalytics.logRetention(eventName: String): Unit ``` Records a retention/lifecycle event for user engagement tracking with a free-form event name string. ## DetourEventNames [Section titled “DetourEventNames”](#detoureventnames) | Category | Event Name | String Value | | :---------- | :--------------------------- | :------------------------------ | | **General** | `Login` | `login` | | | `Search` | `search` | | | `Share` | `share` | | | `SignUp` | `sign_up` | | | `TutorialBegin` | `tutorial_begin` | | | `TutorialComplete` | `tutorial_complete` | | | `ReEngage` | `re_engage` | | | `Invite` | `invite` | | | `OpenedFromPushNotification` | `opened_from_push_notification` | | **Sales** | `AddPaymentInfo` | `add_payment_info` | | | `AddShippingInfo` | `add_shipping_info` | | | `AddToCart` | `add_to_cart` | | | `RemoveFromCart` | `remove_from_cart` | | | `Refund` | `refund` | | | `ViewItem` | `view_item` | | | `BeginCheckout` | `begin_checkout` | | | `Purchase` | `purchase` | | | `AdImpression` | `ad_impression` | ## Related pages [Section titled “Related pages”](#related-pages) * [Usage](/docs/sdk/android/1.0.x/sdk-usage) * [Custom Storage](/docs/sdk/android/1.0.x/custom-storage) * [Testing & Troubleshooting](/docs/sdk/android/1.0.x/testing) # Custom Storage > Provide a custom storage adapter (EncryptedSharedPreferences or your own) for the Detour Android SDK. By default, Android SDK uses an internal `SharedPreferences`-based storage implementation. If your app requires encrypted storage, DataStore, or another persistence layer, pass a custom `DetourStorage` via `DetourConfig.storage`. ## Storage interface [Section titled “Storage interface”](#storage-interface) ```kotlin interface DetourStorage { suspend fun getItem(key: String): String? suspend fun setItem(key: String, value: String) suspend fun removeItem(key: String) { } } ``` ## What SDK persists [Section titled “What SDK persists”](#what-sdk-persists) SDK stores internal keys for: * first-launch detection (`Detour_firstEntranceFlag`) * analytics device identifier (`Detour_deviceId`) Custom storage should preserve values across app restarts to keep deferred-flow and analytics behavior consistent. ## Examples [Section titled “Examples”](#examples) The adapter below wraps `EncryptedSharedPreferences`. Any class that implements `DetourStorage` works, so use it as a starting point for DataStore or another persistence layer. ### EncryptedSharedPreferences [Section titled “EncryptedSharedPreferences”](#encryptedsharedpreferences) ```kotlin import android.content.Context import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey import com.swmansion.detour.storage.DetourStorage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class EncryptedStorageProvider(context: Context) : DetourStorage { private val masterKey = MasterKey.Builder(context) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build() private val prefs = EncryptedSharedPreferences.create( context, "DetourSecureStorage", masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ) override suspend fun getItem(key: String): String? = withContext(Dispatchers.IO) { prefs.getString(key, null) } override suspend fun setItem(key: String, value: String) = withContext(Dispatchers.IO) { prefs.edit().putString(key, value).apply() } } ``` ```kotlin val config = DetourConfig( apiKey = BuildConfig.DETOUR_API_KEY, appId = BuildConfig.DETOUR_APP_ID, storage = EncryptedStorageProvider(this) ) ``` ## Related pages [Section titled “Related pages”](#related-pages) * [Usage](/docs/sdk/android/1.0.x/sdk-usage) * [API Reference](/docs/sdk/android/1.0.x/api-reference) # Examples > Runnable Detour Android example apps covering full and deferred-only integrations. The Android SDK repository ships two runnable apps, one per integration style. Clone the one closest to your setup, run it, and copy what you need. Each card below links to the example’s `README.md` on GitHub, with the setup steps, the test flow, and the full source. ## Example catalog [Section titled “Example catalog”](#example-catalog) [example ](https://github.com/software-mansion-labs/android-detour/blob/1.0.1/examples/example/README.md)The full setup. DetourDelegate handles App Links, custom scheme links and deferred links, and the app logs analytics events. [example-deferred-only ](https://github.com/software-mansion-labs/android-detour/blob/1.0.1/examples/example-deferred-only/README.md)For apps whose navigation already handles App Links. Detour resolves only the deferred link, while the splash screen is shown. ## Recommended user path [Section titled “Recommended user path”](#recommended-user-path) 1. Complete [Installation](/docs/sdk/android/1.0.x/sdk-installation). 2. Read [Usage](/docs/sdk/android/1.0.x/sdk-usage) for lifecycle and routing model. 3. Start from the closest example and adapt manifest/routing logic. 4. Validate behavior with [Testing & Troubleshooting](/docs/sdk/android/1.0.x/testing). # Installation > Install the Detour Android SDK via Gradle (Kotlin or Groovy DSL). Detour provides an Android SDK (`com.swmansion:detour`) for deferred deep links, app links, and scheme links. Beta Android SDK `1.0.1` is currently in beta. APIs and integration details may still change before stable release. Before installation, make sure your app is created in the Detour Dashboard and you have: * `appId` from **API configuration** * publishable `apiKey` from **API configuration** * Android link integration snippet from **App configuration** ## Requirements [Section titled “Requirements”](#requirements) * Android min SDK: `21` (Android 5.0+) * Kotlin: `2.0+` * JVM target: `11` ## Add dependency [Section titled “Add dependency”](#add-dependency) * build.gradle.kts ```kotlin dependencies { implementation("com.swmansion:detour:1.0.1") } ``` * build.gradle ```groovy dependencies { implementation "com.swmansion:detour:1.0.1" } ``` ## Configure credentials [Section titled “Configure credentials”](#configure-credentials) Store credentials in config/environment and pass them through `BuildConfig` (same pattern as SDK examples): app/build.gradle.kts ```kotlin buildConfigField("String", "DETOUR_API_KEY", "\"${secrets["DETOUR_API_KEY"] ?: ""}\"") buildConfigField("String", "DETOUR_APP_ID", "\"${secrets["DETOUR_APP_ID"] ?: ""}\"") ``` ```kotlin // Activity/Application code val config = DetourConfig( apiKey = BuildConfig.DETOUR_API_KEY, appId = BuildConfig.DETOUR_APP_ID ) ``` ## Initialize SDK once [Section titled “Initialize SDK once”](#initialize-sdk-once) Call `Detour.initialize(...)` once before processing links. ```kotlin Detour.initialize(applicationContext, config) ``` You can initialize in `Application.onCreate()` or in the first activity before calling `processLink()` / `getDeferredLink()`. ## Manifest integration [Section titled “Manifest integration”](#manifest-integration) Add `VIEW` intent filters for links you want Android to dispatch to your app: * verified App Links (`https://...`) from Dashboard snippet * optional custom URI scheme (`myapp://...`) Example SDK apps use this pattern in `AndroidManifest.xml` and handle both app links and scheme links in one flow. ## Next steps [Section titled “Next steps”](#next-steps) 1. Wire `DetourDelegate` or manual processing in [Usage](/docs/sdk/android/1.0.x/sdk-usage). 2. Pick the closest runnable pattern in [Examples](/docs/sdk/android/1.0.x/examples). # Usage > Initialize Detour with DetourDelegate, resolve deferred links, and handle App Links and scheme links in your Android app. ## Use `DetourDelegate` in Activity lifecycle [Section titled “Use DetourDelegate in Activity lifecycle”](#use-detourdelegate-in-activity-lifecycle) `DetourDelegate` is the recommended integration for classic Android apps. It wraps coroutine calls to `Detour.processLink(...)` and returns `LinkResult` in a callback. ```kotlin import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.swmansion.detour.Detour import com.swmansion.detour.DetourConfig import com.swmansion.detour.DetourDelegate import com.swmansion.detour.models.LinkProcessingMode import com.swmansion.detour.models.LinkResult class MainActivity : AppCompatActivity() { private val detourDelegate = DetourDelegate( lifecycleOwner = this, config = DetourConfig( apiKey = BuildConfig.DETOUR_API_KEY, appId = BuildConfig.DETOUR_APP_ID, linkProcessingMode = LinkProcessingMode.ALL, ), onLinkResult = { result -> handleLinkResult(result) } ) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Detour.initialize(this, detourDelegate.config) detourDelegate.onCreate(intent) } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) setIntent(intent) detourDelegate.onNewIntent(intent) } private fun handleLinkResult(result: LinkResult) { // handle result and navigate } } ``` If you want complete, runnable integrations for each navigation style, go to [Examples](/docs/sdk/android/1.0.x/examples). ## `LinkResult` handling [Section titled “LinkResult handling”](#linkresult-handling) `processLink()` / `getDeferredLink()` return one of: * `LinkResult.Success` — contains `url`, `route`, `pathname`, `params`, `type` * `LinkResult.NotFirstLaunch` — deferred flow already consumed in this install/session * `LinkResult.NoLink` — no matching link * `LinkResult.Error` — runtime/integration error Typical routing pattern: ```kotlin when (result) { is LinkResult.Success -> navigateToRoute(result.route, result.params) is LinkResult.NotFirstLaunch -> Unit is LinkResult.NoLink -> Unit is LinkResult.Error -> Log.e("Detour", "Link error", result.exception) } ``` ## Choose `linkProcessingMode` [Section titled “Choose linkProcessingMode”](#choose-linkprocessingmode) `DetourConfig.linkProcessingMode` controls which link sources are handled: | Value | Deferred | App Links (`http/https`) | Custom scheme | | --------------- | -------- | ------------------------ | ------------- | | `ALL` (default) | Yes | Yes | Yes | | `WEB_ONLY` | Yes | Yes | No | | `DEFERRED_ONLY` | Yes | No | No | Use `DEFERRED_ONLY` when your navigation stack already handles app-link/scheme intents and Detour should only resolve first-install deferred links. ## Deferred-only manual flow [Section titled “Deferred-only manual flow”](#deferred-only-manual-flow) In `DEFERRED_ONLY`, you can skip `DetourDelegate` and call `Detour.getDeferredLink()` directly: ```kotlin Detour.initialize( this, DetourConfig( apiKey = BuildConfig.DETOUR_API_KEY, appId = BuildConfig.DETOUR_APP_ID, linkProcessingMode = LinkProcessingMode.DEFERRED_ONLY, ) ) lifecycleScope.launch { when (val result = Detour.getDeferredLink()) { is LinkResult.Success -> navigateToRoute(result.route, result.params) else -> Unit } } ``` See the `example-deferred-only` app in [Examples](/docs/sdk/android/1.0.x/examples). ## Analytics module [Section titled “Analytics module”](#analytics-module) `DetourAnalytics` is initialized together with `Detour.initialize(...)`. ```kotlin import com.swmansion.detour.analytics.DetourAnalytics import com.swmansion.detour.analytics.DetourEventNames DetourAnalytics.logEvent( DetourEventNames.Purchase, mapOf("item" to "subscription_premium", "currency" to "USD"), ) DetourAnalytics.logRetention("home_screen_opened") ``` ## Related pages [Section titled “Related pages”](#related-pages) * [Examples](/docs/sdk/android/1.0.x/examples) * [API Reference](/docs/sdk/android/1.0.x/api-reference) * [Custom Storage](/docs/sdk/android/1.0.x/custom-storage) * [Testing & Troubleshooting](/docs/sdk/android/1.0.x/testing) # Testing & Troubleshooting > Test deferred linking and App Links on Android and troubleshoot common Detour Android SDK issues. Testing deferred linking on Android has platform constraints, mainly around install referrer availability and app-link verification state. ## Deferred flow constraints [Section titled “Deferred flow constraints”](#deferred-flow-constraints) * Deferred matching is intended for first-launch flow after install. * Deterministic matching uses Play Install Referrer `click_id` when available. * If install referrer is unavailable, SDK falls back to probabilistic matching. * Repeated deferred attempts in same install/session return `NotFirstLaunch`. ## Recommended deferred test flow [Section titled “Recommended deferred test flow”](#recommended-deferred-test-flow) 1. Uninstall the app from the device or emulator. 2. Open a Detour link in a browser. 3. Install and launch the app. 4. Verify `LinkResult.Success` with `type = DEFERRED`, or expected fallback (`NoLink` / `Error`). 5. Relaunch the app and confirm the deferred link is not consumed again, that is `LinkResult.NotFirstLaunch`. Missing `click_id` If you encounter click\_id missing in your build see limitations section [Play Store referrer missing](/docs/platform/architecture/architecture-limitations#play-store-referrer-missing) ## Testing app links and scheme links [Section titled “Testing app links and scheme links”](#testing-app-links-and-scheme-links) For app-link/scheme integration (`LinkProcessingMode.ALL`), verify both cold and warm starts. ### App Link via ADB [Section titled “App Link via ADB”](#app-link-via-adb) ```bash adb shell am start -a android.intent.action.VIEW \ -d "https:///products/42" \ ``` Expected: `LinkResult.Success` with `type = VERIFIED`. ### Custom scheme via ADB [Section titled “Custom scheme via ADB”](#custom-scheme-via-adb) ```bash adb shell am start -a android.intent.action.VIEW \ -d "myapp://products/42" \ ``` Expected: `LinkResult.Success` with `type = SCHEME` (only when `LinkProcessingMode.ALL`). ### App Link verification and the SHA-256 fingerprint [Section titled “App Link verification and the SHA-256 fingerprint”](#app-link-verification-and-the-sha-256-fingerprint) An `autoVerify` App Link opens your app only if the Dashboard SHA-256 matches the key that signed **the installed APK**: | How the app is installed | Signed with | Where to read the SHA-256 | | :----------------------------------------------- | :------------------------- | :------------------------------------------------------------------------------------------------------ | | Google Play, any track | Play App Signing key | Play Console → **Protected with Play** → **Manage Play app signing** → *App signing key certificate* | | Android Studio run / `./gradlew assembleDebug` | AGP default debug keystore | `keytool -list -v -keystore ~/.android/debug.keystore -storepass android`, or `./gradlew signingReport` | | `./gradlew assembleRelease` with your own config | Your release keystore | `keytool -list -v -keystore `, or `./gradlew signingReport` | `./gradlew signingReport` prints the `Store:` path each variant really uses, so it stays correct when a project overrides `signingConfigs.debug`. To check a binary directly: `apksigner verify --print-certs app-debug.apk`. Then check what the device made of it: ```bash adb shell pm verify-app-links --re-verify adb shell pm get-app-links # expect: verified ``` The complete procedure is described in [Finding your Android SHA-256 fingerprint](/docs/platform/fundamentals/universal-app-links#finding-your-android-sha-256-fingerprint). ## Troubleshooting checklist [Section titled “Troubleshooting checklist”](#troubleshooting-checklist) * `Detour SDK not initialized` * `Detour.initialize(...)` was not called before `processLink()` / `getDeferredLink()`. * Immediate `NoLink` * no matching click found. Verify the link click happened before install. * Always `NotFirstLaunch` * first-launch flag already stored. Reinstall the app for deferred testing. * App links open browser instead of app * check Dashboard app-link domain config and manifest intent-filter alignment. * confirm the Dashboard SHA-256 matches the key that signed the installed APK. See [App Link verification and the SHA-256 fingerprint](#app-link-verification-and-the-sha-256-fingerprint). * Custom scheme not handled * verify manifest `` and `LinkProcessingMode.ALL`. * Analytics events not visible * ensure valid `apiKey`/`appId` and call `DetourAnalytics` only after SDK initialization. ## Debugging [Section titled “Debugging”](#debugging) Use logcat while testing: ```bash adb logcat | grep -i detour ``` Useful tags from SDK internals: * `Detour` * `DetourApiClient` * `InstallReferrerHelper` * `DetourAnalytics` For complete end-to-end sample flows, use [Examples](/docs/sdk/android/1.0.x/examples). # API Reference > Complete API reference for the Detour Flutter plugin. ## DetourService [Section titled “DetourService”](#detourservice) High-level orchestration layer for routing-safe integration. Extends `ChangeNotifier` so it can be used with Flutter’s `ListenableBuilder` or provider-based state management. ### Constructor [Section titled “Constructor”](#constructor) ```dart DetourService({ Duration duplicateSuppressionWindow = const Duration(seconds: 2), }) ``` * `duplicateSuppressionWindow` * Type `Duration` Default `Duration(seconds: 2)` * Time window used to suppress duplicate emissions for the same link (matched by `type|url`, and the URL comparison ignores case). Prevents double-navigation when the OS delivers the same link to both the initial and runtime paths. ### Properties [Section titled “Properties”](#properties) * `isStarted` * Type `bool` * `true` once `start()` has configured the native SDK, until `stop()` is called. While it is `true`, `start()` calls are no-ops. * `isInitialLinkProcessed` * Type `bool` * `true` once the initial link resolve flow has completed, regardless of whether a link was found. Use this to conditionally show a splash screen. * `pendingIntent` * Type [`DetourIntent?`](#detourintent) * Last navigation intent waiting to be consumed. Set when a link is resolved and cleared by `consumePendingIntent()`. Notifies listeners on change. ### Methods [Section titled “Methods”](#methods) #### `start` [Section titled “start”](#start) ```dart Future start(DetourConfig config) ``` Starts the Detour flow: configures the native SDK, subscribes to the runtime link stream, and resolves the initial link. Safe to call multiple times. Only the first call starts the service. After `stop()`, the next call starts it again. #### `processLink` [Section titled “processLink”](#processlink) ```dart Future processLink(String url, {bool emitIntent = true}) ``` Processes an arbitrary URL through the native SDK. When `emitIntent` is `true` (default) and the result contains a link, a new `DetourIntent` with source `manual` is emitted and `pendingIntent` is updated. #### `logEvent` [Section titled “logEvent”](#logevent) ```dart Future logEvent(DetourEventName eventName, {Map? data}) ``` Records a user action using a predefined [`DetourEventName`](#detoureventname). The optional `data` parameter accepts a map of arbitrary metadata. #### `logRetention` [Section titled “logRetention”](#logretention) ```dart Future logRetention(String eventName) ``` Records a retention/lifecycle event with a free-form event name string. #### `consumePendingIntent` [Section titled “consumePendingIntent”](#consumependingintent) ```dart void consumePendingIntent() ``` Marks the current `pendingIntent` as consumed by setting it to `null` and notifying listeners. Call this after your navigation layer has handled the intent to prevent repeated navigation. #### `stop` [Section titled “stop”](#stop) ```dart Future stop() ``` Cancels the runtime link stream subscription and resets the service. `pendingIntent` becomes `null`, `isStarted` and `isInitialLinkProcessed` become `false`, and the recent links held for duplicate suppression are cleared. Listeners are notified. A later `start()` call runs the full start flow again, including [`resolveInitialLink`](#resolveinitiallink). On Android, the native SDK keeps the configuration from the first `start()`. The repeated [`resolveInitialLink`](#resolveinitiallink) follows the rules for repeated calls. ## DetourFlutterPlugin [Section titled “DetourFlutterPlugin”](#detourflutterplugin) Low-level method/event-channel bridge to native SDKs. Use this directly only when you need fine-grained control over the link lifecycle. For most integrations, prefer [`DetourService`](#detourservice). ### Methods [Section titled “Methods”](#methods-1) #### `configure` [Section titled “configure”](#configure) ```dart Future configure(DetourConfig config) ``` Configures the native SDK with credentials and behavioral settings. Must be called before other methods. #### `resolveInitialLink` [Section titled “resolveInitialLink”](#resolveinitiallink) ```dart Future resolveInitialLink() ``` Resolves the link that launched the app (cold start). On iOS, a repeated call returns a result with no link. On Android, a repeated call processes the launch intent again, so it returns the same App Link or custom scheme link as the first call. On both platforms, only the first call can return a deferred link. #### `processLink` [Section titled “processLink”](#processlink-1) ```dart Future processLink(String url) ``` Processes a single URL through the native SDK and returns the result. #### `linkStream` [Section titled “linkStream”](#linkstream) ```dart Stream get linkStream ``` Stream of results for links the app receives while it is running. It carries Universal Links (iOS) and App Links (Android) unless `linkProcessingMode` is `LinkProcessingMode.deferredOnly`, and custom scheme links only when it is `LinkProcessingMode.all`. #### `logEvent` [Section titled “logEvent”](#logevent-1) ```dart Future logEvent(DetourEventName eventName, {Map? data}) ``` Forwards an analytics event to the native SDK. #### `logRetention` [Section titled “logRetention”](#logretention-1) ```dart Future logRetention(String eventName) ``` Forwards a retention event to the native SDK. ## Types [Section titled “Types”](#types) ### DetourConfig [Section titled “DetourConfig”](#detourconfig) ```dart class DetourConfig { final String apiKey; final String appID; final bool shouldUseClipboard; final LinkProcessingMode linkProcessingMode; } ``` * `apiKey` required * Type `String` * Publishable API key from the Detour Dashboard. Used as a Bearer token for all API calls (deferred link matching, short-link resolution, analytics) * `appID` required * Type `String` * Unique app identifier from the Detour Dashboard. Used to route API requests to the correct app. * `shouldUseClipboard` * Type `bool` Default `true` * iOS only. When `true`, the SDK reads the device clipboard as part of probabilistic fingerprinting for deferred link matching. May trigger the iOS system paste permission prompt. * `linkProcessingMode` * Type [`LinkProcessingMode` ](#linkprocessingmode)Default `LinkProcessingMode.all` * Controls which link sources the SDK processes. See [LinkProcessingMode](#linkprocessingmode) for details. ### DetourResult [Section titled “DetourResult”](#detourresult) ```dart class DetourResult { final bool processed; final DetourLink? link; } ``` Result returned by link processing methods. * `processed` * Type `bool` * `true` when the native SDK has finished processing (regardless of whether a link was found) * `link` * Type [`DetourLink?`](#detourlink) * Parsed link data, or `null` if no link was matched. ### DetourLink [Section titled “DetourLink”](#detourlink) ```dart class DetourLink { final String url; final String route; final String pathname; final Map params; final LinkType type; } ``` Parsed link data returned inside a `DetourResult`. * `url` * Type `String` * Full URL that was matched or opened. * `route` * Type `String` * In-app route for navigation, as a path with query string, first path segment (app hash) stripped (e.g. `/promo/123?utm=1`) * `pathname` * Type `String` * Route path without query string (e.g. `/promo/123`) * `params` * Type `Map` * Parsed query parameters as key-value pairs. * `type` * Type [`LinkType`](#linktype) * How the link arrived in the app. ### DetourIntent [Section titled “DetourIntent”](#detourintent) ```dart class DetourIntent { final DetourLink link; final DetourIntentSource source; final DateTime receivedAt; } ``` A single navigation intent produced by `DetourService`. Represents a resolved link that is ready to be consumed by the app’s navigation layer. * `link` * Type [`DetourLink`](#detourlink) * The resolved link data. * `source` * Type [`DetourIntentSource`](#detourintentsource) * Origin of the link, which says how it was resolved. * `receivedAt` * Type `DateTime` * Timestamp when the intent was created. ### DetourIntentSource [Section titled “DetourIntentSource”](#detourintentsource) ```dart enum DetourIntentSource { initial, runtime, manual, } ``` Origin of a resolved Detour link emitted by `DetourService`: | Value | Meaning | | --------- | ----------------------------------------------------------------- | | `initial` | Link resolved from cold-start flow via `resolveInitialLink()` | | `runtime` | Link received while the app is running via the native link stream | | `manual` | Link resolved from an explicit `processLink(url)` call | ### LinkType [Section titled “LinkType”](#linktype) ```dart enum LinkType { deferred, verified, scheme, } ``` Describes how the link arrived in the app: | Value | Meaning | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `deferred` | The link was obtained from the Detour deferred-link API. This happens when a user who does not have the app installed clicks a Detour link, installs the app, and opens it. The SDK retrieves the original link from the backend on first launch | | `verified` | The link arrived as a Universal Link (iOS) or App Link (Android), an HTTP/HTTPS URL that the OS verified belongs to your app and delivered directly to it | | `scheme` | The link arrived via a custom URL scheme (e.g. `myapp://path`). These are non-HTTP URLs that the OS routes to your app based on the registered scheme | ### LinkProcessingMode [Section titled “LinkProcessingMode”](#linkprocessingmode) ```dart enum LinkProcessingMode { all, webOnly, deferredOnly, } ``` Controls which link sources the native SDK processes: | Value | Behavior | | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `all` (default) | Processes all link sources: deferred links from the Detour API, Universal Links (iOS) / App Links (Android), and custom scheme deep links | | `webOnly` | Same as `all` but filters out custom scheme links. Only deferred links and HTTP/HTTPS links are processed | | `deferredOnly` | Only processes deferred links from the Detour API. Skips runtime link listener and initial URL check. Recommended when your navigation framework already handles runtime deep links | ### DetourEventName [Section titled “DetourEventName”](#detoureventname) Standard event names for use with `logEvent()`. | Enum value | String value | | :------------------------------------------- | :------------------------------ | | **General** | | | `DetourEventName.login` | `login` | | `DetourEventName.search` | `search` | | `DetourEventName.share` | `share` | | `DetourEventName.signUp` | `sign_up` | | `DetourEventName.tutorialBegin` | `tutorial_begin` | | `DetourEventName.tutorialComplete` | `tutorial_complete` | | `DetourEventName.reEngage` | `re_engage` | | `DetourEventName.invite` | `invite` | | `DetourEventName.openedFromPushNotification` | `opened_from_push_notification` | | **Sales** | | | `DetourEventName.addPaymentInfo` | `add_payment_info` | | `DetourEventName.addShippingInfo` | `add_shipping_info` | | `DetourEventName.addToCart` | `add_to_cart` | | `DetourEventName.removeFromCart` | `remove_from_cart` | | `DetourEventName.refund` | `refund` | | `DetourEventName.viewItem` | `view_item` | | `DetourEventName.beginCheckout` | `begin_checkout` | | `DetourEventName.purchase` | `purchase` | | `DetourEventName.adImpression` | `ad_impression` | ## Notes [Section titled “Notes”](#notes) * `logEvent` accepts only predefined `DetourEventName` enum values. * The Flutter bridge does not expose `mountAnalytics`, `unmountAnalytics`, or `resetSession`. Analytics are managed automatically by the native SDKs. ## Related pages [Section titled “Related pages”](#related-pages) * [Usage](/docs/sdk/flutter/1.1.x/sdk-usage) * [Examples](/docs/sdk/flutter/1.1.x/examples) * [Testing & Troubleshooting](/docs/sdk/flutter/1.1.x/testing) # Custom Storage > Provide a custom storage adapter for the Detour Flutter plugin. In Flutter Plugin `1.1.1`, custom storage injection is not exposed in Dart API. ## Current behavior [Section titled “Current behavior”](#current-behavior) * Flutter bridge does not accept storage adapter in `DetourConfig`. * Persistence is handled by native SDK internals: * Android native SDK storage (the plugin bridge passes `storage = null`, so the native SDK falls back to its own SharedPreferences store) * iOS native SDK `UserDefaults` keys for first-entrance and analytics device ID ## Practical impact [Section titled “Practical impact”](#practical-impact) * You cannot pass a Flutter-level storage implementation (for example Hive/SecureStorage) to Detour yet. * Deferred-link first-launch semantics depend on native persisted state. * To retest deferred flow, use reinstall flow (see [Testing & Troubleshooting](/docs/sdk/flutter/1.1.x/testing)). ## Related pages [Section titled “Related pages”](#related-pages) * [Usage](/docs/sdk/flutter/1.1.x/sdk-usage) * [API Reference](/docs/sdk/flutter/1.1.x/api-reference) # Examples > A runnable Detour Flutter example app with end-to-end deep link integration. The plugin repository ships one runnable app: a full `DetourService` integration that handles deferred, Universal/App, and custom scheme links on both Android and iOS. Clone it, run it, and copy what you need. The card below links to the example’s `README.md` on GitHub, with the setup steps and the full source. ## Example catalog [Section titled “Example catalog”](#example-catalog) [example ](https://github.com/software-mansion-labs/detour-flutter-plugin/blob/v1.1.1/example/README.md)A complete DetourService integration for Android and iOS, with deep link configuration for both platforms and analytics calls. Start here for a production-like setup. ## Recommended user path [Section titled “Recommended user path”](#recommended-user-path) 1. Complete [Installation](/docs/sdk/flutter/1.1.x/sdk-installation). 2. Read [Usage](/docs/sdk/flutter/1.1.x/sdk-usage) for service lifecycle and routing model. 3. Adapt the example to your package ID, bundle ID, scheme, and Detour domain. 4. Validate behavior with [Testing & Troubleshooting](/docs/sdk/flutter/1.1.x/testing). # Installation > Install the Detour Flutter plugin and configure Android and iOS deep linking. Detour provides a Flutter plugin (`detour_flutter_plugin`) that bridges native Detour SDK behavior on Android and iOS. Beta Flutter Plugin version `1.1.1` is currently in beta. API and integration details may still change before stable release. Before installation, make sure your app is created in the Detour Dashboard and you have: * `appID` from **API configuration** * publishable `apiKey` from **API configuration** * platform integration snippets from **App configuration** ## Requirements [Section titled “Requirements”](#requirements) * Flutter: `>=3.3.0` * Dart: `^3.11.1` * Android min SDK: `24` * iOS: `13.0+` ## Install package [Section titled “Install package”](#install-package) Add the package to your `pubspec.yaml`: ```yaml dependencies: detour_flutter_plugin: ^1.1.1 ``` Then run: ```bash flutter pub get ``` ## Native SDK dependencies [Section titled “Native SDK dependencies”](#native-sdk-dependencies) `detour_flutter_plugin` wraps native Detour SDKs: * Android bridge depends on `com.swmansion.detour:detour-sdk:1.0.1`. * iOS bridge depends on `Detour` CocoaPod (`~> 1.0.2`), pulled automatically via the plugin podspec. For Android dependency resolution, keep `google()` and `mavenCentral()` in your project repositories. For iOS integration updates, run: ```bash cd ios pod install cd .. ``` ## App config [Section titled “App config”](#app-config) Use your credentials in `DetourConfig`: ```dart const DetourConfig( apiKey: '', appID: '', shouldUseClipboard: true, linkProcessingMode: LinkProcessingMode.all, ) ``` ## Platform integration [Section titled “Platform integration”](#platform-integration) The plugin handles bridge wiring, but your app still needs native deep-link setup: * Android: * App Links intent-filter from Dashboard * optional custom scheme intent-filter (if you use scheme links) * iOS: * Associated Domains (`applinks:`) * `CFBundleURLTypes` for custom scheme links * if you override AppDelegate/SceneDelegate deep-link callbacks, always forward to `super` ### iOS delegate forwarding (required when overriding) [Section titled “iOS delegate forwarding (required when overriding)”](#ios-delegate-forwarding-required-when-overriding) Flutter plugins (including Detour) receive lifecycle callbacks through Flutter delegate chain.\ If you override iOS deep-link callbacks, return `super` result: ```swift override func application( _ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:] ) -> Bool { return super.application(app, open: url, options: options) } override func application( _ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void ) -> Bool { return super.application( application, continue: userActivity, restorationHandler: restorationHandler ) } ``` Reference implementation: * [AppDelegate.swift (example app)](https://github.com/software-mansion-labs/detour-flutter-plugin/blob/v1.1.1/example/ios/Runner/AppDelegate.swift) * [SceneDelegate.swift (example app)](https://github.com/software-mansion-labs/detour-flutter-plugin/blob/v1.1.1/example/ios/Runner/SceneDelegate.swift) Clipboard on iOS 14+ On iOS 14+, clipboard matching uses `detectPatterns` before reading pasteboard content. Actual clipboard value is read only when probable URL is detected. ## Next steps [Section titled “Next steps”](#next-steps) 1. Integrate startup/runtime flow in [Usage](/docs/sdk/flutter/1.1.x/sdk-usage). 2. Start from the runnable app in [Examples](/docs/sdk/flutter/1.1.x/examples). # Usage > Start DetourService, consume pending intents, and handle initial and runtime links in your Flutter app. ## Recommended integration with `DetourService` [Section titled “Recommended integration with DetourService”](#recommended-integration-with-detourservice) `DetourService` is the recommended orchestration layer. It: * configures native bridge once, * merges initial and runtime link handling into one pending intent queue, * exposes startup readiness via `isInitialLinkProcessed`, * uses explicit consume semantics (`consumePendingIntent()`), * suppresses short-window duplicate emissions. ```dart import 'package:detour_flutter_plugin/detour_flutter_plugin.dart'; import 'package:flutter/material.dart'; class MyApp extends StatefulWidget { const MyApp({super.key}); @override State createState() => _MyAppState(); } class _MyAppState extends State { final detour = DetourService(); @override void initState() { super.initState(); detour.addListener(_onDetourChanged); _startDetour(); } Future _startDetour() async { await detour.start( const DetourConfig( apiKey: '', appID: '', shouldUseClipboard: true, linkProcessingMode: LinkProcessingMode.all, ), ); } void _onDetourChanged() { final intent = detour.pendingIntent; if (intent == null) return; // Route once, then mark as consumed. // context.go(intent.link.route); detour.consumePendingIntent(); } } ``` If you want complete, runnable integration, go to [Examples](/docs/sdk/flutter/1.1.x/examples). ## Link processing mode [Section titled “Link processing mode”](#link-processing-mode) `DetourConfig.linkProcessingMode` controls link sources: | Value | Universal/App links | Deferred links | Custom scheme links | | ---------------------------------- | ------------------- | -------------- | ------------------- | | `LinkProcessingMode.all` (default) | Yes | Yes | Yes | | `LinkProcessingMode.webOnly` | Yes | Yes | No | | `LinkProcessingMode.deferredOnly` | No | Yes | No | ## `DetourResult` and routing fields [Section titled “DetourResult and routing fields”](#detourresult-and-routing-fields) `DetourResult` contains: * * `processed` * `link` Type `DetourLink?` If `link != null`, routing payload is: * `link.route` Path + query. * `link.pathname` Path only. * `link.params` Type `Map` * `link.url` Raw URL string. * `link.type` Type `deferred | verified | scheme` ## Manual processing [Section titled “Manual processing”](#manual-processing) You can process arbitrary URLs explicitly: ```dart final result = await detour.processLink( 'https://example.godetour.link/abc123?source=push', ); if (result.link != null) { // Navigate with result.link!.route } ``` Use `emitIntent: false` when you need result inspection without pushing intent into service queue. ## Low-level API (`DetourFlutterPlugin`) [Section titled “Low-level API (DetourFlutterPlugin)”](#low-level-api-detourflutterplugin) If you need full manual control, use `DetourFlutterPlugin` directly: ```dart final plugin = DetourFlutterPlugin(); await plugin.configure( const DetourConfig( apiKey: '', appID: '', ), ); final initial = await plugin.resolveInitialLink(); final stream = plugin.linkStream; final processed = await plugin.processLink('https://example.godetour.link/path'); ``` ## Analytics [Section titled “Analytics”](#analytics) Use predefined enum events only: ```dart await detour.logEvent( DetourEventName.purchase, data: {'value': 9.99, 'currency': 'USD'}, ); await detour.logRetention('home_screen_viewed'); ``` `logEvent` accepts `DetourEventName` (not arbitrary event strings). ## Related pages [Section titled “Related pages”](#related-pages) * [Examples](/docs/sdk/flutter/1.1.x/examples) * [API Reference](/docs/sdk/flutter/1.1.x/api-reference) * [Custom Storage](/docs/sdk/flutter/1.1.x/custom-storage) * [Testing & Troubleshooting](/docs/sdk/flutter/1.1.x/testing) # Testing & Troubleshooting > Test deferred linking on Android and iOS in Flutter and troubleshoot common Detour Flutter plugin issues. Deferred flow testing has platform constraints: full attribution path depends on store redirect chain. ## Deferred flow constraints [Section titled “Deferred flow constraints”](#deferred-flow-constraints) * Deferred matching is first-launch oriented. * Runtime callbacks do not re-trigger deferred matching logic. * Flutter bridge does not expose session reset API in `1.1.1`. ## Recommended deferred test flow [Section titled “Recommended deferred test flow”](#recommended-deferred-test-flow) 1. Uninstall the app from the device or simulator. 2. Open a Detour link. 3. Install and launch the app. 4. Verify the initial intent and the resolved route in your UI logs. 5. Relaunch the app and confirm the deferred behavior is not repeated. ## Runtime link tests [Section titled “Runtime link tests”](#runtime-link-tests) ### Android App Link [Section titled “Android App Link”](#android-app-link) ```bash adb shell am start -a android.intent.action.VIEW \ -d "https:///" \ com.example.app ``` ### Android custom scheme [Section titled “Android custom scheme”](#android-custom-scheme) ```bash adb shell am start -a android.intent.action.VIEW \ -d "myapp://product/42" \ com.example.app ``` ### iOS custom scheme (simulator) [Section titled “iOS custom scheme (simulator)”](#ios-custom-scheme-simulator) ```bash xcrun simctl openurl booted "myapp://product/42" ``` ### iOS Universal Link (simulator) [Section titled “iOS Universal Link (simulator)”](#ios-universal-link-simulator) ```bash xcrun simctl openurl booted "https:///" ``` ## Platform-specific notes [Section titled “Platform-specific notes”](#platform-specific-notes) ### iOS [Section titled “iOS”](#ios) Copy the link before installing Clipboard matching requires that you copy the link to your clipboard before installing. Ensure `shouldUseClipboard: true` is set in `DetourConfig` and that the copy-link feature is enabled in the Detour Dashboard for your iOS configuration. ### Android [Section titled “Android”](#android) Install referrer `click_id` is often unavailable in local/dev-store scenarios. In that case matching falls back to probabilistic flow. For local install referrer testing workarounds, see [How to test the Play Store Install Referrer API](https://medium.com/@madicdjordje/how-to-test-the-play-store-install-referrer-api-78a63d59945b). Missing `click_id` If you encounter click\_id missing in your production build see limitations section [Play Store referrer missing](/docs/platform/architecture/architecture-limitations#play-store-referrer-missing) ### Testing Android App Links [Section titled “Testing Android App Links”](#testing-android-app-links) If an installed app does not open Detour links, the Dashboard SHA-256 probably does not match the key that signed **the build on your device**: | How you install the app | SHA-256 to use | Where to find it | | :------------------------------------------ | :------------------------- | :------------------------------------------------------------------------------------------------------ | | Google Play, any track | Play App Signing key | Play Console → **Protected with Play** → **Manage Play app signing** → *App signing key certificate* | | `flutter run` / `flutter build apk --debug` | AGP default debug keystore | `keytool -list -v -keystore ~/.android/debug.keystore -storepass android`, or `./gradlew signingReport` | | Release build with your own keystore | Your release keystore | `keytool -list -v -keystore ` | The Flutter template signs `release` with the debug config until you add your own release keystore, so a fresh `flutter build apk --release` is still debug-signed. To check any build directly: `apksigner verify --print-certs build/app/outputs/flutter-apk/app-debug.apk`. The complete procedure is described in [Finding your Android SHA-256 fingerprint](/docs/platform/fundamentals/universal-app-links#finding-your-android-sha-256-fingerprint). ## Common issues [Section titled “Common issues”](#common-issues) * **No match found (404)** * Check timestamps. The click may be older than the match window (15 minutes). * Confirm the click was recorded in your Detour Dashboard. * On Android deterministic flow: confirm `click_id` is present in the install referrer. * **429 Too Many Requests** * Detour limits deferred link matching per device and IP address, and per app. Wait between reinstall tests on one device. For tests on many devices at once, create a separate app for testing in the Dashboard, because each app has its own credentials and its own limit. * **`NOT_CONFIGURED` platform error** * `resolveInitialLink()` and `processLink()` raise it on both platforms. On Android, `logEvent()` and `logRetention()` raise it too. * Await `DetourService.start(...)` or `DetourFlutterPlugin.configure(...)` before you call these methods. If `configure(...)` failed, fix that error first. * **`INVALID_ARGS` platform error** * Android only. `configure(...)` raises it when `apiKey` or `appID` is empty, `processLink(...)` when `url` is empty, and `logRetention(...)` when `eventName` is empty. A value that contains only whitespace counts as empty. * Pass non-empty values. * **`NO_ACTIVITY` platform error** * On Android, `configure(...)` ran in a Flutter engine that has no attached activity, such as a separate engine started for background work. * Call `DetourService.start(...)` or `DetourFlutterPlugin.configure(...)` from your app’s UI code, not from a background handler. * **`NATIVE_ERROR` platform error** * Android only. `resolveInitialLink()` and `processLink()` raise it, and `linkStream` emits it as an error event, when the native SDK fails to process a link. `DetourService.start()` throws the same error and leaves `isInitialLinkProcessed` at `false`, so catch it if your startup flow waits for that flag. * A failed deferred match request on first launch raises it. When the server responded, the message starts with the HTTP status code: `[401]` for a wrong `apiKey` or `appID`, `[402]` when your organization is over its plan’s click limit, and `[429]` for too many match requests. Without a status code, the request did not complete. * **iOS clipboard not read** * Ensure `shouldUseClipboard: true` is set in `DetourConfig`. * Ensure the copy-link feature is enabled in the Detour Dashboard for your iOS app configuration. * The link must be copied to the clipboard before the app is opened, because the clipboard is only read during `resolveInitialLink()`. * **Deferred link not returned on subsequent test runs** * Deferred matching is first-launch based. Reinstall the app for a clean re-test. * **Android release build fails with `Type com.swmansion.detour.BuildConfig is defined multiple times`** * Debug builds of the same project succeed. * `detour_flutter_plugin` `1.1.1` uses the Android namespace `com.swmansion.detour`, which the native Detour SDK also uses. The release build then contains two classes named `com.swmansion.detour.BuildConfig`. * No `1.1.x` release fixes this. Upgrade to `detour_flutter_plugin: ^1.2.1` and run `flutter pub get`. ## Debugging [Section titled “Debugging”](#debugging) * Android: * `adb logcat | grep -i detour` * iOS simulator: * `xcrun simctl spawn booted log stream --predicate 'subsystem == "com.swmansion.detour"'` For full runnable setup and commands, use [Examples](/docs/sdk/flutter/1.1.x/examples).