> ## 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 REST API

> Step-by-step guide to onboarding individual users via the Uphold REST API: create the user, complete KYC processes, and unlock capabilities.

This guide walks through the complete individual user onboarding flow using the REST API — from creating the user to confirming their capabilities are unlocked.

## Prerequisites

* API client credentials with permission to create users and manage KYC.

## Walkthrough

```mermaid theme={null}
sequenceDiagram
  autonumber
  participant U as Client App
  participant B as Your Backend
  participant A as Core API
  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 }
  A-->>W: core.user.created
  B->>A: GET /core/kyc
  A-->>B: { processes with statuses }
  B->>A: PATCH /core/kyc/processes/email
  B->>A: POST /core/files + PATCH /core/kyc/processes/identity
  A-->>W: core.kyc.*.status-changed (for each process)
  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>

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

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.

## Check required processes

The KYC processes required for a user are determined by the Terms of Service they accepted at registration — different ToS codes map to different regulatory regimes with different requirements. After creating the user, call [Get KYC overview](/rest-apis/core-api/kyc/get-overview) to see the full list of applicable processes and their current statuses.

```http theme={null}
GET /core/kyc
```

A successful response returns the list of processes with their statuses. Processes still waiting for the user to provide information or verification start as `pending`, while any process whose data was already completed at [user creation](#create-the-user) (e.g. `profile`) may already be `ok`.

```json Response [expandable] theme={null}
{
  "email": {
    "code": "email",
    "status": "pending"
  },
  "phone": {
    "code": "phone",
    "status": "pending"
  },
  "profile": {
    "code": "profile",
    "status": "ok"
  },
  "identity": {
    "code": "identity",
    "status": "pending"
  },
  "proofOfAddress": {
    "code": "proof-of-address",
    "status": "pending"
  },
  "customerDueDiligence": {
    "code": "customer-due-diligence",
    "status": "pending"
  },
  "enhancedDueDiligence": {
    "code": "enhanced-due-diligence",
    "status": "exempt"
  },
  "cryptoRiskAssessment": {
    "code": "crypto-risk-assessment",
    "status": "exempt"
  },
  "selfCategorizationStatement": {
    "code": "self-categorization-statement",
    "status": "exempt"
  },
  "taxDetails": {
    "code": "tax-details",
    "status": "pending"
  },
  "screening": {
    "code": "screening",
    "status": "pending"
  },
  "risk": {
    "code": "risk",
    "status": "pending"
  }
}
```

Processes with `status: "exempt"` are not required for this user's region. See the [regional requirements](/developer-guides/user-onboarding/individual/overview#regional-requirements) for the full requirements matrix per region.

## Complete KYC processes

| Process                                                       | Category               |
| ------------------------------------------------------------- | ---------------------- |
| [email](#email-and-phone), [phone](#email-and-phone)          | Direct submission      |
| [profile](#profile)                                           | Direct submission      |
| [identity](#identity)                                         | File-based             |
| [proofOfAddress](#proof-of-address)                           | File-based             |
| [customerDueDiligence](#customer-due-diligence)               | Form-based             |
| [enhancedDueDiligence](#enhanced-due-diligence)               | File-based             |
| [cryptoRiskAssessment](#crypto-risk-assessment)               | Form-based             |
| [selfCategorizationStatement](#self-categorization-statement) | Form-based             |
| [taxDetails](#tax-details)                                    | Form-based             |
| [screening](#screening-and-risk), [risk](#screening-and-risk) | Background (automatic) |

<Note>`profile` may already be `ok` if you provided that data at [user creation](#create-the-user) — check the process status before submitting it again.</Note>

Some processes have dependencies (`verification.dependencies`) that must be satisfied before submission:

* `identity` and `proofOfAddress` depend on `phone` and `profile` being completed — the data collected in these processes is used for data matching against the submitted documents.
* `enhancedDueDiligence` is only triggered after `customerDueDiligence` returns a high-risk score — submit it only when prompted.
* `taxDetails` depends on `profile` being completed — the user details are combined with tax information to correctly set up the user's tax configuration.
* All other processes can be submitted in any order or in parallel.

***

### Email and phone

Verify the user's email and phone.

<Tabs sync={false}>
  <Tab title="Partner-verified">
    Call [Update email](/rest-apis/core-api/kyc/update-email) with `input` containing the email address and `output` containing the verification datetime.

    ```http theme={null}
    PATCH /core/kyc/processes/email
    {
      "input": {
        "email": "john.doe@example.com"
      },
      "output": {
        "verifiedAt": "2025-01-29T10:32:00Z"
      }
    }
    ```

    A successful response returns the submitted information with an `ok` status.

    ```json Response theme={null}
    {
      "email": {
        "code": "email",
        "status": "ok",
        "verification": {
          "model": "partner-verified",
          "method": "manual",
          "dependencies": []
        },
        "input": {
          "email": "john.doe@example.com"
        },
        "output": {
          "verifiedAt": "2025-01-29T10:32:00Z"
        }
      }
    }
    ```
  </Tab>

  <Tab title="Uphold-verified">
    <Callout icon="person-digging" color="#F59E0B" iconType="regular">This mode is under construction. Contact your Account Manager for the latest availability.</Callout>
  </Tab>
</Tabs>

Verify the user's phone the same way, calling [Update phone](/rest-apis/core-api/kyc/update-phone) with the phone number and verification datetime.

***

### Profile

Declare the user's personal information and residential address — name, date of birth, citizenship, and address — by calling [Update profile](/rest-apis/core-api/kyc/update-profile) with the relevant fields directly in `input`; if these were already supplied at [user creation](#create-the-user), this process may already be `ok`, so call the endpoint only to fill in missing fields or update existing ones.

```http theme={null}
PATCH /core/kyc/processes/profile
{
  "input": {
    "fullName": "John Doe",
    "birthdate": "1987-01-01",
    "primaryCitizenship": "GB",
    "address": {
      "city": "Manchester",
      "line1": "1 High Street",
      "postalCode": "M4 1AA"
    }
  }
}
```

A successful response returns the submitted information with an `ok` status, along with `verification.rules` describing which fields apply for this user and whether they can still be edited.

```json Response theme={null}
{
  "profile": {
    "code": "profile",
    "status": "ok",
    "verification": {
      "model": "uphold-verified",
      "method": "manual",
      "dependencies": [],
      "rules": {
        "fullName": { "presence": "required", "editable": true },
        "birthdate": { "presence": "required", "editable": true },
        "birthplace": { 
          "presence": "disallowed", 
          "editable": false,
          "rules": {
            "country": { "presence": "required", "editable": false },
            "town": { "presence": "required", "editable": false }
          }
        },
        "primaryCitizenship": { "presence": "required", "editable": true },
        "otherCitizenships": { "presence": "disallowed", "editable": false },
        "address": {
          "presence": "required",
          "editable": true,
          "rules": {
            "subdivision": { "presence": "required", "editable": true },
            "city": { "presence": "required", "editable": true },
            "line1": { "presence": "required", "editable": true },
            "line2": { "presence": "optional", "editable": true },
            "postalCode": { "presence": "required", "editable": true }
          }
        }
      }
    },
    "input": {
      "fullName": "John Doe",
      "birthdate": "1987-01-01",
      "primaryCitizenship": "GB",
      "address": {
        "country": "GB",
        "subdivision": "GB-MAN",
        "city": "Manchester",
        "line1": "1 High Street",
        "postalCode": "M4 1AA"
      }
    }
  }
}
```

Fields that are disallowed or no longer editable for this user (e.g. additional citizenships) don't fail the request — they're silently ignored and listed in a `warnings` object on the response.

```json Response theme={null}
{
  "profile": {
    // ... profile fields ...
  },
  "warnings": {
    "input": [
      {
        "code": "verification_rule_violated",
        "message": "Field 'input.otherCitizenships' was ignored as its presence is disallowed",
        "details": {
          "property": "input.otherCitizenships",
          "rule": "presence"
        }
      }
    ]
  }
}
```

***

### Identity

Verify that the user is the person they claim to be.

<Tabs sync={false}>
  <Tab title="Partner-verified">
    Your organization performs identity verification through an already-approved method and submits the result to Uphold.

    Two sub-types are supported:

    <AccordionGroup>
      <Accordion title="Document submission" icon="id-card">
        <Steps>
          <Step title="Create files">
            Call [Create file](/rest-apis/core-api/files/create-file) for each document to obtain an upload URL.

            ```http theme={null}
            POST /core/files
            {
              "category": "image",
              "contentType": "image/png"
            }
            ```

            A successful response returns an `upload` object with a presigned URL and form data for uploading the file to the provided storage service.

            ```json Response theme={null}
            {
              "file": {
                "id": "38cce774-b225-41ed-a9fd-f9c9777a13de",
                "status": "pending",
                "category": "image",
                "contentType": "image/png",
                "upload": {
                  "url": "https://example.com/upload",
                  "formData": {
                    "X-Amz-Algorithm": "AWS4-HMAC-SHA256",
                    "X-Amz-Credential": "ASIE4FQORBNQHNQD5CG6/20240801/us-east-1/s3/aws4_request",
                    "X-Amz-Date": "20240801T102500Z",
                    "X-Amz-Security-Token": "IQoJb3JpZ2luX2VjEIv//////////...JBr6h2HkzH/aFSArQcs=",
                    "X-Amz-Signature": "b4da8cf1a59327988a8108c965a4789fc8ba2d20f5bffda076106b2b254def29",
                    "Key": "4c13b6f5-987e-43df-bc12-042b58307a80",
                    "Policy": "eyJjb25kaXRpb25zIjpbeyJidWN...TA6MjU6MDAuMjAyWiJ9"
                  },
                  "expiresAt": "2024-03-13T20:20:39Z"
                }
              }
            }
            ```
          </Step>

          <Step title="Upload the file">
            Upload the file to the provided `upload.url` using the returned `upload.formData` fields as multipart form parameters.
          </Step>

          <Step title="Submit the process">
            Call [Update identity](/rest-apis/core-api/kyc/update-identity), referencing the file IDs in `input.media` and setting `output` to the verification results from your provider.

            ```http theme={null}
            PATCH /core/kyc/processes/identity
            {
              "type": "document-submission",
              "input": {
                "media": [
                  { "context": "photo-document-front", "fileId": "470b2192-893f-4ce6-9daa-816d4c319a84" },
                  { "context": "photo-selfie", "fileId": "5d3a1c72-bd4f-4e2a-a123-9f8765432100" }
                ]
              },
              "output": {
                "provider": "veriff",
                "document": {
                  "type": "passport",
                  "number": "7700225VH",
                  "country": "GB",
                  "expiresAt": "2026-03-13"
                },
                "person": {
                  "givenName": "John",
                  "familyName": "Doe",
                  "birthdate": "1987-01-01",
                  "gender": "male"
                },
                "verifiedAt": "2025-01-29T10:36:00Z"
              }
            }
            ```

            A successful response returns the submitted information with an `ok` status.

            ```json Response theme={null}
            {
              "identity": {
                "code": "identity",
                "status": "ok",
                "type": "document-submission",
                "verification": {
                  "model": "partner-verified",
                  "method": "manual",
                  "dependencies": ["phone", "profile"],
                  "submittable": false
                },
                "input": {
                  "media": [
                    { "context": "photo-document-front", "fileId": "470b2192-893f-4ce6-9daa-816d4c319a84" },
                    { "context": "photo-selfie", "fileId": "5d3a1c72-bd4f-4e2a-a123-9f8765432100" }
                  ]
                },
                "output": {
                  "provider": "veriff",
                  "document": {
                    "type": "passport",
                    "number": "7700225VH",
                    "country": "GB",
                    "expiresAt": "2026-03-13"
                  },
                  "person": {
                    "givenName": "John",
                    "familyName": "Doe",
                    "birthdate": "1987-01-01",
                    "gender": "male"
                  },
                  "verifiedAt": "2025-01-29T10:36:00Z"
                }
              }
            }
            ```
          </Step>
        </Steps>
      </Accordion>

      <Accordion title="Electronic verification (e-IDV)" icon="fingerprint">
        Your organization performs electronic identity verification through an already-approved provider that checks the user's declared information against public and third-party records, without requiring physical documents, and submits the result to Uphold. Call [Update identity](/rest-apis/core-api/kyc/update-identity), populating `input.data` with the user's declared information and `output` with the provider and verification datetime.

        ```http theme={null}
        PATCH /core/kyc/processes/identity
        {
          "type": "electronic-verification",
          "input": {
            "data": {
              "givenName": "John",
              "familyName": "Doe",
              "birthdate": "1987-01-01",
              "citizenshipCountry": "GB",
              "address": {
                "country": "GB",
                "city": "Manchester",
                "line1": "1 High Street",
                "postalCode": "M4 1AA"
              }
            }
          },
          "output": {
            "provider": "onfido",
            "verifiedAt": "2025-01-29T10:36:00Z"
          }
        }
        ```

        A successful response returns the submitted information with an `ok` status.

        ```json Response theme={null}
        {
          "identity": {
            "code": "identity",
            "status": "ok",
            "type": "electronic-verification",
            "verification": {
              "model": "partner-verified",
              "method": "manual",
              "dependencies": ["phone", "profile"],
              "submittable": false
            },
            "input": {
              "data": {
                "givenName": "John",
                "familyName": "Doe",
                "birthdate": "1987-01-01",
                "citizenshipCountry": "GB",
                "address": {
                  "country": "GB",
                  "city": "Manchester",
                  "line1": "1 High Street",
                  "postalCode": "M4 1AA"
                }
              }
            },
            "output": {
              "provider": "onfido",
              "verifiedAt": "2025-01-29T10:36:00Z"
            }
          }
        }
        ```

        <Note>This type does not unblock the same capabilities as document submission. See the [comparison table](/rest-apis/core-api/kyc/update-identity#when-to-use-document-submission-verification-vs-electronic-verification) for details.</Note>
      </Accordion>
    </AccordionGroup>
  </Tab>

  <Tab title="Uphold-verified">
    Uphold performs identity verification on the user's behalf. Two methods are supported:

    <CardGroup cols={2}>
      <Card title="Via KYC Widget" icon="puzzle-piece" href="/developer-guides/user-onboarding/individual/via-kyc-widget">
        Launch the Uphold-hosted KYC Widget to collect identity documents and complete verification through a pre-built UI.
      </Card>

      <Card title="Via KYC Connector" icon="plug" href="/developer-guides/user-onboarding/individual/via-kyc-connector">
        Ingest an existing identity verification from your KYC provider (Sumsub, Veriff) without building a custom mapping layer.
      </Card>
    </CardGroup>
  </Tab>
</Tabs>

***

### Proof of address

Verify the user's residential address. The process may already be `ok` in certain regions (e.g., US) if the `identity` process was completed with an identity document containing an address.

<Tabs sync={false}>
  <Tab title="Partner-verified">
    Your organization performs proof-of-address verification through a contracted provider and submits the result to Uphold.

    Two sub-types are supported:

    <AccordionGroup>
      <Accordion title="Document submission" icon="file-invoice">
        <Steps>
          <Step title="Create the file">
            Call [Create file](/rest-apis/core-api/files/create-file) to obtain an upload URL for the document.

            ```http theme={null}
            POST /core/files
            {
              "category": "document",
              "contentType": "image/png"
            }
            ```

            A successful response returns an `upload` object with a presigned URL and form data for uploading the file to the storage provider.

            ```json Response theme={null}
            {
              "file": {
                "id": "38cce774-b225-41ed-a9fd-f9c9777a13de",
                "status": "pending",
                "category": "document",
                "contentType": "image/png",
                "upload": {
                  "url": "https://example.com/upload",
                  "formData": {
                    "X-Amz-Algorithm": "AWS4-HMAC-SHA256",
                    "X-Amz-Credential": "ASIE4FQORBNQHNQD5CG6/20240801/us-east-1/s3/aws4_request",
                    "X-Amz-Date": "20240801T102500Z",
                    "X-Amz-Security-Token": "IQoJb3JpZ2luX2VjEIv//////////...JBr6h2HkzH/aFSArQcs=",
                    "X-Amz-Signature": "b4da8cf1a59327988a8108c965a4789fc8ba2d20f5bffda076106b2b254def29",
                    "Key": "4c13b6f5-987e-43df-bc12-042b58307a80",
                    "Policy": "eyJjb25kaXRpb25zIjpbeyJidWN...TA6MjU6MDAuMjAyWiJ9"
                  },
                  "expiresAt": "2024-03-13T20:20:39Z"
                }
              }
            }
            ```
          </Step>

          <Step title="Upload the file">
            Upload the file to the provided `upload.url` using the returned `upload.formData` fields as multipart form parameters.
          </Step>

          <Step title="Submit the process">
            Call [Update proof-of-address](/rest-apis/core-api/kyc/update-proof-of-address), referencing the file ID in `input.media` and setting `output` to the verification results from your provider.

            ```http theme={null}
            PATCH /core/kyc/processes/proof-of-address
            {
              "type": "document-submission",
              "input": {
                "media": [
                  { "context": "address-proof", "fileId": "459c0447-8916-4bad-bb67-f2354fcfb10b" }
                ]
              },
              "output": {
                "provider": "sumsub",
                "person": {
                  "givenName": "John",
                  "familyName": "Doe",
                  "address": {
                    "country": "GB",
                    "subdivision": "GB-MAN",
                    "city": "Manchester",
                    "line1": "1 High Street",
                    "line2": "Northern Quarter",
                    "postalCode": "M4 1AA"
                  }
                },
                "verifiedAt": "2025-01-29T10:37:00Z"
              }
            }
            ```

            A successful response returns the submitted information with an `ok` status.

            ```json Response theme={null}
            {
              "proofOfAddress": {
                "code": "proof-of-address",
                "status": "ok",
                "type": "document-submission",
                "verification": {
                  "model": "partner-verified",
                  "method": "manual",
                  "dependencies": ["phone", "profile"]
                },
                "input": {
                  "media": [
                    { "context": "address-proof", "fileId": "459c0447-8916-4bad-bb67-f2354fcfb10b" }
                  ]
                },
                "output": {
                  "provider": "sumsub",
                  "person": {
                    "givenName": "John",
                    "familyName": "Doe",
                    "address": {
                      "country": "GB",
                      "subdivision": "GB-MAN",
                      "city": "Manchester",
                      "line1": "1 High Street",
                      "line2": "Northern Quarter",
                      "postalCode": "M4 1AA"
                    }
                  },
                  "verifiedAt": "2025-01-29T10:37:00Z"
                }
              }
            }
            ```
          </Step>
        </Steps>
      </Accordion>

      <Accordion title="Electronic verification" icon="fingerprint">
        Some KYC providers can verify the user's address electronically, without requiring a physical document, by checking the declared address against public and third-party records.

        Call [Update proof-of-address](/rest-apis/core-api/kyc/update-proof-of-address), populating `input.data` with the user's declared information and `output` with the provider and verification datetime.

        ```http theme={null}
        PATCH /core/kyc/processes/proof-of-address
        {
          "type": "electronic-verification",
          "input": {
            "data": {
              "givenName": "John",
              "familyName": "Doe",
              "address": {
                "country": "GB",
                "subdivision": "GB-MAN",
                "city": "Manchester",
                "line1": "1 High Street",
                "line2": "Northern Quarter",
                "postalCode": "M4 1AA"
              }
            }
          },
          "output": {
            "provider": "sumsub",
            "verifiedAt": "2025-01-29T10:37:00Z"
          }
        }
        ```

        A successful response returns the submitted information with an `ok` status.

        ```json Response theme={null}
        {
          "proofOfAddress": {
            "code": "proof-of-address",
            "status": "ok",
            "type": "electronic-verification",
            "verification": {
              "model": "partner-verified",
              "method": "manual",
              "dependencies": ["phone", "profile"]
            },
            "input": {
              "data": {
                "givenName": "John",
                "familyName": "Doe",
                "address": {
                  "country": "GB",
                  "subdivision": "GB-MAN",
                  "city": "Manchester",
                  "line1": "1 High Street",
                  "line2": "Northern Quarter",
                  "postalCode": "M4 1AA"
                }
              }
            },
            "output": {
              "provider": "sumsub",
              "verifiedAt": "2025-01-29T10:37:00Z"
            }
          }
        }
        ```
      </Accordion>
    </AccordionGroup>
  </Tab>

  <Tab title="Uphold-verified">
    Uphold performs proof-of-address verification on the user's behalf. Two methods are supported:

    <CardGroup cols={2}>
      <Card title="Via KYC Widget" icon="puzzle-piece" href="/developer-guides/user-onboarding/individual/via-kyc-widget">
        Launch the Uphold-hosted KYC Widget to collect proof-of-address documents and complete verification through a pre-built UI.
      </Card>

      <Card title="Via KYC Connector" icon="plug" href="/developer-guides/user-onboarding/individual/via-kyc-connector">
        Ingest an existing proof-of-address verification from your KYC provider (Sumsub, Veriff) without building a custom mapping layer.
      </Card>
    </CardGroup>
  </Tab>
</Tabs>

***

### Customer due diligence

Assess the user's financial profile and risk level based on their expected activities, source of funds, and other relevant information.

<Tabs sync={false}>
  <Tab title="Uphold-verified">
    Uphold performs customer due diligence checks based on the user's profile and declared intent. Call [Update customer due diligence](/rest-apis/core-api/kyc/update-customer-due-diligence) with `input` containing the answers for each form step, repeating until `status` returns `ok`.

    <Note>Customer due diligence is a form-based process with multiple steps. The required fields may vary based on the user's region, expected activities, and other factors. Always check the returned `hint` object for the specific schema to display. See [Dynamic forms](/developer-guides/resources/dynamic-forms/introduction) for guidance on rendering the form and submitting the data.</Note>

    ```http theme={null}
    PATCH /core/kyc/processes/customer-due-diligence
    {
      "input": {
        "formId": "ae80271a-a3f0-453b-8dc6-36b777817217",
        "answers": {
          "intent": {
            "expected-activities": [
              "cryptocurrency_investing",
              "deposit_withdraw_crypto"
            ]
          }
        }
      }
    }
    ```

    A successful response returns the submitted information with an `ok` status, along with the calculated risk score and verification datetime.

    ```json Response theme={null}
    {
      "customerDueDiligence": {
        "code": "customer-due-diligence",
        "status": "ok",
        "verification": {
          "model": "uphold-verified",
          "method": "manual",
          "dependencies": []
        },
        "input": {
          "formId": "ae80271a-a3f0-453b-8dc6-36b777817217",
          "answers": {
            "intent": {
              "expected-activities": [
                "cryptocurrency_investing",
                "deposit_withdraw_crypto"
              ]
            },
            "financial-information": {
              "source-of-funds": "salary",
              "annual-income-range": "40000-60000-GBP",
              "expected-annual-deposits-range": "0-5000-GBP"
            },
            "employment": {
              "status": "employed"
            },
            "employment-details": {
              "industry": "engineering"
            }
          }
        },
        "output": {
          "score": "low",
          "expiresAt": "2025-01-01T00:00:00Z",
          "verifiedAt": "2022-01-01T00:00:00Z"
        }
      }
    }
    ```

    **Understanding score**

    The `output.score` reflects the user's assessed risk level. The higher the score, the shorter the recollection period before the user must resubmit.

    **Understanding expiration**

    The `output.expiresAt` is the deadline by which the user must resubmit customer due diligence — it opens for recollection 60 days before that date, at which point the process status reverts to `pending`. The user remains approved during this window and can continue to operate without restrictions until the deadline is reached.
  </Tab>

  <Tab title="Partner-verified">
    Your organization collects the user's financial profile information, calculates the user's risk score, then submits the result to Uphold. Call [Update customer due diligence](/rest-apis/core-api/kyc/update-customer-due-diligence) with `input` containing the user's financial profile information and `output` containing the calculated risk score and verification datetime.

    ```http theme={null}
    PATCH /core/kyc/processes/customer-due-diligence
    {
      "input": {
        "answers": {
          "financial-profile": {
            "expected-activities": [
              "cryptocurrency_investing"
            ],
            "source-of-funds": "salary",
            "annual-income-range": "40000-60000-GBP",
            "savings-and-investments-range": "0-5000-GBP",
            "employment-status": "employed",
            "employment-industry": "Engineering",
            "employment-occupation": "Galactic Vibe Curator"
          }
        }
      },
      "output": {
        "score": "low",
        "expiresAt": "2026-01-01T00:00:00Z",
        "verifiedAt": "2023-01-01T00:00:00Z"
      }
    }
    ```

    A successful response returns the submitted information with an `ok` status.

    ```json Response theme={null}
    {
      "customerDueDiligence": {
        "code": "customer-due-diligence",
        "status": "ok",
        "verification": {
          "model": "partner-verified",
          "method": "manual",
          "dependencies": []
        },
        "input": {
          "formId": "ae80271a-a3f0-453b-8dc6-36b777817217",
          "answers": {
            "financial-profile": {
              "expected-activities": [
                "cryptocurrency_investing"
              ],
              "source-of-funds": "salary",
              "annual-income-range": "40000-60000-GBP",
              "savings-and-investments-range": "0-5000-GBP",
              "employment-status": "employed",
              "employment-industry": "Engineering",
              "employment-occupation": "Galactic Vibe Curator"
            }
          }
        },
        "output": {
          "score": "low",
          "expiresAt": "2026-01-01T00:00:00Z",
          "verifiedAt": "2023-01-01T00:00:00Z"
        }
      }
    }
    ```

    **Understanding expiration**

    Even though `output.expiresAt` is set by your organization, the process will automatically re-open for recollection 60 days before that date, reverting to `pending` until the user resubmits. The user remains approved during this window and can continue to operate without restrictions until the deadline is reached.
  </Tab>
</Tabs>

***

### Enhanced due diligence

Enhanced due diligence is an additional layer of verification required for users with a high customer due diligence score.

<Tabs sync={false}>
  <Tab title="Partner-verified">
    Your organization performs enhanced due diligence through an already-approved method, such as manual review of source-of-funds document (e.g. pay stub, bank statement, portfolio statement), and submits the result to Uphold.

    <Steps>
      <Step title="Create the file">
        Call [Create file](/rest-apis/core-api/files/create-file) to obtain an upload URL for the source-of-funds document.

        ```http theme={null}
        POST /core/files
        {
          "category": "document",
          "contentType": "image/png"
        }
        ```

        A successful response returns an `upload` object with a presigned URL and form data for uploading the file to the storage provider.

        ```json Response theme={null}
        {
          "file": {
            "id": "57a3ecf2-5404-4654-9ece-feb504a69f86",
            "status": "pending",
            "category": "document",
            "contentType": "image/png",
            "upload": {
              "url": "https://example.com/upload",
              "formData": {
                "X-Amz-Algorithm": "AWS4-HMAC-SHA256",
                "X-Amz-Credential": "ASIE4FQORBNQHNQD5CG6/20240801/us-east-1/s3/aws4_request",
                "X-Amz-Date": "20240801T102500Z",
                "X-Amz-Security-Token": "IQoJb3JpZ2luX2VjEIv//////////...JBr6h2HkzH/aFSArQcs=",
                "X-Amz-Signature": "b4da8cf1a59327988a8108c965a4789fc8ba2d20f5bffda076106b2b254def29",
                "Key": "4c13b6f5-987e-43df-bc12-042b58307a80",
                "Policy": "eyJjb25kaXRpb25zIjpbeyJidWN...TA6MjU6MDAuMjAyWiJ9"
              },
              "expiresAt": "2024-03-13T20:20:39Z"
            }
          }
        }
        ```
      </Step>

      <Step title="Upload the file">
        Upload the file to the provided `upload.url` using the returned `upload.formData` fields as multipart form parameters.
      </Step>

      <Step title="Submit the process">
        Call [Update enhanced due diligence](/rest-apis/core-api/kyc/update-enhanced-due-diligence), referencing the file ID in `input.media` and setting `output` containing the verification datetime.

        ```http theme={null}
        PATCH /core/kyc/processes/enhanced-due-diligence
        {
          "input": {
            "media": [
              { "context": "source-of-funds-proof", "fileId": "57a3ecf2-5404-4654-9ece-feb504a69f86" }
            ]
          },
          "output": {
            "verifiedAt": "2025-01-29T10:38:00Z"
          }
        }
        ```

        A successful response returns the submitted information with an `ok` status.

        ```json Response theme={null}
        {
          "enhancedDueDiligence": {
            "code": "enhanced-due-diligence",
            "status": "ok",
            "verification": {
              "model": "partner-verified",
              "method": "manual",
              "dependencies": ["customer-due-diligence"]
            },
            "input": {
              "media": [
                { "context": "source-of-funds-proof", "fileId": "57a3ecf2-5404-4654-9ece-feb504a69f86" }
              ]
            },
            "output": {
              "verifiedAt": "2025-01-29T10:38:00Z"
            }
          }
        }
        ```
      </Step>
    </Steps>
  </Tab>

  <Tab title="Uphold-verified">
    <Callout icon="person-digging" color="#F59E0B" iconType="regular">This mode is under construction. Contact your Account Manager for the latest availability.</Callout>
  </Tab>
</Tabs>

***

### Crypto risk assessment

Assess the user's knowledge of cryptocurrency risks and their suitability for engaging in crypto-related activities.

<Tabs sync={false}>
  <Tab title="Uphold-verified">
    Uphold administers a crypto risk assessment quiz to the user, with a different set of questions per attempt, and evaluates the answers to determine whether the user is approved for crypto-related activities. Call [Update crypto risk assessment](/rest-apis/core-api/kyc/update-crypto-risk-assessment) with `input` containing the answers for each form step, repeating until `output` is updated.

    <Note>Crypto risk assessment is a form-based process with multiple steps. The required fields may vary based on the user's region, current attempt, and other factors. Always check the returned `hint` object for the specific schema to display. See [Dynamic forms](/developer-guides/resources/dynamic-forms/introduction) for guidance on rendering the form and submitting the data.</Note>

    ```http theme={null}
    PATCH /core/kyc/processes/crypto-risk-assessment
    {
      "input": {
        "formId": "7a0f4229-e3de-4dfd-8f91-9b1308b2dc33",
        "answers": {
          "decision-making": {
            "who-makes-investment-decisions": "i_decide"
          }
        }
      }
    }
    ```

    A successful response returns the submitted information with an `ok` status, along with approval result, attempts used and remaining, and verification datetime.

    ```json Response theme={null}
    {
      "cryptoRiskAssessment": {
        "code": "crypto-risk-assessment",
        "status": "ok",
        "verification": {
          "model": "uphold-verified",
          "method": "manual",
          "dependencies": []
        },
        "input": {
          "formId": "7a0f4229-e3de-4dfd-8f91-9b1308b2dc33",
          "answers": {
            "decision-making": {
              "who-makes-investment-decisions": "i_decide"
            },
            "loss-tolerance": {
              "acceptable-loss": "full_loss_possible"
            },
            "volatility-understanding": {
              "expected-price-behaviour": "highly_volatile"
            },
            "liquidity-awareness": {
              "selling-timeline": "may_be_delayed"
            },
            "regulatory-protection": {
              "compensation-coverage": "no_coverage"
            },
            "portfolio-allocation": {
              "share-of-net-assets": "small_share"
            },
            "complexity-acknowledgement": {
              "product-mechanics": "complex_and_risky"
            }
          }
        },
        "output": {
          "result": "approved",
          "attempts": {
            "used": 1,
            "maximum": 5
          },
          "verifiedAt": "2022-01-01T00:00:00Z"
        }
      }
    }
    ```

    **Understanding result**

    When the `output.result` is `approved`, the user is cleared for crypto-related activities. When `rejected`, the user can either retry the quiz (when attempts remain) or will be offboarded (when the maximum attempts have been reached).

    **Understanding attempts**

    Use the `output.attempts` object to track how many attempts the user has made and the maximum allowed. When a cooldown is active, `retryAt` is present and indicates the earliest datetime the user can submit a new attempt.

    ```json theme={null}
    {
      "attempts": {
        "used": 1,
        "maximum": 5,
        "retryAt": "2025-01-29T12:00:00Z"
      }
    }
    ```

    **Understanding offboarding**

    If a failed attempt occurs, the `output.offboard` object is present. `endsAt` indicates either the deadline by which the user must submit a new attempt (when attempts remain), or the expected offboarding datetime (when the maximum has been reached). `completedAt` is set to the effective datetime when the user was offboarded.

    ```json theme={null}
    {
      "offboard": {
        "endsAt": "2025-06-29T00:00:00Z",
        "completedAt": "2025-01-29T12:00:00Z"
      }
    }
    ```
  </Tab>

  <Tab title="Partner-verified">
    Your organization administers a crypto risk assessment quiz to the user and submits the results to Uphold. Call [Update crypto risk assessment](/rest-apis/core-api/kyc/update-crypto-risk-assessment) with `input` containing the form answers and `output` containing the approval result, attempts used and remaining, and verification datetime.

    ```http theme={null}
    PATCH /core/kyc/processes/crypto-risk-assessment
    {
      "input": {
        "answers": {
          "crypto-quiz": {
            "decision-making": "i_decide",
            "loss-tolerance": "full_loss_possible",
            "volatility-understanding": "highly_volatile",
            "liquidity-awareness": "may_be_delayed",
            "regulatory-protection": "no_coverage",
            "portfolio-allocation": "small_share",
            "complexity-acknowledgement": "complex_and_risky"
          }
        }
      },
      "output": {
        "result": "approved",
        "attempts": {
          "used": 1,
          "maximum": 5
        },
        "verifiedAt": "2023-01-01T00:00:00Z"
      }
    }
    ```

    A successful response returns the submitted information with an `ok` status.

    ```json Response theme={null}
    {
      "cryptoRiskAssessment": {
        "code": "crypto-risk-assessment",
        "status": "ok",
        "verification": {
          "model": "partner-verified",
          "method": "manual",
          "dependencies": []
        },
        "input": {
          "formId": "7a0f4229-e3de-4dfd-8f91-9b1308b2dc33",
          "answers": {
            "crypto-quiz": {
              "decision-making": "i_decide",
              "loss-tolerance": "full_loss_possible",
              "volatility-understanding": "highly_volatile",
              "liquidity-awareness": "may_be_delayed",
              "regulatory-protection": "no_coverage",
              "portfolio-allocation": "small_share",
              "complexity-acknowledgement": "complex_and_risky"
            }
          }
        },
        "output": {
          "result": "approved",
          "attempts": {
            "used": 1,
            "maximum": 5
          },
          "verifiedAt": "2023-01-01T00:00:00Z"
        }
      }
    }
    ```
  </Tab>
</Tabs>

***

### Self-categorization statement

Collect the user's self-declared financial profile and categorize them accordingly.

<Tabs sync={false}>
  <Tab title="Uphold-verified">
    Uphold administers a self-categorization statement form to the user, and evaluates the answers to determine the user's category. Call [Update self-categorization statement](/rest-apis/core-api/kyc/update-self-categorization-statement) with `input` containing the answers for each form step, repeating until `output` is updated.

    <Note>Self-categorization statement is a form-based process with multiple steps. The required fields may vary based on the user's region, category, and other factors. Always check the returned `hint` object for the specific schema to display. See [Dynamic forms](/developer-guides/resources/dynamic-forms/introduction) for guidance on rendering the form and submitting the data.</Note>

    ```http theme={null}
    PATCH /core/kyc/processes/self-categorization-statement
    {
      "input": {
        "formId": "ac33651f-f2d3-47c4-8e8d-06fb87361f5c",
        "answers": {
          "investor": {
            "type": "restricted_investor"
          }
        }
      }
    }
    ```

    A successful response returns the submitted information with an `ok` status, along with the categorization result and verification datetime.

    ```json Response theme={null}
    {
      "selfCategorizationStatement": {
        "code": "self-categorization-statement",
        "status": "ok",
        "verification": {
          "model": "uphold-verified",
          "method": "manual",
          "dependencies": []
        },
        "input": {
          "formId": "ac33651f-f2d3-47c4-8e8d-06fb87361f5c",
          "answers": {
            "investor": {
              "type": "restricted_investor"
            },
            "investor-profile": {
              "invested-less-than-10-percent": "yes",
              "invested-percentage": "5",
              "invest-less-than-10-percent": "yes",
              "invest-percentage": "5",
              "investor-type-confirmation": true
            }
          }
        },
        "output": {
          "result": "approved",
          "attempts": {
            "used": 1,
            "maximum": 30
          },
          "expiresAt": "2025-01-01T00:00:00Z",
          "verifiedAt": "2022-01-01T00:00:00Z"
        }
      }
    }
    ```

    **Understanding result**

    When the `output.result` is `approved`, the user is cleared for the activities allowed for their category. When `rejected`, the user can either retry the statement (when attempts remain) or will be offboarded (when the maximum attempts have been reached).

    **Understanding attempts**

    Use the `output.attempts` object to track how many attempts the user has made and the maximum allowed.

    ```json theme={null}
    {
      "attempts": {
        "used": 1,
        "maximum": 30
      }
    }
    ```

    **Understanding expiration**

    The `output.expiresAt` is the deadline by which the user must resubmit the self-categorization statement — it opens for recollection 60 days before that date, at which point the process status reverts to `pending`. The user remains approved during this window and can continue to operate without restrictions until the deadline is reached.
  </Tab>

  <Tab title="Partner-verified">
    Your organization administers a self-categorization statement form to the user and submits the results to Uphold. Call [Update self-categorization statement](/rest-apis/core-api/kyc/update-self-categorization-statement) with `input` containing the form answers and `output` containing the categorization result, attempts used and remaining, expiration, and verification datetime.

    ```http theme={null}
    PATCH /core/kyc/processes/self-categorization-statement
    {
      "input": {
        "answers": {
          "investor-category": {
            "type": "high_net_worth_investor",
            "annual-income-above-100000": "yes",
            "annual-income": 120000,
            "net-assets-above-250000": "yes",
            "net-assets": 255000
          }
        }
      },
      "output": {
        "result": "approved",
        "attempts": {
          "used": 1,
          "maximum": 30
        },
        "expiresAt": "2026-01-01T00:00:00Z",
        "verifiedAt": "2023-01-01T00:00:00Z"
      }
    }
    ```

    A successful response returns the submitted information with an `ok` status.

    ```json Response theme={null}
    {
      "selfCategorizationStatement": {
        "code": "self-categorization-statement",
        "status": "ok",
        "verification": {
          "model": "partner-verified",
          "method": "manual",
          "dependencies": []
        },
        "input": {
          "answers": {
            "investor-category": {
              "type": "high_net_worth_investor",
              "annual-income-above-100000": "yes",
              "annual-income": 120000,
              "net-assets-above-250000": "yes",
              "net-assets": 255000
            }
          }
        },
        "output": {
          "result": "approved",
          "attempts": {
            "used": 1,
            "maximum": 30
          },
          "expiresAt": "2026-01-01T00:00:00Z",
          "verifiedAt": "2023-01-01T00:00:00Z"
        }
      }
    }
    ```

    **Understanding expiration**

    Even though `output.expiresAt` is set by your organization, the process will automatically re-open for recollection 60 days before that date, reverting to `pending` until the user resubmits. The user remains approved during this window and can continue to operate without restrictions until the deadline is reached.
  </Tab>
</Tabs>

***

### Tax details

Collect the user's tax residency and tax identification information for tax reporting purposes.

<Tabs sync={false}>
  <Tab title="Uphold-verified">
    Uphold collects tax details through a two-step form. The first step asks for the user's countries of tax residency. Based on the response, the second step requests the required tax identification document numbers for each specified country.

    <Note>See [Dynamic forms](/developer-guides/resources/dynamic-forms/introduction) for guidance on rendering the form and submitting the data.</Note>

    <Steps>
      <Step title="Submit tax residency">
        Call [Update tax details](/rest-apis/core-api/kyc/update-tax-details) with the countries where the user is tax resident.

        <Note>The required information varies based on the user's region. Always check the form schema returned in the `hint` dynamically to determine which fields to collect.</Note>

        ```http theme={null}
        PATCH /core/kyc/processes/tax-details
        {
          "input": {
            "taxResidency": {
              "countries": ["GB"]
            }
          }
        }
        ```

        The response returns `pending` with the next form step in `hint`, pre-filled with the required tax identification documents for each declared country.

        ```json Response theme={null}
        {
          "taxDetails": {
            "code": "tax-details",
            "status": "pending",
            "verification": {
              "model": "uphold-verified",
              "method": "manual",
              "dependencies": ["profile"]
            },
            "input": {
              "taxResidency": {
                "countries": ["GB"]
              },
              "taxIdentification": {
                "documents": [
                  {
                    "type": "tin",
                    "country": "GB"
                  }
                ]
              }
            },
            "hint": {
              "type": "form",
              "schema": {
                "type": "object",
                "properties": { /* ... */ }
              },
              "uiSchema": {
                "type": "Categorization",
                "elements": [ /* ... */ ]
              }
            }
          }
        }
        ```
      </Step>

      <Step title="Submit tax identification">
        Call [Update tax details](/rest-apis/core-api/kyc/update-tax-details) again with the tax identification document numbers and certification of the information's accuracy.

        <Note>The required information varies based on the user's region. Always check the form schema returned in the `hint` dynamically to determine which fields to collect.</Note>

        ```http theme={null}
        PATCH /core/kyc/processes/tax-details
        {
          "input": {
            "taxResidency": {
              "countries": ["GB"]
            },
            "taxIdentification": {
              "documents": [
                {
                  "type": "tin",
                  "country": "GB",
                  "value": "1234567890"
                }
              ],
              "certify": true
            }
          }
        }
        ```

        A successful response returns the submitted information with an `ok` status. The form is always returned — even when the process is `ok` — so the user can update their tax details when needed.

        ```json Response theme={null}
        {
          "taxDetails": {
            "code": "tax-details",
            "status": "ok",
            "verification": {
              "model": "uphold-verified",
              "method": "manual",
              "dependencies": ["profile"]
            },
            "input": {
              "taxResidency": {
                "countries": [
                  "GB"
                ]
              },
              "taxIdentification": {
                "documents": [
                  {
                    "type": "tin",
                    "country": "GB",
                    "number": "6405444702"
                  }
                ],
                "certify": true
              }
            },
            "output": {
              "result": "approved",
              "verifiedAt": "2022-01-01T00:00:00Z"
            },
            "hint": {
              "type": "form",
              "schema": {
                "type": "object",
                "properties": { /* ... */ }
              },
              "uiSchema": {
                "type": "Categorization",
                "elements": [ /* ... */ ]
              }
            }
          }
        }
        ```
      </Step>
    </Steps>
  </Tab>

  <Tab title="Partner-verified">
    <Callout icon="person-digging" color="#F59E0B" iconType="regular">This mode is under construction. Contact your Account Manager for the latest availability.</Callout>
  </Tab>
</Tabs>

***

### Screening and risk

Uphold automatically screens users against sanctions lists and assesses their risk level based on various factors, such as their location, transaction patterns, and other relevant data.

```json theme={null}
{
  "screening": {
    "code": "screening",
    "status": "ok",
    "verification": {
      "model": "uphold-verified",
      "method": "automatic",
      "triggers": ["profile", "identity"],
      "dependencies": []
    },
    "output": {
      "result": "approved"
    }
  },
  "risk": {
    "code": "risk",
    "status": "ok",
    "verification": {
      "model": "uphold-verified",
      "method": "automatic",
      "dependencies": []
    },
    "output": {
      "result": "approved"
    }
  }
}
```

**Understanding result**

When the `output.result` is `approved`, the user has passed screening or risk assessment. When `rejected`, the user has been flagged for potential issues and may require further review or offboarding.

## Monitor user onboarding

Prefer **webhooks** for real-time updates, or fall back to **polling** if webhooks are not feasible.

* Webhook events (recommended):
  * [core.user.created](/rest-apis/core-api/users/webhooks/user-created)
  * [core.kyc.\*.status-changed](/rest-apis/core-api/kyc/webhooks)
    * `status: pending` → waiting for the user to provide information
    * `status: running` → Uphold is processing the provided information
    * `status: ok` → the process has been successfully verified
    * `status: failed` → verification failed — the user may need to resubmit
    * `status: exempt` → the process is not required for this user
* Polling (fallback): [Get KYC overview](/rest-apis/core-api/kyc/get-overview)

## Check capabilities

Always check if the required capabilities for the user's intended activities are unlocked before allowing them to transact. Call [List capabilities](/rest-apis/core-api/capabilities/list-user-capabilities) to check restrictions and requirements for each capability.

```http theme={null}
GET /core/capabilities
```

The response includes the list of capabilities with any outstanding `requirements` or `restrictions`.

```json Response theme={null}
{
  "capabilities": [
    {
      "key": "crypto-deposits",
      "enabled": true,
      "requirements": [],
      "restrictions": []
    },
    {
      "key": "crypto-withdrawals",
      "enabled": true,
      "requirements": [
        "user-must-submit-identity"
      ],
      "restrictions": []
    }
  ]
}
```

<Check>The user is now onboarded and ready to transact.</Check>
