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

# KYC Widget installation and setup

> Install the Uphold KYC Widget SDK and integrate it in web and native applications using sessions from the Widgets API and supported event handlers.

By the end of this guide the KYC Widget will be running in your app, mounted to a container, and emitting events you can react to.

## Before you start

<Warning>Before you can test the KYC Widget in Sandbox or Production, Uphold must complete a one-time internal setup to enable identity verification for your account. Contact your Account Manager to have this provisioned ahead of your integration.</Warning>

You'll need:

* Access to the **[Widgets API](/rest-apis/widgets-api/kyc/create-session)** to create widget sessions. Manage your access in the [Enterprise Portal](https://portal.enterprise.uphold.com).
* A **backend** that can call the Widgets API to create sessions on behalf of your users.
* A **frontend** — either a web app (iframe) or a native app (WebView) — to embed the Widget.

The Widget runs from one of two hosts depending on environment:

| Environment | Host                                               |
| ----------- | -------------------------------------------------- |
| Sandbox     | `https://kyc-widget.enterprise.sandbox.uphold.com` |
| Production  | `https://kyc-widget.enterprise.uphold.com`         |

## Setup

### 1. Install the SDK

Install the SDK in the frontend that will host the Widget — your web app, or the JS bundle loaded by your native WebView.

```bash theme={null}
npm install @uphold/enterprise-kyc-widget-web-sdk
```

### 2. Allow the Widget domain in your CSP

The Widget loads in an iframe. If your app uses a Content Security Policy, allow the Widget host for your environment(s) under `frame-src`.

```html theme={null}
<meta
  http-equiv="Content-Security-Policy"
  content="frame-src 'self' https://kyc-widget.enterprise.sandbox.uphold.com https://kyc-widget.enterprise.uphold.com;"
>
```

<Note>If your app does not use CSP, skip this step.</Note>

### 3. Create a session on your backend

The KYC Widget runs against a session — a short-lived, server-side authorization scoped to one flow and one user. Create it server-side using your OAuth credentials.

<Warning>Never call the Create Session endpoint from the client. Your Client Secret must not leave your backend.</Warning>

Call [`POST /widgets/kyc/sessions`](/rest-apis/widgets-api/kyc/create-session) with the `verify` flow and, optionally, the processes you want the user to complete:

```bash theme={null}
curl -X POST https://api.sandbox.uphold.com/widgets/kyc/sessions \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "X-On-Behalf-Of: user $USER_ID" \
  -H "Content-Type: application/json" \
  -d '{ "flow": "verify", "processes": ["identity", "profile", "proof-of-address"] }'
```

The response wraps the session object:

```json theme={null}
{
  "session": {
    "flow": "verify",
    "data": {
      "processes": ["identity", "profile", "proof-of-address"]
    },
    "url": "https://kyc-widget.enterprise.sandbox.uphold.com/...",
    "token": "..."
  }
}
```

Pass `response.session` to your frontend (e.g. as part of your page response or via your own API endpoint). The SDK takes it as a single `session` argument.

### 4. Initialize and mount the Widget

On the frontend, instantiate `KycWidget` with the session from Step 3, then mount it into a container element.

```javascript theme={null}
import { KycWidget } from '@uphold/enterprise-kyc-widget-web-sdk';

// session is the `session` property from the backend response in Step 3
const widget = new KycWidget(session, {
  theme: { appearance: 'dark' }, // omit to follow system preference
  debug: true                     // verbose logging during development
});

widget.mountIframe(document.getElementById('kyc-container'));
```

<Note>The container must have explicit CSS width and height — the iframe fills its bounds. Minimum recommended size is **400px × 600px**.</Note>

### 5. Handle Widget events

The Widget emits four events during its lifecycle. Wire up handlers before calling `mountIframe`.

| Event      | Fires when                            | What to do                                                                                              |
| ---------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `ready`    | The Widget has finished loading       | Hide your loading state                                                                                 |
| `complete` | The user submitted all KYC processes  | Call `widget.unmount()`, then monitor outcomes via [KYC webhooks](/rest-apis/core-api/kyc/introduction) |
| `cancel`   | The user dismissed the Widget         | Call `widget.unmount()`, return them to your flow                                                       |
| `error`    | The Widget hit an unrecoverable error | Read `event.detail.error`, call `widget.unmount()`, retry                                               |

<Warning>The Widget does not unmount itself. You must call `widget.unmount()` from `complete`, `cancel`, and `error` handlers.</Warning>

```javascript theme={null}
widget.on('ready', () => {
  console.log('KYC Widget is ready');
});

widget.on('complete', () => {
  console.log('KYC processes submitted');
  widget.unmount();
});

widget.on('cancel', () => {
  console.log('KYC cancelled');
  widget.unmount();
});

widget.on('error', (event) => {
  console.error('KYC error:', event.detail.error);
  widget.unmount();
});
```

<Note>The `complete` event signals that the user has submitted all required processes. Final verification outcomes (e.g. identity approved or rejected) are delivered asynchronously via [KYC webhooks](/rest-apis/core-api/kyc/introduction).</Note>

### 6. Test in Sandbox

With your Sandbox credentials and the Sandbox Widget host configured, run through this checklist:

1. The Widget mounts and `ready` fires.
2. Completing the verification flow fires `complete`.
3. Closing or dismissing the Widget fires `cancel`.
4. The browser console shows no CSP violations (look for "Refused to frame").

Once Sandbox is green, swap your OAuth credentials to Production. The Widget host is selected automatically by the session `url` returned from your backend — no client-side environment switching is needed.

## Native app integration

Native mobile apps embed the Widget through a WebView that loads an HTML page hosting the SDK. Events flow between native code and the WebView through a JavaScript bridge.

The setup is the same as the web flow above — install the SDK in your JS bundle, create the session on your backend, and instantiate `KycWidget`. The only addition is the bridge that forwards events to native code.

### WebView HTML template

Bundle this HTML with your app and load it in the WebView. The `sendToNativeApp` helper at the bottom forwards events to whichever bridge is available (iOS, Android, or React Native).

```html [expandable] theme={null}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>KYC Widget</title>
    <style>
      body { margin: 0; padding: 0; }
      #kyc-container { width: 100%; height: 100vh; }
    </style>
  </head>
  <body>
    <div id="kyc-container"></div>

    <!-- Include your JS bundle which contains the SDK -->
    <script src="your-bundle-with-sdk.js"></script>
    <script>
      window.addEventListener('load', () => {
        const session = createKycWidgetSession();
        const widget = new KycWidget(session);

        widget.on('complete', () => {
          sendToNativeApp('complete');
          widget.unmount();
        });

        widget.on('cancel', () => {
          sendToNativeApp('cancel');
          widget.unmount();
        });

        widget.on('error', (event) => {
          sendToNativeApp('error', event.detail.error);
          widget.unmount();
        });

        widget.mountIframe(document.getElementById('kyc-container'));
      });

      function sendToNativeApp(type, data = null) {
        const message = { type, data };

        if (window.webkit?.messageHandlers?.kycWidgetMessage) {
          window.webkit.messageHandlers.kycWidgetMessage.postMessage(message);
        } else if (window.KycBridge) {
          window.KycBridge.onMessage(JSON.stringify(message));
        } else if (window.ReactNativeWebView) {
          window.ReactNativeWebView.postMessage(JSON.stringify(message));
        }
      }
    </script>
  </body>
</html>
```

<Note>The SDK must be included in your WebView bundle (e.g. via your build pipeline). Loading it from a CDN is not supported.</Note>

### Platform setup

<Tabs>
  <Tab title="iOS (Swift)">
    ```swift [expandable] theme={null}
    import WebKit

    class KycViewController: UIViewController, WKScriptMessageHandler {
      @IBOutlet weak var webView: WKWebView!

      override func viewDidLoad() {
        super.viewDidLoad()

        // Register a single message handler for all Widget events
        let contentController = webView.configuration.userContentController
        contentController.add(self, name: "kycWidgetMessage")

        if let url = Bundle.main.url(forResource: "kyc-widget", withExtension: "html") {
          webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
        }
      }

      func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        guard message.name == "kycWidgetMessage",
              let messageDict = message.body as? [String: Any],
              let type = messageDict["type"] as? String else {
          return
        }

        switch type {
          case "complete": handleKycComplete()
          case "cancel":   handleKycCancel()
          case "error":    handleKycError(error: messageDict["data"])
          default: print("Unknown KYC Widget message type: \(type)")
        }
      }

      private func handleKycComplete() { /* navigate to success */ }
      private func handleKycCancel() { /* return user to previous screen */ }
      private func handleKycError(error: Any?) { /* show error UI */ }

      deinit {
        webView?.configuration.userContentController.removeScriptMessageHandler(forName: "kycWidgetMessage")
      }
    }
    ```
  </Tab>

  <Tab title="Android (Java/Kotlin)">
    ```java [expandable] theme={null}
    import android.webkit.WebView;
    import android.webkit.WebSettings;
    import android.webkit.JavascriptInterface;
    import android.util.Log;
    import org.json.JSONObject;

    WebView webView = findViewById(R.id.webview);
    WebSettings webSettings = webView.getSettings();
    webSettings.setJavaScriptEnabled(true);

    webView.addJavascriptInterface(new KycBridge(), "KycBridge");
    webView.loadUrl("file:///android_asset/kyc-widget.html");

    public class KycBridge {
      @JavascriptInterface
      public void onMessage(String messageJson) {
        try {
          JSONObject message = new JSONObject(messageJson);
          String type = message.getString("type");
          Object data = message.opt("data");

          runOnUiThread(() -> {
            switch (type) {
              case "complete": handleKycComplete(); break;
              case "cancel":   handleKycCancel(); break;
              case "error":    handleKycError(data != null ? data.toString() : null); break;
              default: Log.w("Kyc", "Unknown KYC Widget message type: " + type);
            }
          });
        } catch (Exception e) {
          Log.e("Kyc", "Error parsing KYC Widget message", e);
        }
      }

      private void handleKycComplete() { /* navigate to success */ }
      private void handleKycCancel() { /* return user to previous screen */ }
      private void handleKycError(String error) { /* show error UI */ }
    }
    ```

    <Note>If you load local assets, ensure the WebView allows file access according to your security requirements (e.g. `setAllowFileAccess(true)` when needed).</Note>
  </Tab>

  <Tab title="React Native">
    ```javascript [expandable] theme={null}
    import { WebView } from 'react-native-webview';
    import { Alert } from 'react-native';

    const KycScreen = () => {
      const handleMessage = (event) => {
        try {
          const message = JSON.parse(event.nativeEvent.data);

          switch (message.type) {
            case 'complete': handleKycComplete(); break;
            case 'cancel':   handleKycCancel(); break;
            case 'error':    handleKycError(message.data); break;
            default: console.log('Unknown message type:', message.type);
          }
        } catch (error) {
          console.error('Error parsing WebView message:', error);
        }
      };

      const handleKycComplete = () => { /* navigate to success */ };
      const handleKycCancel = () => { /* return user to previous screen */ };
      const handleKycError = (error) => { /* show error UI */ };

      return (
        <WebView
          source={{ uri: 'file:///path/to/kyc-widget.html' }}
          javaScriptEnabled={true}
          domStorageEnabled={true}
          onMessage={handleMessage}
        />
      );
    };
    ```

    <Note>For iOS, `file://` URLs may be restricted. Consider `source={{ html: '<html>...</html>' }}` or a bundled asset, adjusted per platform.</Note>
  </Tab>
</Tabs>

## Configuration reference

The most common SDK options. See the [SDK reference](./sdk-reference#options) for the full schema and all event types.

| Option  | Purpose                                                                                                                                                                                                               |
| ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `theme` | Customize the Widget's appearance: force `light` or `dark` mode, set brand colors, typography, and per-component border radii. See [WidgetThemeOption](/widgets/kyc/sdk-reference#widgetthemeoption) for all options. |
| `debug` | Verbose console logging. Use during development.                                                                                                                                                                      |

## Troubleshooting

**Widget not displaying**

Confirm the Widget host for your environment is in the `frame-src` directive of your CSP (see Step 2). Open DevTools → Console and look for `Refused to frame` violations.

**Container is empty after mount**

The iframe fills its container — the container must have explicit CSS width and height. Minimum recommended size is 400px × 600px.

**Events not firing in native apps**

Verify that:

* JavaScript is enabled in the WebView.
* The message bridge is registered **before** the HTML page loads.
* Event handler names match the platform-specific bridge contract used in `sendToNativeApp`.

**Widget never unmounts.** The SDK does not auto-unmount. Call `widget.unmount()` from each terminal handler (`complete`, `cancel`, `error`).

## Next steps

* Review the complete [SDK Reference](./sdk-reference) for all available methods and events.
* Follow the [user onboarding guide](/developer-guides/user-onboarding/individual/via-kyc-widget) for a step-by-step walkthrough of verifying users with the KYC Widget.
