# Usage

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

Build the config once and pass the same value to every call:

```swift
import Detour

let detourConfig = DetourConfig(
    apiKey: "<YOUR_DETOUR_API_KEY>",
    appID: "<YOUR_DETOUR_APP_ID>",
    shouldUseClipboard: true,
    linkProcessingMode: .all
)
```

## 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)

For a scene-based app, UIKit supplies cold-start URLs and user activities in
`UIScene.ConnectionOptions`:

```swift title="SceneDelegate.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)

```swift title="AppDelegate.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
}
```

:::caution[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](https://detour.swmansion.com/docs/sdk/ios/custom-storage#reset-for-testing).
:::

For a complete, runnable integration with an AppDelegate and SwiftUI state, see [Examples](https://detour.swmansion.com/docs/sdk/ios/examples).

## Handle runtime links

### Universal Links

Verified `https://` links arrive as an `NSUserActivity`. Use the callback for
your app's lifecycle:

```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
    }
}
```

  ```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)

Registered links such as `myapp://product/42` arrive as a URL. Use the callback
for your app's lifecycle:

```swift
func scene(
    _ scene: UIScene,
    openURLContexts URLContexts: Set<UIOpenURLContext>
) {
    guard let url = URLContexts.first?.url else { return }

    Task { @MainActor in
        let result = await Detour.shared.processLink(url, config: detourConfig)
        // handle result.link
    }
}
```

  ```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

`processLink` and `resolveInitialLink` both return a `DetourResult`:

<PropertyList>
  <Property name="processed" type={"Bool"}>
    Link processing finished.
  </Property>
  <Property name="link" type={"DetourLink?"}>
    The matched link, or `nil`.
  </Property>
</PropertyList>

`DetourResult` also forwards the link's fields, so you can read them without unwrapping:

<PropertyList>
  <Property name="route" type={"String?"}>
    Full in-app route, query string included: `/details?utm=1`.
  </Property>
  <Property name="pathname" type={"String?"}>
    Path without the query: `/details`.
  </Property>
  <Property name="params" type={"[String: String]"}>
    Query params, already parsed.
  </Property>
  <Property name="linkType" type={"LinkType?"} typeHref="/sdk/ios/api-reference#linktype">
    Where the link came from: `.deferred`, `.verified`, or `.scheme`.
  </Property>
  <Property name="linkURL" type={"URL?"}>
    The original URL.
  </Property>
</PropertyList>

## 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

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

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

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.

:::note[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](https://detour.swmansion.com/docs/platform/fundamentals/billing).

## 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")
```

:::note[SDK version header]
Every request the SDK sends carries an `X-SDK: ios/<version>` header, used for diagnostics and SDK-usage analysis. It needs no configuration.
:::

## Related pages

- [Examples](https://detour.swmansion.com/docs/sdk/ios/examples)
- [API Reference](https://detour.swmansion.com/docs/sdk/ios/api-reference)
- [Custom Storage](https://detour.swmansion.com/docs/sdk/ios/custom-storage)
- [Testing & Troubleshooting](https://detour.swmansion.com/docs/sdk/ios/testing)