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

The Userpilot Cordova plugin 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.

Cordova doesn't deliver custom-scheme links to your web layer on its own, so this guide uses `cordova-plugin-deeplinks` to receive them. You declare the scheme once in `package.json`, Cordova writes the native configuration during the build, and your JavaScript forwards each link to the Userpilot SDK.

<Warning>
  **Pass the whole URL, not just the path**

  The SDK matches on the scheme and host, so it needs the complete URL. Forwarding only `eventData.path` means no link is ever recognized.
</Warning>

***

## Register the custom URL scheme

### Install the deep links plugin

```bash theme={null}
cordova plugin add cordova-plugin-deeplinks
```

### Configure the plugin

Add the configuration below to your `package.json`, replacing `APP_TOKEN` with your app's Userpilot token from your [Environments Page](https://run.userpilot.io/environment). For example, if your token is `NX-12345678`, your scheme value is `userpilot-nx-12345678`.

```json theme={null}
"plugins": {
  "@userpilot/cordova": {
    "DEPLOYMENT-TARGET": "13.0"
  },
  "cordova-plugin-deeplinks": {
    "URL_SCHEME": "userpilot-APP_TOKEN",
    "DEEPLINK_HOST": "sdk",
    "ANDROID_PATH_PREFIX": "/"
  }
}
```

`DEEPLINK_HOST` must be `sdk`, because that's the host the SDK matches on.

### Generated configuration

From the plugin configuration above, Cordova adds the following to your `config.xml`:

```xml theme={null}
<!-- Deep Links Configuration for cordova-plugin-deeplinks -->
<universal-links>
    <host name="sdk" scheme="userpilot-APP_TOKEN" event="deeplink" />
</universal-links>
```

The `event` value, `deeplink`, is the event name you subscribe to in JavaScript below.

After you build the project, the configuration is reflected in the native platform files. You don't edit these by hand, but they're useful for verifying the scheme landed correctly.

**iOS** — in `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>
```

**Android** — in `AndroidManifest.xml`, on the main Activity:

```xml theme={null}
<activity
    android:name="..."
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    <intent-filter>
        <data android:scheme="userpilot-APP_TOKEN" android:host="sdk" />
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
    </intent-filter>
</activity>
```

***

## Handle the custom URL scheme

Subscribe to the `deeplink` event and pass `eventData.url` to `userpilot.didHandleUrl(url, onSuccess, onFail)`. The success callback receives `true` when the SDK recognized and handled the link, and `false` when your app should handle it.

A link can arrive before `setup` has finished, particularly on a cold start from the Builder's QR code. Buffer that link and replay it once setup completes, otherwise the first preview after a launch is silently dropped.

```js theme={null}
var isUserpilotInitialized = false;
var pendingDeepLinkUrl = null;

function initializeUserpilot() {
  var options = {
    logging: true,
    useInAppBrowser: false,
    disableRequestPushNotificationsPermission: false,
  };

  userpilot.setup(
    'APP_TOKEN',
    options,
    function () {
      isUserpilotInitialized = true;
      setupDeepLinkHandler();

      // Replay a link that arrived before setup finished
      if (pendingDeepLinkUrl) {
        processDeepLink(pendingDeepLinkUrl);
        pendingDeepLinkUrl = null;
      }
    },
    function (error) {
      console.error('Userpilot setup error:', error);
    }
  );
}

function processDeepLink(url) {
  if (!url) return;

  // Hold the link until the SDK is ready to act on it
  if (!isUserpilotInitialized) {
    pendingDeepLinkUrl = url;
    return;
  }

  userpilot.didHandleUrl(
    url,
    function (result) {
      var handled = result === true || result === 'true';

      if (!handled) {
        // Handle a non-Userpilot URL
      }
    },
    function (error) {
      console.error('Userpilot didHandleUrl error:', error);
    }
  );
}

function setupDeepLinkHandler() {
  var ulPlugin =
    typeof universalLinks !== 'undefined'
      ? universalLinks
      : window.plugins && window.plugins.universalLinks;

  if (!ulPlugin) {
    console.error('universalLinks plugin not found');
    return;
  }

  // Subscribe to the event name configured in config.xml
  ulPlugin.subscribe('deeplink', function (eventData) {
    // eventData contains { url, host, path, scheme, hash, ... }
    processDeepLink(eventData.url);
  });
}
```

***

## 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/notification` | Routes an Android push notification tap to the SDK. Query parameters carry the notification payload. |

<Note>
  On iOS, notification taps are handled natively by the SDK and never reach the `deeplink` event, so follow [Push Notifications](./push-notifications) for that setup.
</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).

Because a notification tap arrives as a deep link on Android, a test push also exercises your `deeplink` 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

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

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

# iOS Simulator
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.

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="Nothing happens when I scan the QR code">
    Confirm `URL_SCHEME` in `package.json` matches your token exactly, lowercased, then rebuild so Cordova regenerates the native configuration. Check the generated `Info.plist` and `AndroidManifest.xml` shown above to confirm the scheme actually landed.
  </Accordion>

  <Accordion title="The `deeplink` event never fires">
    Two things to check. `cordova-plugin-deeplinks` must be installed and the app rebuilt after installing it. And the event name you subscribe to must match the `event` attribute in the generated `<universal-links>` entry in `config.xml`, which is `deeplink`.
  </Accordion>

  <Accordion title="The first preview after launching the app is ignored">
    The link arrived before `setup` completed, so the SDK wasn't ready to act on it. Buffer the URL and replay it from the `setup` success callback, as shown above.
  </Accordion>

  <Accordion title="Tapping an Android push notification does nothing">
    Notification taps travel over this same URL scheme on Android, so they need the scheme registration and the `deeplink` forwarding described above. Confirm `DEEPLINK_HOST` is `sdk`.
  </Accordion>

  <Accordion title="Preview works in production but not in staging">
    Each environment has its own token, and the scheme follows the token. Confirm `URL_SCHEME` matches the token that build initializes the SDK with, then rebuild.
  </Accordion>
</AccordionGroup>

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