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

# Onboard individual users via the KYC Widget

> Collect KYC data from individual users using the Uphold KYC Widget, without building a custom verification interface.

The [KYC Widget](/widgets/kyc/introduction) is an embeddable UI component for collecting KYC data from individual users. Instead of building and maintaining a custom verification interface, you embed the Widget and it handles forms, document uploads, and process state on your behalf. It's fully customizable — themes, fonts, and brand colors — so it can match your app's look and feel. See the [KYC Widget SDK reference](/widgets/kyc/sdk-reference) for the full configuration reference.

The KYC Widget currently covers four processes: `profile`, `identity`, `proof-of-address`, and `customer-due-diligence`. Any remaining required processes (e.g. `enhancedDueDiligence`, `taxDetails`) must still be completed via the [REST API](/developer-guides/user-onboarding/individual/via-api).

## Prerequisites

* API client credentials with the scopes to create users and access the KYC Widget.
* The KYC Widget SDK is installed in your frontend. See [Installation and setup](/widgets/kyc/installation-and-setup).

## Walkthrough

```mermaid theme={null}
sequenceDiagram
  autonumber
  participant U as Client App
  participant B as Your Backend
  participant A as Core API
  participant K as KYC Widget
  participant W as Webhooks

  U->>B: User provides personal, address, and contact details
  B->>A: GET /core/terms-of-service?type=general&country={country}
  A-->>B: { termsOfService[] }
  B-->>U: Display Terms of Service
  U->>B: User accepts Terms of Service
  B->>A: POST /core/users (with ToS + X-Uphold-User-Ip)
  A-->>B: { user }
  B->>A: POST /widgets/kyc/sessions
  A-->>B: { session }
  B-->>U: { session }
  U->>K: Initialize Widget
  U->>K: User completes verification processes
  K-->>U: complete
  U->>U: Unmount Widget, show pending state
  A-->>W: core.kyc.*.status-changed (for each process)
  B->>A: GET /core/kyc (verify process statuses)
  B->>A: PATCH /core/kyc/* (complete remaining processes via API)
  B->>A: GET /core/capabilities
  A-->>B: { capabilities[] }
  B-->>U: User is ready to transact
```

## Retrieve terms of service

Before creating a user, retrieve the general Terms of Service applicable to their country of residence by calling the [List terms of service](/rest-apis/core-api/terms-of-service/list-terms-of-service) endpoint with `type=general` and the user's country code.

```http theme={null}
GET /core/terms-of-service?type=general&country={country}
```

Display the Terms of Service content to the user and record their acceptance.

## Create the user

Once the user has accepted the Terms of Service, call [Create user](/rest-apis/core-api/users/create-user) to register them on the platform.

