> ## Documentation Index
> Fetch the complete documentation index at: https://docs.userpilot.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom URL Scheme

> Register and handle the Userpilot custom URL scheme on iOS to preview draft experiences and enter Builder screen-capture mode.

The Userpilot iOS SDK supports a custom URL scheme that lets you preview experiences on a real device before publishing them.

Setting it up is two steps: register the scheme with iOS so the system routes matching links to your app, then forward the incoming URL to the Userpilot SDK. The SDK does all the validation, so you hand it every URL your app receives and act on the result.

***

## Register the custom URL scheme

Update your `Info.plist` to register the scheme. Replace `APP_TOKEN` with your app's Userpilot token, which you can obtain from your [Environments Page](https://run.userpilot.io/environment).

For example, if your token is `NX-12345678`, your scheme value is `userpilot-nx-12345678`.

```xml theme={null}
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLName</key>
        <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>userpilot-APP_TOKEN</string>
        </array>
    </dict>
</array>
```

***

Use the token exactly as passed to the SDK, in lowercase. Keep the `STG-` prefix
for staging: `STG-NX-12345678` uses `userpilot-stg-nx-12345678`, while
`NX-12345678` uses `userpilot-nx-12345678`.

## Handle the custom URL scheme

Forward incoming URLs with `filterAndHandle(_:)` or `didHandleURL(_:)`.
`filterAndHandle(_:)` returns the set of URL contexts that Userpilot did not handle;
`didHandleURL(_:)` returns `true` when Userpilot handled a single URL. Pass unhandled
URLs to your own routing logic.

| **Method** | **Use it in** | **Returns** |
| - | - | - |
| `filterAndHandle(_:)` | A `UISceneDelegate`, where URLs arrive as a `Set<UIOpenURLContext>` | The subset of contexts Userpilot did **not** handle |
| `didHandleURL(_:)` | An `AppDelegate` or SwiftUI `onOpenURL`, where you have a single `URL` | `true` if Userpilot handled the URL |

### Scene delegate

Handle both entry points. `scene(_:willConnectTo:options:)` covers a cold launch from a link, and `scene(_:openURLContexts:)` covers a link that arrives while your app is already running. Miss the first and links only work when the app is already open.

```swift theme={null}
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    // Handle Userpilot deep links
    let unhandledURLContexts = userpilot.filterAndHandle(connectionOptions.urlContexts)

    // Handle any links remaining in unhandledURLContexts
}

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
    // Handle Userpilot deep links
    let unhandledURLContexts = userpilot.filterAndHandle(URLContexts)

    // Handle any links remaining in unhandledURLContexts
}
```

### App delegate

If your app uses only an app delegate, add the following:

```swift theme={null}
func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    // Handle Userpilot deep links
    guard !userpilot.didHandleURL(url) else { return true }

    // Handle a non-Userpilot URL
    return false
}
```

### SwiftUI

A SwiftUI app handles the scheme in the `onOpenURL` modifier attached to the `Scene` of your main `App`:

```swift theme={null}
var body: some Scene {
    WindowGroup {
        MyApp()
            .onOpenURL { url in
                guard !userpilot.didHandleURL(url) else { return }

                // Handle a non-Userpilot URL
            }
    }
}
```

<Tip>
  **Links that arrive before a scene is active**

  A link opened from a cold start can reach the SDK before any window scene is active. The SDK defers those actions and replays them once the scene activates, so you can forward URLs as soon as you receive them without sequencing the call yourself.
</Tip>

***

## Supported links

The SDK only claims a URL when the scheme matches your token and the host is `sdk`. Every other URL is passed back to your app.

| **Link** | **Purpose** |
| - | - |
| `userpilot-{token}://sdk/experience_preview/{experience_id}` | Previews a draft Flow or Survey. This is the link behind the Builder's QR code. |
| `userpilot-{token}://sdk/capture_screen/{session_id}?launch_token={token}` | Enters Builder screen-capture mode for a Builder session. |

<Note>
  Push notifications on iOS do **not** arrive through the URL scheme. The SDK receives them natively, so follow [Push Notifications](./push-notifications) for that setup. On Android the notification tap *is* a deep link, which is why the Android guide covers an extra `sdk/notification` path.
</Note>

***

## Preview an experience with a QR code

Userpilot can render a draft experience on your device before you publish it, so you can check layout, copy, and behavior on a real screen. Preview is supported for **Flows** and **Surveys**.

The preview is delivered over the custom URL scheme. It works once your app is installed on the device and the scheme is registered and handled as described above.

<Steps>
  <Step title="Open the experience">
    Open a Flow or Survey in the Experience Builder and enter **Edit Mode**.
  </Step>

  <Step title="Start the preview">
    Select **Preview** in the top-right corner of the builder. A popup with a **QR code** appears.
  </Step>

  <Step title="Scan the QR code">
    Scan the code with your device camera. Your app opens and the experience appears.
  </Step>
</Steps>

Points to keep in mind:

* You don't need to apply or save your changes before previewing them.
* The experience doesn't need to be live.
* A preview bypasses the experience's targeting and frequency settings, so it appears even when the current user wouldn't normally qualify for it.
* Once you close the experience, the app resumes normal behavior.

***

## Test push notifications

You can send a test push to a specific user from the Userpilot dashboard and verify delivery and tap handling on a real device without sending anything to your wider audience. The notification doesn't need to be live, so you can test it while it's still a draft.

* Test pushes work in both the staging and production environments.
* Test pushes are **not** counted as real sends, and they're excluded from analytics.
* Test pushes bypass the notification's targeting and frequency settings.

For the dashboard steps, see [Mobile push notifications](/in-app-engagement/mobile-content/mobile-push-notifications).

***

## Test a deep link from the command line

Open a link against the iOS Simulator to confirm your scheme is registered and your handler runs, without going through the Builder:

```bash theme={null}
xcrun simctl openurl booted "userpilot-nx-12345678://sdk/experience_preview/12345"
```

Replace the scheme with your own token and `12345` with a real experience ID. Enable SDK logging during setup to see the SDK report which link it received and what it did with it.

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="Nothing happens when I scan the QR code">
    Confirm the scheme in `Info.plist` matches your token exactly, lowercased. A mismatched scheme means iOS never routes the link to your app, so your handler is not called at all. Verify the registration with the `xcrun simctl openurl` command above.
  </Accordion>

  <Accordion title="The app opens but the experience doesn't appear">
    iOS routed the link, so registration is correct and the problem is in the handling step. Check that you call `filterAndHandle(_:)` or `didHandleURL(_:)` on every entry point your app uses, including `scene(_:willConnectTo:options:)` for cold launches. Also confirm the SDK is set up with the same token that generated the QR code.
  </Accordion>

  <Accordion title="Preview works in production but not in staging">
    Each environment has its own token, and the scheme follows the token. Confirm the staging build registers the scheme for the token it initializes the SDK with.
  </Accordion>

  <Accordion title="My own deep links stopped working">
    Both handling methods report whether Userpilot claimed the URL. Make sure you still process the URLs they hand back: the contexts returned by `filterAndHandle(_:)`, or any URL where `didHandleURL(_:)` returned `false`.
  </Accordion>
</AccordionGroup>

<Frame>
  [**For any questions or concerns please reach out to support@userpilot.com**](mailto:support@userpilot.com)
</Frame>
