> ## 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 in .NET MAUI to preview draft experiences and route push notification taps.

The Userpilot MAUI SDK supports a custom URL scheme that lets you preview experiences on a real device before publishing them, and that carries push notification taps back into your app.

Setting it up is two steps on each platform: register the scheme with the OS, then forward incoming URLs from your platform entry point to the Userpilot SDK. The SDK performs all the validation. If the URL belongs to Userpilot it's handled and the call returns `true`; otherwise it returns `false` and your app should handle the link as usual.

***

## Register the custom URL scheme

The default scheme is `userpilot-` followed by your lowercased Userpilot token, which you can obtain from your [Environments Page](https://run.userpilot.io/environment). For example, if your token is `NX-12345678`, the scheme is `userpilot-nx-12345678`.

### Android

Add an intent filter for the scheme to the activity that hosts your MAUI app, alongside the existing `MAIN` and `LAUNCHER` filter. Use `LaunchMode.SingleTop` so links reach the running instance instead of creating a new one.

```csharp theme={null}
[Activity(
    Theme = "@style/Maui.SplashTheme",
    MainLauncher = true,
    LaunchMode = LaunchMode.SingleTop,
    ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
[IntentFilter(
    new[] { Intent.ActionView },
    Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
    DataScheme = "userpilot-APP_TOKEN",
    DataHost = "sdk")]
public class MainActivity : MauiAppCompatActivity
{
}
```

Keep `DataHost = "sdk"` in the intent filter. It generates `android:host="sdk"` in the Android manifest, matching the host Userpilot validates for preview and push-notification URLs.

### iOS

Add the scheme to `Platforms/iOS/Info.plist`:

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

***

## Handle the custom URL scheme

`UserpilotSdk.DidHandleUrl` is the single entry point on both platforms. It accepts either a `string` or, on iOS, an `NSUrl`. URLs that arrive before `UserpilotSdk.Setup` are cached and replayed once setup completes, so a cold launch from a link still works without you sequencing the calls.

### Android

Forward the incoming URL from both `OnCreate`, which covers a cold launch, and `OnNewIntent`, which covers a link arriving while the app runs. Handling only one of the two silently drops links in the other case.

```csharp theme={null}
protected override void OnCreate(Bundle? savedInstanceState)
{
    base.OnCreate(savedInstanceState);

    HandleUserpilotUrl(Intent?.DataString);
}

protected override void OnNewIntent(Intent? intent)
{
    base.OnNewIntent(intent);

    HandleUserpilotUrl(intent?.DataString);
}

private bool HandleUserpilotUrl(string? url)
{
    if (string.IsNullOrEmpty(url))
    {
        return false;
    }

    if (UserpilotSdk.DidHandleUrl(url))
    {
        return true;
    }

    // Not a Userpilot link, so handle it in your app.
    return false;
}
```

### iOS

Override `OpenUrl` in your `AppDelegate`:

```csharp theme={null}
public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
{
    if (UserpilotSdk.DidHandleUrl(url))
    {
        return true;
    }

    // Not a Userpilot link, so handle it in your app.
    return base.OpenUrl(app, url, options);
}
```

***

## Supported links

The SDK only claims a URL when the scheme is one of the accepted values 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. Available on Android and iOS. |
| `userpilot-{token}://sdk/notification` | Routes a push notification tap to the SDK. Android only; on iOS notification taps are handled natively by the SDK. |

See [Push Notifications](./push-notifications) for the delivery setup on each platform.

***

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

Because a notification tap arrives as a deep link on Android, a test push also exercises your `DidHandleUrl` handling there. If the notification appears but tapping it does nothing, the scheme registration is the first thing to check.

***

## Test a deep link from the command line

With the app installed, open a link directly to confirm your scheme is registered and your handler runs, without going through the Builder. The sample app has a **Deep Link** screen that shows the registered scheme and a log of received links.

```bash theme={null}
# Android
adb shell am start -a android.intent.action.VIEW \
  -d "userpilot-maui-sample://sdk/experience_preview/12345"

# iOS Simulator
xcrun simctl openurl booted "userpilot-nx-12345678://sdk/experience_preview/12345"
```

Replace the scheme with the value you registered and `12345` with a real experience ID.

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="Nothing happens when I scan the QR code">
    Confirm the registered scheme matches your token exactly, lowercased. Verify the registration with the commands above: if `adb` reports no matching activity, the intent filter is the problem.
  </Accordion>

  <Accordion title="The app opens but the experience doesn't appear">
    The OS routed the link, so registration is correct and the problem is in the handling step. On Android, check that you forward the URL from **both** `OnCreate` and `OnNewIntent`, since a cold launch only passes through `OnCreate`. Also confirm the SDK is set up with the same token that generated the QR code.
  </Accordion>

  <Accordion title="A link opens a second copy of my activity">
    Set `LaunchMode = LaunchMode.SingleTop` on the activity. Without it, Android may create a new instance for the incoming link rather than delivering it to the running one through `OnNewIntent`.
  </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>
</AccordionGroup>

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