<Tip>The `X-Uphold-User-Ip` [user context](/rest-apis/headers#user-context) header is **mandatory** when creating a user, as it records the user's IP address at the time of Terms of Service acceptance.</Tip>

<Tabs id="create-user-country-examples" sync={false}>
  <Tab id="create-user-us" title="US" icon="https://mintcdn.com/uphold-d4756e17/RUSkzbZyWcGyBsHE/_media/flags/US.svg?fit=max&auto=format&n=RUSkzbZyWcGyBsHE&q=85&s=83017a81447fcec6d7cf24e0742eaa72" width="40" height="40" data-path="_media/flags/US.svg">
    ```http theme={null}
    POST /core/users
    {
      "type": "individual",
      "email": "john.doe@example.com",
      "termsOfService": "general-us-hq",
      "fullName": "John Doe",
      "birthdate": "1987-01-01",
      "primaryCitizenship": "US",
      "address": {
        "country": "US",
        "subdivision": "US-CA",
        "city": "Los Angeles",
        "line1": "100 Main Street",
        "postalCode": "90011"
      },
      "phone": {
        "number": "+12125550123",
        "country": "US"
      }
    }
    ```

    A successful response returns the created user's information.

    ```json Response theme={null}
    {
      "user": {
        "id": "cd21b26d-35d2-408a-9201-b8fdbef7a604",
        "type": "individual",
        "email": "john.doe@uphold.com",
        "fullName": "John Doe",
        "birthdate": "1987-01-01",
        "primaryCitizenship": "US",
        "address": {
          "country": "US",
          "subdivision": "US-CA",
          "city": "Los Angeles",
          "line1": "100 Main Street",
          "postalCode": "90011"
        },
        "phone": {
          "number": "+12125550123",
          "country": "US"
        },
        "createdAt": "2024-03-13T20:20:39Z",
        "updatedAt": "2024-03-13T20:20:39Z"
      }
    }
    ```
  </Tab>

  <Tab id="create-user-gb" title="GB" icon="https://mintcdn.com/uphold-d4756e17/RUSkzbZyWcGyBsHE/_media/flags/GB.svg?fit=max&auto=format&n=RUSkzbZyWcGyBsHE&q=85&s=1042597fce92aad80e43f6a4f042ef58" width="40" height="40" data-path="_media/flags/GB.svg">
    ```http theme={null}
    POST /core/users
    {
      "type": "individual",
      "email": "john.doe@example.com",
      "termsOfService": "general-gb-fca",
      "fullName": "John Doe",
      "birthdate": "1987-01-01",
      "primaryCitizenship": "GB",
      "address": {
        "country": "GB",
        "subdivision": "GB-MAN",
        "city": "Manchester",
        "line1": "1 High Street",
        "postalCode": "M4 1AA"
      },
      "phone": {
        "number": "+447911123456",
        "country": "GB"
      }
    }
    ```

    A successful response returns the created user's information.

    ```json Response theme={null}
    {
      "user": {
        "id": "cd21b26d-35d2-408a-9201-b8fdbef7a604",
        "type": "individual",
        "email": "john.doe@uphold.com",
        "fullName": "John Doe",
        "birthdate": "1987-01-01",
        "primaryCitizenship": "GB",
        "address": {
          "country": "GB",
          "subdivision": "GB-MAN",
          "city": "Manchester",
          "line1": "1 High Street",
          "postalCode": "M4 1AA"
        },
        "phone": {
          "number": "+447911123456",
          "country": "GB"
        },
        "createdAt": "2024-03-13T20:20:39Z",
        "updatedAt": "2024-03-13T20:20:39Z"
      }
    }
    ```
  </Tab>
</Tabs>

You can optionally include custom [entity metadata](/rest-apis/entity-metadata) in the `metadata` field to store your own business data (e.g. external IDs or tracking parameters).

Subscribe to the `core.user.created` webhook to be notified asynchronously.

## Create a session

Call [Create kyc session](/rest-apis/widgets-api/kyc/create-session) to start the `verify` flow. Specify `processes` to limit which verification steps the user sees, or omit it to run all available processes. To customize the theme and layout, see the example [here](/widgets/kyc/installation-and-setup#1-create-a-session-on-your-backend).

```http theme={null}
POST /widgets/kyc/sessions
{
  "flow": "verify",
  "processes": ["customer-due-diligence", "identity", "proof-of-address"]
}
```

The response contains a `session` object with a `url` field that loads the widget. Return `response.session` to your frontend as is.

```json theme={null}
{
  "session": {
    "url": "https://kyc-widget.enterprise.uphold.com/?sessionToken=..."
  }
}
```

<Note>The order of the `processes` array does not affect the order the user sees. The Widget always presents the processes you requested in a fixed sequence: `profile`, `identity`, `proof-of-address`, then `customer-due-diligence`.</Note>

## Set up the Widget

Initialize the widget for the `verify` flow using the session data returned from the API.

<CodeGroup>
  ```javascript Web SDK [expandable] theme={null}
  import { KycWidget } from '@uphold/enterprise-kyc-widget-web-sdk';

  // session is `response.session` from your backend
  const widget = new KycWidget(session);

  widget.on('ready', () => {
    // Hide your loading state
  });

  widget.on('complete', () => {
    widget.unmount();
    // Show a pending state — verification outcome arrives via webhook
  });

  widget.on('cancel', () => {
    widget.unmount();
    // Return the user to the previous screen
  });

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

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

  ```html Without SDK [expandable] theme={null}
  <div id="kyc-container"></div>

  <script>
    // session is `response.session` from your backend
    async function initializeKycWidget(session) {
      const container = document.getElementById('kyc-container');
      const sessionOrigin = new URL(session.url).origin;

      const iframe = document.createElement('iframe');
      iframe.src = session.url;
      iframe.setAttribute('allow', "clipboard-write 'src'; clipboard-read 'src';");
      iframe.style.width = '100%';
      iframe.style.height = '100%';
      iframe.style.border = 'none';

      function teardown() {
        window.removeEventListener('message', onMessage);
        iframe.remove();
      }

      function onMessage(event) {
        if (event.origin !== sessionOrigin) return;

        switch (event.data?.type) {
          case 'load':
            // Optional: repeat theme or layout from Create session to avoid a brief flash of defaults
            iframe.contentWindow.postMessage({ options: {}, type: 'init' }, sessionOrigin);
            break;
          case 'ready':
            // Hide your loading state
            break;
          case 'complete':
            teardown();
            // Show a pending state — verification outcome arrives via webhook
            break;
          case 'cancel':
            teardown();
            // Return the user to the previous screen
            break;
          case 'error':
            console.error('KYC error:', event.data.error);
            teardown();
            break;
        }
      }

      window.addEventListener('message', onMessage);
      container.appendChild(iframe);
    }
  </script>
  ```
</CodeGroup>

<Info>
  Both examples above are for web applications — either creating the iframe yourself or letting the SDK do it. For native apps using a WebView, see [Native apps with the Web SDK](/widgets/kyc/installation-and-setup#native-apps-with-the-web-sdk) for the SDK's native-bundling pattern, or [Setup without the SDK](/widgets/kyc/installation-and-setup#setup-without-the-sdk) for the no-SDK approach that loads the session `url` directly as the WebView's top-level page.
</Info>

## Customer due diligence

Customer due diligence (CDD) collects the user's financial profile — source of funds, purpose of the account, employment status — and produces a risk score. Unlike `identity` and `proof-of-address`, it is a **form-based** process: its fields vary by region, expected activity, and previous answers, and it is scored on submission rather than reviewed afterwards. Once the user completes the final step, the process returns `ok` straight away, with no in-review state to wait on. A high risk score instead triggers [enhanced due diligence](#enhanced-due-diligence).

### Periodic recollection

Customer due diligence expires. The process exposes an `output.expiresAt` deadline and reopens for recollection 60 days before that date, at which point its status returns to `pending`. The user stays approved and can keep transacting throughout that window.

To let the user resubmit, create a new session that includes `customer-due-diligence` and mount the Widget again. Poll [Get KYC overview](/rest-apis/core-api/kyc/get-overview) or watch the `core.kyc.customer-due-diligence.status-changed` webhook to know when recollection has opened.

### Enhanced due diligence

If the resulting risk score categorizes the user as high risk, an [`enhancedDueDiligence`](/rest-apis/core-api/kyc/update-enhanced-due-diligence) process is triggered. **The Widget does not cover enhanced due diligence** — collect it via the [REST API](/developer-guides/user-onboarding/individual/via-api). Submit it only when prompted.

## Monitor outcomes

The `complete` event signals submission, not approval. `identity` and `proof-of-address` may come back settled or still under review, so read the status rather than assuming. `customer-due-diligence` is already settled when the Widget closes — see [Customer due diligence in the Widget](#customer-due-diligence-in-the-widget).

Subscribe to the relevant events for the processes you requested:

* [`core.kyc.identity.status-changed`](/rest-apis/core-api/kyc/webhooks/identity-status-changed) — identity verification result
* [`core.kyc.proof-of-address.status-changed`](/rest-apis/core-api/kyc/webhooks/proof-of-address-status-changed) — proof of address result
* [`core.kyc.customer-due-diligence.status-changed`](/rest-apis/core-api/kyc/webhooks/customer-due-diligence-status-changed) — customer due diligence result, and reopening for recollection

Call [Get KYC overview](/rest-apis/core-api/kyc/get-overview) afterwards to pick up any [`enhancedDueDiligence`](#enhanced-due-diligence) process the submission triggered — the Widget cannot collect it.

<Warning>In Sandbox, proof-of-address submissions are automatically approved — no validation is performed on the documents you submit for this process through the Widget at the moment.</Warning>

You can also poll [Get KYC overview](/rest-apis/core-api/kyc/get-overview) (`GET /core/kyc`) at any time to check the current status of all KYC processes for a user.
