` is the unique identifier of the specific entity instance (e.g., user ID, account ID)
For each API that supports metadata, refer to the respective documentation to see the list of supported entities.
### Creating metadata
**Option 1: During entity creation**
Include a `metadata` field when creating entities. Here's an example of creating a user with metadata:
```json theme={null}
POST /core/users
{
"email": "john.doe@example.com",
"termsOfService": "general-gb-fca",
"country": "GB",
"subdivision": "GB-MAN",
"citizenshipCountry": "GB",
"partnerOnboardedAt": "2025-02-01T00:00:00Z",
"metadata": {
"externalId": "usr_12345",
"tier": "premium"
}
}
```
If metadata creation fails, the entity is still created successfully. The response will include an `errors.metadata` property. See [Error handling](#errors-during-entity-creation) for details.
**Option 2: Using the dedicated endpoint**
Use the **Set metadata** endpoint after entity creation. Here's an example of adding metadata to the authenticated user:
```json theme={null}
PUT /core/users/me/metadata
{
"externalId": "usr_12345",
"tier": "premium"
}
```
### Retrieving metadata
To fetch the metadata for an entity, use the **Get metadata** endpoint. Here's an example of retrieving metadata for the authenticated user:
```
GET /core/users/me/metadata
```
Response:
```json theme={null}
{
"metadata": {
"externalId": "usr_12345",
"tier": "premium"
}
}
```
### Updating metadata
**Option 1: Full replacement**
To completely replace the existing metadata, use the **Set metadata** endpoint. Here's an example of updating the metadata for the authenticated user:
```json theme={null}
PUT /core/users/me/metadata
{
"externalId": "usr_12345",
"tier": "enterprise"
}
```
**Option 2: Partial update**
To modify specific properties, use the **Update metadata** endpoint with a [JSON Patch](http://jsonpatch.com/) payload. Here's an example of partially updating the metadata for the authenticated user:
```json theme={null}
PATCH /core/users/me/metadata
[
{ "op": "replace", "path": "/tier", "value": "enterprise" },
{ "op": "add", "path": "/region", "value": "us-west" }
]
```
### Deleting metadata
To remove all metadata associated with an entity, use the **Delete metadata** endpoint. Here's an example of deleting the metadata for the authenticated user:
```
DELETE /core/users/me/metadata
```
## Error handling
### Size limit exceeded
Metadata payloads have a maximum size of **1024 Unicode characters**. If the payload exceeds this limit, a `413 Payload Too Large` error is returned:
```json theme={null}
{
"code": "content_too_large",
"message": "The entity metadata size is greater than maximum size limit",
"details": {
"threshold": {
"unit": "characters",
"limit": "1024"
}
}
}
```
### Errors during entity creation
When creating an entity with metadata, if the metadata operation fails, **the entity is still created successfully**. The response includes an `errors.metadata` property describing the issue:
```json theme={null}
{
"user": {
"id": "cd21b26d-35d2-408a-9201-b8fdbef7a604",
"email": "john.doe@example.com"
// ... other user properties ...
},
"errors": {
"metadata": {
"code": "content_too_large",
"message": "The entity metadata size is greater than maximum size limit",
"details": {
"threshold": {
"unit": "characters",
"limit": "1024"
}
}
}
}
}
```
This ensures that entity creation is not blocked by metadata issues. You can add the metadata afterward using the **Set metadata** endpoint.
## HTTP conditional requests
Metadata endpoints support standard HTTP conditional request headers for concurrency control and efficient caching.
These headers are optional but recommended to prevent conflicts and optimize bandwidth.
**`ETag` response header**:
This header is included in responses to represent the current revision of the metadata.
**`If-Match` request header**:
When updating (via PUT or PATCH) or deleting metadata, include this header with the `ETag` value from a previous response to ensure you're modifying the expected version. If the `ETag` does not match the current version, a `412 Precondition Failed` error is returned, preventing accidental overwrites.
**`If-None-Match` request header**:
When retrieving metadata, include this header with the `ETag` value from a previous response. If the metadata has not changed, a `304 Not Modified` response is returned, saving bandwidth.
Additionally, when creating metadata and you want to ensure none already exists, you can use the `If-None-Match` header with a value of `*`. If metadata already exists, a `412 Precondition Failed` error will be returned.
# Error handling
Source: https://developer.uphold.com/rest-apis/errors
Handle Uphold REST API errors using documented error codes and messages from the OpenAPI spec, with structured error shapes and HTTP status conventions.
Error handling is a core design principle of the API.
This helps you build robust integrations and applications that present meaningful error messages to end users.
## Comprehensive list
All possible error scenarios are enumerated as part of the OpenAPI spec, resulting in a comprehensive list of error codes and messages.
You may see these errors on every endpoint's page, by cycling through the different HTTP status codes in the response examples.
## Consistent shape
The API follows a consistent approach to describe errors. Every error response includes a standard payload with the following fields:
```json [expandable] theme={null}
{
// A code that identifies the error.
"code": "country_not_supported",
// A developer readable message describing the error.
"message": "The country is not supported",
// Additional details about the error.
"details": {}
}
```
## HTTP status codes
Every error response includes an HTTP status code to indicate the nature of the error. Here are some of the most common status codes you might encounter:
* `400 Bad Request`: The request is malformed or didn't pass validation according to the endpoint's OpenAPI schema.
* `401 Unauthorized`: The request is missing authentication credentials or the provided credentials are invalid.
* `403 Forbidden`: The request is authenticated but doesn't have the necessary permissions (token scopes) to access the resource.
* `404 Not Found`: The requested resource doesn't exist.
* `409 Conflict`: The request failed due to a business logic error.
* `429 Too Many Requests`: The client has sent too many requests in a given amount of time and is being rate-limited.
However, the `code` field in the error payload is where you primarily should look as it provides a more granular description of the failure.
# Common headers
Source: https://developer.uphold.com/rest-apis/headers
Reference for the common request and response headers used across Uphold REST APIs to pass user context, idempotency keys, and other request metadata.
The REST APIs use custom headers for passing context in requests and returning useful metadata in responses. This page documents the headers that are common to all endpoints. Some endpoints may include additional headers specific to their functionality.
## Request headers
### User context
The REST APIs are not meant to be called directly by your frontend applications, but rather by your backend systems. A side-effect of this is that Uphold is unable to extract context of users' requests, such as the user's IP address, user agent, and other information that would be typically available otherwise.
To address this, Uphold provides a way for you to pass users' context via headers. This context enriches the request and provides better insights into end users' actions. In fact, **certain APIs require** parts of the context to be present in order to function correctly, so you should include it in all API requests made on behalf of your end users.
Include the following headers in your API requests:
* `X-Uphold-User-Ip`: The IP address of the end user.
* `X-Uphold-User-Agent`: The user agent string of the end user's device.
* `X-Uphold-User-Origin`: The original `Origin` header if the request was made from a browser.
* `X-Uphold-User-Country`: The country code ([ISO 3166](https://en.wikipedia.org/wiki/ISO_3166-2) - two letter) associated with the geolocation of the end user.
* `X-Uphold-User-Subdivision`: The subdivision code ([ISO 3166](https://en.wikipedia.org/wiki/ISO_3166-2)) associated with the geolocation of the end user.
* `X-Uphold-User-City`: The city associated with the geolocation of the end user.
## Response headers
### Request id
Every API response includes an `X-Uphold-Request-Id` header containing a unique identifier for the request. You can use this identifier when contacting Uphold support to help troubleshoot specific requests.
```
X-Uphold-Request-Id: 9d71b4edeb46c8f9-MAD
```
# Introduction to the Uphold Enterprise REST APIs
Source: https://developer.uphold.com/rest-apis/introduction
Overview of Uphold's modular REST API suite for building enterprise crypto products. Includes OpenAPI specs, User onboarding, Transactions, Portfolio, and more.
Uphold offers a suite of REST APIs that allow you to interact with the platform programmatically, enabling you to build your application on top of its services.
The REST APIs are modular in principle and can be combined to serve different business needs and to build custom solutions.
[OpenAPI](https://www.openapis.org/) specifications are available for all APIs, which is a standard to describe HTTP APIs.
You may use these specifications to generate client libraries in your preferred programming language.
The APIs provide fundamental building blocks, forming the backbone of the REST API suite. With these building blocks, you can build a wide range of solutions with a high degree of flexibility.
Core building blocks, including users, assets, accounts, transactions, and more.
Ingest KYC data from third-party providers into the Uphold platform.
Market news and statistics for digital assets.
Generate URLs to embed widgets in your application.
Interact with Topper product features and functionalities.
# KYC Connector API introduction
Source: https://developer.uphold.com/rest-apis/kyc-connector-api/introduction
Connect third-party KYC providers like Sumsub and Veriff to Uphold's platform. Ingestions map provider payloads to KYC processes without bespoke code.
The KYC Connector API is an API designed to connect third-party KYC providers with Uphold's platform, without you having to build and maintain bespoke integrations.
It functions as an ingestion layer between mainstream KYC providers and Uphold's platform [KYC processes](../core-api/kyc/introduction#kyc-processes), such as `profile`, `address`, `identity` and `proofOfAddress`.
## Key features and benefits
* Compatible with multiple KYC providers, so you can connect whichever provider you use without any custom integration work.
* The KYC Connector API ingests and normalizes provider data for you.
* Enable your platform to continue relying on your existing KYC provider.
## Supported providers
A leading and comprehensive verification solution that focuses on compliance and user experience.
An AI-driven, scalable verification that helps businesses meet compliance requirements.
A leading and comprehensive verification solution that focuses on compliance and user experience.
An AI-driven, scalable verification that helps businesses meet compliance requirements.
## Ingestions
KYC Connector is a **workflow-based ingestion system**.
An ingestion pulls KYC data from an existing user verification on the provider side and submits it to Uphold's platform on their behalf. The verification must already be approved before you can create one.
When creating an ingestion, you provide a reference to the user's verification and specify which KYC processes to ingest — the API handles fetching and normalizing the provider data for you.
### Statuses
The ingestion workflow follows these statuses:
* **Queued**: When you create an ingestion, it starts in the `queued` state.
* **Running**: The ingestion will be quickly picked up by a worker, which changes its status to `running` and starts processing it. During this phase, the worker interacts with the provider's API to retrieve KYC data and files for the specified processes.
* **Finished**: Once the ingestion processing comes to an end, its status changes to `finished`.
You can track ingestions in two ways:
* **Polling**: Poll the endpoint that retrieves an ingestion by its ID.
* **Webhooks**: Subscribe to webhooks to be notified when an ingestion status changes.
### Results
Under the `result` field, you can find the outcome of the ingestion of each KYC process.
For example, if you created an ingestion to ingest both `identity` and `proofOfAddress` processes, the result will contain the outcome of both processes. Each process will have its own status, which can be `processing`, `completed`, or `failed` as well as extracted data and an error if the ingestion for that process failed. Below are examples of possible results for an `identity` ingestion:
```json theme={null}
{
"result": {
"identity": {
"status": "processing"
}
}
}
```
```json theme={null}
{
"result": {
"identity": {
"status": "completed",
"data": {
"document": {
"type": "passport",
"number": "7700225VH",
"country": "GB",
"expiresAt": "2026-03-13"
},
"person": {
"givenName": "John",
"familyName": "Doe",
"birthdate": "1987-01-01",
"gender": "male"
},
"verifiedAt": "2020-01-01T00:00:00Z"
},
"files": [
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"category": "document",
"context": "photo-document-front",
"contentType": "image/png",
"size": 204800,
"providerDetails": {
"fileId": "732150141"
}
}
]
}
}
}
```
```json theme={null}
{
"result": {
"identity": {
"status": "failed",
"error": {
"code": "provider_data_not_available",
"message": "Profile data not available in Sumsub applicant info",
"hint": "Most likely the share token refers to an applicant without identity verification process."
}
}
}
}
```
### Important note on `completed` status
When a process status is marked as `completed`, it means that the data has been **extracted and submitted** to Uphold's platform. However, it does **not** necessarily mean that the KYC process is "ok". You should always check the actual KYC process status after ingestion using the [Get KYC Overview](../core-api/kyc/get-overview) endpoint. For instance:
* The `profile` process ingestion can be `completed` but the actual `profile` KYC process status might still be `pending` in case there are still fields to be submitted.
* The `identity` process ingestion can be `completed` but the actual `identity` KYC process status might be `failed` in situations like a duplicate identity conflict with another user or if the user is underage.
Simply put, `completed` indicates successful data extraction and submission to the platform, not a successful KYC verification result.
# Create Sumsub ingestion
Source: https://developer.uphold.com/rest-apis/kyc-connector-api/sumsub/create-ingestion
/_media/specs/kyc-connector-openapi.mintlify.json post /kyc-connector/sumsub/ingestions
Create a new Sumsub ingestion.
# Get Sumsub ingestion
Source: https://developer.uphold.com/rest-apis/kyc-connector-api/sumsub/get-ingestion
/_media/specs/kyc-connector-openapi.mintlify.json get /kyc-connector/sumsub/ingestions/{ingestionId}
Retrieve an existing Sumsub ingestion by id.
# List Sumsub ingestions
Source: https://developer.uphold.com/rest-apis/kyc-connector-api/sumsub/list-ingestions
/_media/specs/kyc-connector-openapi.mintlify.json get /kyc-connector/sumsub/ingestions
List Sumsub ingestions made by a user.
# Sumsub KYC Connector overview
Source: https://developer.uphold.com/rest-apis/kyc-connector-api/sumsub/overview
Use Sumsub Reusable KYC with the Uphold KYC Connector to share verifications between your Sumsub account and Uphold. Includes setup and configuration.
The Sumsub KYC Connector leverages [Sumsub's Reusable KYC](https://docs.sumsub.com/docs/reusable-kyc) feature to enable secure and compliant sharing of KYC data between your Sumsub account and Uphold's platform (using Uphold's Sumsub account).
## Setup requirements
Before you can use the Sumsub KYC Connector, both the **donor** (your Sumsub account where users complete verification) and the **recipient** (Uphold's Sumsub account) must be properly configured in Sumsub's dashboard to enable Reusable KYC data sharing.
Please reach out to your Uphold account manager to help coordinate the setup with you.
Once the setup is complete, sharing happens via a [share token](https://docs.sumsub.com/reference/generate-share-token) that you must generate for a given applicant. Uphold uses this token to copy the applicant's KYC data and documents.
## Supported processes
The Sumsub KYC Connector supports ingestion of the following KYC processes. Each process has specific considerations you should be aware of:
### Profile
Ingests [profile](../../core-api/kyc/introduction.mdx#kyc-processes) information such as the user's full name, date of birth, place of birth, and citizenship.
The source fields from Sumsub used for this process are as follows:
* `fullName`: Sourced from `fixedInfo.firstName`, `fixedInfo.middleName`, and `fixedInfo.lastName`. Falls back to the same fields on `info` if `fixedInfo` is not available.
* `birthdate`: Sourced from `fixedInfo.dob` or `info.dob`.
* `birthplace`: Sourced from `fixedInfo.countryOfBirth` for the country and `fixedInfo.placeOfBirth` or `info.stateOfBirth` for the town. Falls back to the same fields on `info` if `fixedInfo` is not available.
* `primaryCitizenship`: Sourced from `fixedInfo.nationality` or `info.nationality`.
Please check the [`ingestion.result.profile`](./get-ingestion#response-ingestion-result-profile) field in the specification for more details. Furthermore, here's a breakdown of errors that can happen for this process:
| Error Code | Scenarios |
| ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| provider\_data\_not\_available | • Profile data is not available for the Sumsub applicant |
| provider\_data\_invalid | • Sumsub applicant contains first or middle name but no last name
• Profile data is incomplete (when birthplace country or town is provided but not both)
• Extracted profile data failed validation |
| sumsub\_client\_error | • Sumsub API returned an error when fetching applicant data |
| sumsub\_applicant\_review\_state\_invalid | • Sumsub applicant review answer is not approved (GREEN) on the donor
• Sumsub applicant is in an unexpected review status (`init`, `onHold`, `awaitingUser`) |
| provider\_data\_readiness\_timeout\_exceeded | • Max retries exceeded waiting for provider data to be ready |
| workflow\_error | • Workflow was canceled
• Workflow timed out
• Workflow had an unrecoverable error |
| unknown\_error | • Any unhandled error during the workflow |
### Address
Ingests [address](../../core-api/kyc/introduction.mdx#kyc-processes) information by matching addresses from the Sumsub applicant against the user's country and subdivision provided via the [Create User](../../core-api/users/create-user.mdx#body-country) endpoint.
The ingestion follows a **prioritized matching** strategy:
1. **Declared addresses**: If the applicant has `fixedInfo.addresses` (declared addresses), these are the only only addresses added to the prioritized list (step 2 and 3 are skipped).
2. **Address from verification step**: If the applicant has completed a **Proof of address** verification step with a `GREEN` (approved) review result, the address from the associated document is added to the prioritized list.
3. **All other applicant addresses**: All other addresses from the applicant's `info.addresses` are added to the prioritized list with addresses sourced from `proofOfAddress` documents prioritized over other sources.
Then, the ingestion attempts to find a compatible address using the following matching criteria in order:
1. **Country and subdivision match**: From the prioritized list, the ingestion looks for an address that matches both the user's existing subdivision and country in Uphold.
2. **Country-only match**: If no exact match is found and either the user's existing address or the Sumsub address has an empty subdivision, the ingestion will match by country only.
If no compatible address is found through this matching process, the address ingestion will fail with a `provider_data_not_available` error.
Please check the [`ingestion.result.address`](./get-ingestion#response-ingestion-result-address) field in the specification for more details. Furthermore, here's a breakdown of errors that can happen for this process:
| Error Code | Scenarios |
| --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| provider\_data\_not\_available | • No address data found for the Sumsub applicant |
| provider\_data\_invalid | • Address data is incomplete (missing required fields)
• Address country does not match the user's declared address country
• Extracted address data failed validation |
| operation\_not\_allowed | • Subdivision does not match user-declared subdivision (US residents only, state)
• Subdivision updates are not allowed for the organization |
| sumsub\_client\_error | • Sumsub API returned an unrecoverable error |
| sumsub\_applicant\_review\_state\_invalid | • Sumsub applicant review answer is not approved (GREEN) on the donor
• Sumsub applicant is in an unexpected review status (`init`, `onHold`, `awaitingUser`) |
| workflow\_error | • Workflow was canceled
• Workflow timed out
• Workflow had an unrecoverable error |
| unknown\_error | • Any unhandled error during the workflow |
### Identity
Ingests government-issued [identity](../../core-api/kyc/introduction.mdx#kyc-processes) data, including document type, number, issuing country, expiration date, and biographic information.
This process requires that the Sumsub verification level completed by the applicant includes both **Identity document** and **Selfie** verification steps.
The exact settings for these steps (e.g., document types allowed) should match Uphold's KYC compliance requirements. If the applicant has completed both steps but the settings do not align with Uphold's requirements, the ingestion for this process will fail. Please consult with your Account Manager to ensure proper configuration.
Please check the [`ingestion.result.identity`](./get-ingestion#response-ingestion-result-identity) field in the specification for more details. Furthermore, here's a breakdown of errors that can happen for this process:
| Error Code | Scenarios |
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| provider\_data\_readiness\_timeout\_exceeded | • Max retries exceeded waiting for provider data to be ready |
| provider\_data\_not\_available | • No identity verification step found for the Sumsub applicant
• No selfie verification step found for the Sumsub applicant |
| provider\_data\_invalid | • Sumsub's identity verification step review answer is not approved (GREEN)
• Sumsub's selfie verification step review answer is not GREEN
• Identity data extracted from Sumsub is incomplete (missing required fields)
• Extracted identity data failed validation (e.g., user is under age) |
| provider\_file\_download\_failed | • User has reached the maximum number of files allowed
• Timeout waiting for file to be downloaded
• Downloaded file status is invalid |
| dependent\_process\_failed | • Dependent profile process has failed |
| operation\_not\_allowed | • Current status of the identity process does not allow updates
• User cannot submit identity due to incomplete declared information |
| sumsub\_client\_error | • Sumsub API returned an unrecoverable error |
| sumsub\_applicant\_review\_state\_invalid | • Sumsub applicant review answer is not approved (GREEN) on the donor
• Sumsub applicant is in an unexpected review status (`init`, `onHold`, `awaitingUser`) |
| workflow\_error | • Workflow was canceled
• Workflow timed out
• Workflow had an unrecoverable error |
| unknown\_error | • Any unhandled error during the workflow |
### Proof-of-address
Ingests [proof-of-address](../../core-api/kyc/introduction.mdx#kyc-processes) documentation, such as utility bills, bank statements, or government correspondence.
This process requires that the Sumsub verification level completed by the applicant includes a **Proof of address** verification step.
The exact settings for this step (e.g., document types allowed) should match Uphold's KYC compliance requirements. If the applicant has completed a step but the settings do not align with Uphold's requirements, the ingestion for this process will fail. Please consult with your Account Manager to ensure proper configuration.
Please check the [`ingestion.result.proofOfAddress`](./get-ingestion#response-ingestion-result-address) field in the specification for more details. Furthermore, here's a breakdown of errors that can happen for this process:
| Error Code | Scenarios |
| ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| provider\_data\_readiness\_timeout\_exceeded | • Max retries exceeded waiting for provider data to be ready |
| provider\_data\_not\_available | • No proof-of-address verification step found for the Sumsub applicant |
| provider\_data\_invalid | • Sumsub's proof-of-address verification step review answer is not approved (GREEN)
• Proof-of-address data extracted from Sumsub is incomplete (missing required fields)
• Extracted proof-of-address data failed validation |
| provider\_file\_download\_failed | • User has reached the maximum number of files allowed
• Timeout waiting for file to be downloaded
• Downloaded file status is invalid |
| dependent\_process\_failed | • Dependent profile process has failed
• Dependent address process has failed |
| operation\_not\_allowed | • Current status of the proof-of-address process does not allow updates
• User cannot submit proof-of-address due to incomplete declared information |
| sumsub\_client\_error | • Sumsub API returned an unrecoverable error |
| sumsub\_applicant\_review\_state\_invalid | • Sumsub applicant review answer is not approved (GREEN) on the donor
• Sumsub applicant is in an unexpected review status (`init`, `onHold`, `awaitingUser`) |
| workflow\_error | • Workflow was canceled
• Workflow timed out
• Workflow had an unrecoverable error |
| unknown\_error | • Any unhandled error during the workflow |
# Sumsub ingestion created
Source: https://developer.uphold.com/rest-apis/kyc-connector-api/sumsub/webhooks/ingestion-created
/_media/specs/kyc-connector-openapi.mintlify.json webhook kyc-connector.sumsub.ingestion.created
A new Sumsub ingestion has been created.
# Sumsub ingestion status changed
Source: https://developer.uphold.com/rest-apis/kyc-connector-api/sumsub/webhooks/ingestion-status-changed
/_media/specs/kyc-connector-openapi.mintlify.json webhook kyc-connector.sumsub.ingestion.status-changed
The Sumsub ingestion status has been changed.
# Create Veriff ingestion
Source: https://developer.uphold.com/rest-apis/kyc-connector-api/veriff/create-ingestion
/_media/specs/kyc-connector-openapi.mintlify.json post /kyc-connector/veriff/ingestions
Create a new Veriff ingestion.
# Get Veriff config
Source: https://developer.uphold.com/rest-apis/kyc-connector-api/veriff/get-config
/_media/specs/kyc-connector-openapi.mintlify.json get /kyc-connector/veriff/config
Retrieve the Veriff provider configuration for the organization.
# Get Veriff ingestion
Source: https://developer.uphold.com/rest-apis/kyc-connector-api/veriff/get-ingestion
/_media/specs/kyc-connector-openapi.mintlify.json get /kyc-connector/veriff/ingestions/{ingestionId}
Retrieve an existing Veriff ingestion by id.
# List Veriff ingestions
Source: https://developer.uphold.com/rest-apis/kyc-connector-api/veriff/list-ingestions
/_media/specs/kyc-connector-openapi.mintlify.json get /kyc-connector/veriff/ingestions
List Veriff ingestions made by a user.
# Veriff KYC Connector overview
Source: https://developer.uphold.com/rest-apis/kyc-connector-api/veriff/overview
Use the Uphold KYC Connector with Veriff's verification platform to share KYC data between your Veriff Station integration and Uphold's KYC processes.
The Veriff KYC Connector integrates with [Veriff's verification platform](https://www.veriff.com/) to enable secure and compliant sharing of KYC data between your Veriff integration and Uphold's platform.
## Setup requirements
Before you can use the Veriff KYC Connector, you must have at least one Veriff integration set up in your [Veriff Station dashboard](https://station.veriff.com/integrations), with the appropriate verification flows enabled. Refer to [Veriff's developer documentation](https://devdocs.veriff.com/) for guidance on setting up integrations.
## Getting started
### Configuring your integrations on Uphold
Once you have your Veriff integrations ready, you need to register them with Uphold using the [Set Veriff config](./set-config) endpoint. This is required before creating any ingestions.
For each integration, you will need:
* **Integration name**: A name of your choice to identify the integration.
* **API key** and **shared secret key**: Found in your [Veriff Station dashboard](https://station.veriff.com/integrations) under the integration's settings.
Uphold stores these credentials securely, so that when creating an ingestion you only need to reference the **integration name** and the **session ID**.
### Selecting a session
A session represents a Veriff verification for a user. Only sessions in an **approved** state are accepted. The KYC processes that can be ingested depend on the verification flow and features enabled in your Veriff integration. For example:
* A standard IDV session with document and selfie verification can be used to ingest `profile`, `address`, and `identity` processes.
* A PoA session with address validation can be used to ingest `address` and `proof-of-address` processes.
### Creating ingestions
To [create an ingestion](./create-ingestion), you provide the **session ID** and **integration name** for each Veriff session you want to process. You can include both an IDV and a PoA session in a single ingestion.
```json POST /kyc-connector/veriff/ingestions theme={null}
{
"processes": [
"profile",
"address",
"identity",
"proof-of-address"
],
"sessions": [
{
"processes": [
"profile",
"identity"
],
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"integrationName": "my-idv-integration"
},
{
"processes": [
"address",
"proof-of-address"
],
"sessionId": "661f9511-f30c-52e5-b827-557766551111",
"integrationName": "my-poa-integration"
}
]
}
```
## Supported processes
The Veriff KYC Connector supports ingestion of the following KYC processes. Each process has specific requirements and considerations you should be aware of:
### Profile
Ingests [profile](../../core-api/kyc/introduction.mdx#kyc-processes) information such as the user's full name, date of birth, and citizenship.
The source fields from Veriff used for this process are as follows:
* `fullName`: Sourced from `person.firstName` and `person.lastName`.
* `birthdate`: Sourced from `person.dateOfBirth`.
* `birthplace`: Sourced from `person.placeOfBirth`. Only extracted when both the town and country are present in the Veriff verification; if either is missing, this field will not be populated.
* `primaryCitizenship`: Sourced from `person.nationality` or `person.citizenship`.
Please check the [`ingestion.result.profile`](./get-ingestion#response-ingestion-result-profile) field in the specification for more details. Furthermore, here's a breakdown of errors that can happen for this process:
| Error Code | Scenarios |
| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| provider\_data\_not\_available | • Profile data is not available for the Veriff verification
• No IDV session provided |
| veriff\_session\_not\_approved | • Veriff session is not in an approved state |
| provider\_data\_recheck\_failed | • Verification data did not pass validation checks |
| provider\_config\_error | • Failed to fetch or parse the organization's provider config
• Integration referenced in session not found in provider config |
| veriff\_client\_error | • Veriff API returned an error when fetching verification data |
| workflow\_error | • Workflow was canceled
• Workflow timed out
• Workflow had an unrecoverable error |
| unknown\_error | • Any unhandled error during the workflow |
### Address
Ingests [address](../../core-api/kyc/introduction.mdx#kyc-processes) information by matching addresses from the Veriff verification against the user's subdivision and country provided via the [Create User](../../core-api/users/create-user.mdx#body-country) endpoint.
Address data can come from **either an IDV or PoA Veriff session**:
1. **Address matching (PoA feature)**: If the PoA "address matching" feature is enabled for your integration in the Veriff platform, the user's declared address is extracted directly from the matching result.
2. **Person addresses fallback**: Otherwise, the ingestion searches all addresses in the verification's person data for one that matches the user's declared country and/or subdivision, prioritizing a country + subdivision match over a country-only match.
When both session types are provided, the PoA address matching data always takes precedence over the IDV person addresses fallback.
If no compatible address is found through this process, the address ingestion will fail with a `provider_data_not_available` error.
Please check the [`ingestion.result.address`](./get-ingestion#response-ingestion-result-address) field in the specification for more details. Furthermore, here's a breakdown of errors that can happen for this process:
| Error Code | Scenarios |
| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| provider\_data\_not\_available | • No address data found in the Veriff verification
• No IDV or PoA session provided |
| veriff\_session\_not\_approved | • Veriff session is not in an approved state |
| provider\_data\_recheck\_failed | • Verification data did not pass validation checks |
| provider\_config\_error | • Failed to fetch or parse the organization's provider config
• Integration referenced in session not found in provider config |
| veriff\_client\_error | • Veriff API returned an unrecoverable error |
| workflow\_error | • Workflow was canceled
• Workflow timed out
• Workflow had an unrecoverable error |
| unknown\_error | • Any unhandled error during the workflow |
### Identity
Ingests government-issued [identity](../../core-api/kyc/introduction.mdx#kyc-processes) data, including document type, number, issuing country, expiration date, and biographic information.
This process **requires a Veriff IDV session** that includes both identity document verification and a selfie/biometric check.
Please check the [`ingestion.result.identity`](./get-ingestion#response-ingestion-result-identity) field in the specification for more details. Furthermore, here's a breakdown of errors that can happen for this process:
| Error Code | Scenarios |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| provider\_data\_not\_available | • No person data found in Veriff verification
• No IDV session provided |
| provider\_data\_invalid | • No document data found in Veriff verification |
| veriff\_session\_not\_approved | • Veriff session is not in an approved state |
| provider\_data\_recheck\_failed | • Verification data did not pass validation checks |
| provider\_config\_error | • Failed to fetch or parse the organization's provider config
• Integration referenced in session not found in provider config |
| provider\_file\_download\_failed | • User has reached the maximum number of files allowed
• Timeout waiting for file to be downloaded
• Downloaded file status is invalid |
| dependent\_process\_failed | • Dependent profile process has failed |
| operation\_not\_allowed | • Current status of the identity process does not allow updates
• User cannot submit identity due to incomplete declared information |
| veriff\_client\_error | • Veriff API returned an unrecoverable error |
| workflow\_error | • Workflow was canceled
• Workflow timed out
• Workflow had an unrecoverable error |
| unknown\_error | • Any unhandled error during the workflow |
### Proof-of-address
Ingests [proof-of-address](../../core-api/kyc/introduction.mdx#kyc-processes) documentation, such as utility bills, bank statements, or government correspondence.
This process **requires a PoA session**. Veriff's PoA verification must be enabled for your Veriff integration.
The ingestion extracts the proof-of-address data using the following priority:
1. **Address validation (PoA feature)**: If the PoA **address validation** feature is enabled for your integration in the Veriff platform, the address is extracted directly from the validation result.
2. **Address matching (PoA feature)**: If the PoA **address matching** feature is enabled for your integration in the Veriff platform, the verified address is extracted directly from the matching result.
Please check the [`ingestion.result.proofOfAddress`](./get-ingestion#response-ingestion-result-proofOfAddress) field in the specification for more details. Furthermore, here's a breakdown of errors that can happen for this process:
| Error Code | Scenarios |
| ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| provider\_data\_not\_available | • No proof-of-address data found in the Veriff PoA session
• No PoA session provided |
| veriff\_session\_not\_approved | • Veriff session is not in an approved state |
| provider\_data\_recheck\_failed | • Verification data did not pass validation checks |
| provider\_config\_error | • Failed to fetch or parse the organization's provider config
• Integration referenced in session not found in provider config |
| provider\_file\_download\_failed | • User has reached the maximum number of files allowed
• Timeout waiting for file to be downloaded
• Downloaded file status is invalid |
| dependent\_process\_failed | • Dependent profile process has failed
• Dependent address process has failed |
| operation\_not\_allowed | • Current status of the proof-of-address process does not allow updates
• User cannot submit proof-of-address due to incomplete declared information |
| veriff\_client\_error | • Veriff API returned an unrecoverable error |
| workflow\_error | • Workflow was canceled
• Workflow timed out
• Workflow had an unrecoverable error |
| unknown\_error | • Any unhandled error during the workflow |
# Set Veriff config
Source: https://developer.uphold.com/rest-apis/kyc-connector-api/veriff/set-config
/_media/specs/kyc-connector-openapi.mintlify.json put /kyc-connector/veriff/config
Create or update the Veriff provider configuration for the organization.
# Veriff ingestion created
Source: https://developer.uphold.com/rest-apis/kyc-connector-api/veriff/webhooks/ingestion-created
/_media/specs/kyc-connector-openapi.mintlify.json webhook kyc-connector.veriff.ingestion.created
A new Veriff ingestion has been created.
# Veriff ingestion status changed
Source: https://developer.uphold.com/rest-apis/kyc-connector-api/veriff/webhooks/ingestion-status-changed
/_media/specs/kyc-connector-openapi.mintlify.json webhook kyc-connector.veriff.ingestion.status-changed
The Veriff ingestion status has been changed.
# Get asset information
Source: https://developer.uphold.com/rest-apis/market-pulse-api/assets/get-asset-information
/_media/specs/market-pulse-openapi.mintlify.json get /market-pulse/assets/{assetCode}/information
Retrieve descriptive information for a specific asset.
# Get asset statistics
Source: https://developer.uphold.com/rest-apis/market-pulse-api/assets/get-asset-statistics
/_media/specs/market-pulse-openapi.mintlify.json get /market-pulse/assets/{assetCode}/statistics
Retrieve real-time market statistics for a specific asset.
# List asset news
Source: https://developer.uphold.com/rest-apis/market-pulse-api/assets/list-asset-news
/_media/specs/market-pulse-openapi.mintlify.json get /market-pulse/assets/{assetCode}/news
Retrieve the latest news for a specific asset.
# List general news
Source: https://developer.uphold.com/rest-apis/market-pulse-api/general/list-general-news
/_media/specs/market-pulse-openapi.mintlify.json get /market-pulse/news
Retrieve the latest general market news.
# Pagination
Source: https://developer.uphold.com/rest-apis/pagination
Paginate Uphold REST API list responses using page-based and cursor-based pagination. Includes parameters, response metadata, and best-practice usage.
Some endpoints may return a large number of results, so pagination is available to help you request only a subset of the results at a time.
## Types of pagination
There are two types of pagination used underneath:
* **Page-based pagination**: This method uses a paging parameter to indicate the starting point of records to return.
* **Cursor-based pagination**: This method utilizes a unique identifier to determine the position in the data set.
There are different advantages and disadvantages to each method, but performance wise cursor-based pagination is generally more efficient.
Currently, most of the endpoints are offset-based, but they are being migrated to cursor-based.
To make this transparent to you, the pagination response object is standardized to work with both methods.
## Pagination in responses
When an endpoint supports pagination, the response will include a `pagination` object with the following properties:
* `next`: The URL to the next page. This property will be omitted if the result set is empty or if the next page has no results.
* `previous`: The URL to the previous page. This property will be omitted if the result set is empty or if there's no previous page.
* `first`: The URL to the first page. This property will be omitted if the result set is empty.
Here are a few examples of how the `pagination` object may look like in the response:
```json On first page theme={null}
{
"pagination": {
"first": "https://api.enterprise.uphold.com/core/transactions?page=1&perPage=10",
"next": "https://api.enterprise.uphold.com/core/transactions?page=2&perPage=10",
}
}
```
```json On second page theme={null}
{
"pagination": {
"first": "https://api.enterprise.uphold.com/core/transactions?page=1&perPage=10",
"next": "https://api.enterprise.uphold.com/core/transactions?page=3&perPage=10",
"previous": "https://api.enterprise.uphold.com/core/transactions?page=1&perPage=10",
}
}
```
```json On last page theme={null}
{
"pagination": {
"first": "https://api.enterprise.uphold.com/core/transactions?page=1&perPage=10",
"previous": "https://api.enterprise.uphold.com/core/transactions?page=10&perPage=10",
}
}
```
Make sure to use these URLs when requesting pages instead of hardcoding them, otherwise your implementation may break as endpoints migrate to cursor-based pagination in the future.
## Number of results per page
To control how many results are returned per page, you can use the `perPage` query parameter. For example, to request 100 results per page, you would add `perPage=100` query parameter to the URL.
The `perPage` parameter will automatically be included in the URLs in the `pagination` object, so you don't need to worry about it when following the pagination links.
# Rate limits
Source: https://developer.uphold.com/rest-apis/rate-limits
Understand Uphold REST API rate limits and 429 Too Many Requests responses. Covers the three rate limit types and best practices for retries and backoff.
To ensure the stability and reliability of the Enterprise APIs, safeguards are in place against bursts of incoming traffic.
If you exceed the allowed number of requests in a short period, you may receive error responses with a `429 Too Many Requests` status code.
## Type of rate limits
There are three types of rate limits enforced:
* **Global rate limits**: These limits apply globally and cap the total number of requests your integration can make within a specific time period.
* **Specific endpoint rate limits**: These limits apply to certain endpoints and cap the total number of requests made to that endpoint within a specific time period.
* **User rate limits**: These limits apply to requests made on behalf of a user and cap the total number of requests made by that user within a specific time period.
The exact limits may vary per partner and contract. You will receive detailed rate limit information for your integration during onboarding.
## Rate limit headers
When you receive a `429 Too Many Requests` response, you also receive a `Reset-After` header with the amount of time in seconds you should wait before making another request.
# Test helpers
Source: https://developer.uphold.com/rest-apis/test-helpers
Use Uphold Sandbox-only test helpers to simulate deposits, withdrawals, and compliance state changes — speeding up integration testing without real events.
Test helpers are specialized endpoints designed to help you simulate various external events, scenarios, and states within the **Sandbox environment**, such as receiving deposits, handling withdrawals, or triggering compliance statuses.
Test helpers are only available in the Sandbox environment and will return `404 Not Found` if called in Production.
## Why use test helpers?
During development and testing, you often need to simulate external events that would normally be triggered by third-party systems or real-world actions. To simplify these simulations, test helpers enable you to:
Quickly and reliably simulate events without waiting for real-world processes (e.g., an incoming bank deposit).
Test edge cases and failure scenarios like account opening rejection and user sanctioned.
Validate your application's integration with the APIs in a controlled environment.
Build comprehensive automated test suites with predictable external event simulation.
Verify your handling of specific event-driven behaviors and webhook notifications.
Prepare and validate your implementation before moving to Production.
## Key concepts
* **Event triggering:** API-driven activation of events to simulate real-world scenarios.
* **Asynchronous behavior:** Many test helpers return a `202 Accepted` status, indicating that the request has been accepted and processing is asynchronous.
* **Resource management:** Automatically create or modify resources in response to simulated external events.
Before using test helpers, keep the following considerations in mind:
* Behavior in Sandbox may differ slightly from Production in terms of performance, response times, and event handling.
* Ensure your integration code is environment-aware to prevent accidental calls to Sandbox endpoints from Production environments.
Explore the detailed documentation for each test helper to start simulating events and scenarios, streamlining your testing and integration workflow.
You can find these endpoints within the **Test helpers** folder, located under each API's group of endpoints in the API documentation.
# Create session
Source: https://developer.uphold.com/rest-apis/topper-api/kyc-sharing/create-session
/_media/specs/topper-openapi.mintlify.json post /topper/kyc-sharing/sessions
Create a new session for KYC sharing.
# Identify user
Source: https://developer.uphold.com/rest-apis/topper-api/kyc-sharing/identify-user
/_media/specs/topper-openapi.mintlify.json post /topper/kyc-sharing/identify-user
Identify a user for KYC sharing.
# API versioning
Source: https://developer.uphold.com/rest-apis/versioning
How Uphold versions REST APIs to maintain backward compatibility and provide a clear upgrade path for breaking changes when they become necessary.
The REST APIs will be versioned to ensure backward compatibility and provide a clear upgrade path.
For the time being, Uphold is committed to maintaining compatibility and avoiding breaking changes. Should any become necessary, you will receive reasonable notice in advance.
More details about the versioning strategy will be shared when the need for backward-incompatible changes arises.
# Webhooks
Source: https://developer.uphold.com/rest-apis/webhooks
Receive near real-time events from Uphold's REST API endpoints via webhooks. Subscribe to user, account, transaction, KYC, and other event types.
Webhooks are a way for you to receive near real-time events of what's happening on the platform.
## Event types
Each group of endpoints has its own set of events that you can subscribe to. For example, the `Users` group of endpoints of the `Core API` has events like `core.user.created`, `core.user.deleted`, etc.
You may find the list of events that you can subscribe to in the documentation of the group of endpoints you are interested in.
## Consistent shape
Every webhook event has a consistent shape, with the following structure:
```json [expandable] theme={null}
{
// A unique identifier for the event.
"id": "6a3d8cea-5250-4bd0-a443-eb07b04c85d4",
// The type of the event.
"type": "core.users.created",
// The timestamp at which the event was created.
"createdAt": "2025-02-20T21:21:09.943Z",
// The data associated with the event, specific to the event type.
"data": {}
}
```
## Signature check
To prevent attackers from impersonating services by sending fake webhooks, each webhook delivery is signed with a unique key specific to the receiving endpoint. This signature allows you to verify that the webhook genuinely originates from Uphold and that only legitimate webhooks are processed.
Each webhook call includes three headers with additional information that are used for verification:
* `Webhook-Id`: The unique message identifier for the delivery. This identifier will be the same when the same webhook is being resent (e.g., due to a previous failure).
* `Webhook-Timestamp`: Timestamp in seconds since epoch of when the webhook was sent.
* `Webhook-Signature`: The Base64 encoded list of signatures (space delimited).
To learn more about how to verify the signature, refer to the [Webhook Signature Verification](https://docs.svix.com/receiving/verifying-payloads/why) from the webhook provider, Svix.
They offer a SDK for different languages to help you with the verification process or you can implement the verification manually by following their guide.
Do not confuse `Webhook-Id` and `Webhook-Timestamp` headers with `id` and `createdAt` fields in the event payload. The former are headers sent by Svix and are scoped to a delivery, while the latter are fields generated by the Uphold platform.
## Retry schedule
When a webhook delivery fails (e.g., due to a non-2xx response or timeout), the system will automatically retry the delivery based on an exponential backoff strategy. The schedule starts with short intervals and gradually increases over time, helping to avoid overwhelming the receiver. Retries continue for up to 24 hours or until a successful response is received.
In addition to automatic retries, you can also trigger manual redelivery attempts through the Enterprise Portal, under the Webhooks section. This is useful for immediate reprocessing or debugging.
For detailed information on the retry behavior, refer to the [retry policy](https://docs.svix.com/retries) from the webhook provider, Svix.
## Subscribing to webhooks
The easiest way to subscribe to webhooks is through the Webhooks Portal.
Not only that, but you can also check delivery logs and test your webhook endpoint to ensure it is working correctly.
There are two ways to get access to the Webhooks Portal:
* Via the REST API by calling the [Request webhook management link](./core-api/webhooks/request-management-link) endpoint.
* Via the Enterprise Portal, under the Webhooks section.
## IP whitelisting
If your webhook receiving endpoint is behind a firewall or NAT, you may need to allow traffic from Svix's IP addresses (the webhook provider).
You can find the list of Svix's IP addresses at [https://docs.svix.com/webhook-ips.json](https://docs.svix.com/webhook-ips.json).
# Create session
Source: https://developer.uphold.com/rest-apis/widgets-api/kyc/create-session
/_media/specs/widgets-openapi.mintlify.json post /widgets/kyc/sessions
Create a new session for the KYC Widget.
# Create session
Source: https://developer.uphold.com/rest-apis/widgets-api/payment/create-session
/_media/specs/widgets-openapi.mintlify.json post /widgets/payment/sessions
Create a new session for the Payment Widget.
# Create session
Source: https://developer.uphold.com/rest-apis/widgets-api/travel-rule/create-session
/_media/specs/widgets-openapi.mintlify.json post /widgets/travel-rule/sessions
Create a new session for the Travel Rule Widget.
# Widgets changelog
Source: https://developer.uphold.com/widgets/changelog
Track Uphold Widget releases — Payment Widget, Travel Rule Widget, and others — with new features, enhancements, and breaking changes (RSS available).
### Asset selection in crypto withdrawal flow
**Summary**
The crypto withdrawal flow in the [Payment Widget](/widgets/payment/introduction) now supports crypto asset selection, along network and destination address, simplifying the user journey and concentrating all data collection required for a crypto withdrawal inside the Payment Widget.
The `complete` event payload for `via: "crypto-network"` selections now includes a new `selection.asset` field containing the selected asset code (e.g. `BTC`, `XRP`).
The updated `CryptoNetworkSelection` type ships in `@uphold/enterprise-payment-widget-web-sdk` version `0.12` or higher — partners should upgrade to that version to get proper TypeScript support for the new `selection.asset` field.
**Documentation**
* [Crypto withdrawal via the Payment Widget](/developer-guides/crypto-transfers/withdrawal/via-payment-widget)
* [CryptoNetworkSelection type](/widgets/payment/sdk-reference#withdrawalselection)
### Dark mode support in Payment Widget
**Summary**
Introduced dark mode support in the [Payment Widget](/widgets/payment/introduction). By default, the Widget automatically matches the user's system appearance preference. Applications embedding the Widget can also force a specific theme by passing the `theme` option when instantiating it.
Theme override support requires `@uphold/enterprise-payment-widget-web-sdk` version `0.11` or higher.
**Documentation**
* [Payment Widget configuration options](/widgets/payment/sdk-reference#options)
### ACH support in Payment Widget
**Summary**
Introduced support for ACH bank transfers in the [Payment Widget](/widgets/payment/introduction), allowing users in the United States to perform ACH deposits and withdrawals.
**Documentation**
* [ACH deposit](/developer-guides/bank-transfers/deposit/via-payment-widget/ach)
* [ACH withdrawal](/developer-guides/bank-transfers/withdrawal/via-payment-widget/ach)
### Account limit per asset in Payment Widget
**Summary**
Introduced the `maxAccountsPerAsset` option in the [Payment Widget](/widgets/payment/introduction), allowing partners to set the number of accounts that can be created per asset when generating bank deposit details. Check the documentation for more details on how this option works.
**Documentation**
* [maxAccountsPerAsset option](/widgets/payment/sdk-reference#options)
### Crypto support and payment method filtering in Payment Widget
**Summary**
Introduced support for cryptocurrency deposits and withdrawals in the [Payment Widget](/widgets/payment/introduction). Users can now generate details for crypto deposits and provide destination addresses for crypto withdrawals.
Partners can now configure which payment methods to make available to their users through the new `paymentMethods` option. This allows filtering by payment type (card, bank, crypto), and for crypto and bank options, filtering which assets are available to the user.
**Documentation**
* [Payment method filtering](/widgets/payment/sdk-reference#options)
* [Crypto deposit](/developer-guides/crypto-transfers/deposit/via-payment-widget)
* [Crypto withdrawal](/developer-guides/crypto-transfers/withdrawal/via-payment-widget)
### Travel Rule Widget
**Summary**
Introduced the [Travel Rule Widget](/widgets/travel-rule/introduction), a fully Uphold-managed, embeddable solution that enables compliant collection and exchange of originator and beneficiary information for crypto transactions — ensuring adherence to global Travel Rule regulations.
### FPS support in Payment Widget
**Summary**
Introduced support for Faster Payments Service (FPS) in the [Payment Widget](/widgets/payment/introduction), supporting instant push bank deposits and bank withdrawals for users in the United Kingdom and EU.
**Documentation**
* [FPS deposit](/developer-guides/bank-transfers/deposit/via-payment-widget/fps)
* [FPS withdrawal](/developer-guides/bank-transfers/withdrawal/via-payment-widget/fps)
### Card support in Payment Widget
**Summary**
Introduced the [Payment Widget](/widgets/payment/introduction), a fully Uphold-managed, embeddable solution that allows partners to support payment methods without implementing the API endpoints.
The Payment Widget is launched with support for debit and credit cards.
**Documentation**
* [SDK reference](/widgets/payment/sdk-reference)
* [Card deposit](/developer-guides/card-transfers/deposit/via-payment-widget)
* [Card withdrawal](/developer-guides/card-transfers/withdrawal/via-payment-widget)
# Uphold Widgets introduction
Source: https://developer.uphold.com/widgets/introduction
Embed turnkey Uphold widgets — Payment, Travel Rule, KYC — to add payment methods, transactions, and compliance flows to your applications.
To reduce the time to market, we offer turnkey solutions in the form of widgets.
Widgets are designed to be easily integrated into your existing applications, allowing you to offer a seamless experience to your users with some degree of flexibility and customization.
Facilitate payments from and to your users.
Ensure compliance with Travel Rule regulations for crypto transfers.
KYC Widget Coming soon> } icon="address-card" href="/widgets/kyc">
Simplify collecting KYC from your users.
Offer crypto onramp and offramp to your users.
# KYC Widget introduction
Source: https://developer.uphold.com/widgets/kyc/introduction
The Uphold KYC Widget will simplify collecting KYC data from end-users. The widget is in development — contact Uphold if you'd like early access.
This widget is currently under development.
If you are interested in using this widget to simplify the collection of KYC information from end-users, please [reach out to us](https://uphold.com/enterprise#contact).
# Payment Widget installation and setup
Source: https://developer.uphold.com/widgets/payment/installation-and-setup
Install the Uphold Payment 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 Payment Widget will be running in your app, mounted to a container, and emitting events you can react to.
## Before you start
You'll need:
* Access to [Widgets API](/rest-apis/widgets-api/payment/create-session) to create widget sessions. Manage your access in [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 | Widget host |
| ----------- | ------------------------------------------------------ |
| Sandbox | `https://payment-widget.enterprise.sandbox.uphold.com` |
| Production | `https://payment-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-payment-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}
```
If your app does not use CSP, skip this step.
### 3. Create a session on your backend
The Payment 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.
Never call the Create Session endpoint from the client. Your Client Secret must not leave your backend.
Call [`POST /widgets/payment/sessions`](/rest-apis/widgets-api/payment/create-session) with the desired `flow` (`select-for-deposit`, `select-for-withdrawal`, or `authorize`) and the user the session is for:
```bash theme={null}
curl -X POST https://api.sandbox.uphold.com/widgets/payment/sessions \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "X-On-Behalf-Of: user $USER_ID" \
-H "Content-Type: application/json" \
-d '{ "flow": "select-for-deposit" }'
```
The response contains the three fields the SDK needs:
```json theme={null}
{
"flow": "select-for-deposit",
"url": "https://payment-widget.enterprise.sandbox.uphold.com/...",
"token": "..."
}
```
Pass the entire response object 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 `PaymentWidget` with the session from Step 3, then mount it into a container element.
```javascript [expandable] theme={null}
import { PaymentWidget } from '@uphold/enterprise-payment-widget-web-sdk';
// session is the object returned by your backend in Step 3
const widget = new PaymentWidget(session, {
paymentMethods: [
{ type: 'card' },
{ type: 'bank' },
{ type: 'crypto', assets: { include: ['BTC', 'ETH', 'XRP'] } }
],
theme: { appearance: 'dark' }, // omit to follow system preference
debug: true // verbose logging during development
});
widget.mountIframe(document.getElementById('payment-container'));
```
The container must have explicit CSS width and height — the iframe fills its bounds. Minimum recommended size is **400px × 600px**.
For type inference on the `complete` event, pass the flow as a generic: `new PaymentWidget<'select-for-deposit'>(session)`. See the [SDK reference](./sdk-reference#constructor) for full constructor details.
### 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 finished selection or authorization | Read `event.detail.value`, then call `widget.unmount()` |
| `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 |
The Widget does not unmount itself. You must call `widget.unmount()` from `complete`, `cancel`, and `error` handlers.
```javascript [expandable] theme={null}
widget.on('ready', () => {
console.log('Payment Widget is ready');
});
widget.on('complete', (event) => {
console.log('Selection:', event.detail.value);
widget.unmount();
});
widget.on('cancel', () => {
console.log('Payment cancelled');
widget.unmount();
});
widget.on('error', (event) => {
console.error('Payment error:', event.detail.error);
widget.unmount();
});
```
The shape of `event.detail.value` varies by flow. See [Events](./sdk-reference#events) in the SDK reference for the full type definitions.
### 6. Test in Sandbox
With your Sandbox credentials and the Sandbox Widget host configured, run through this checklist:
* The Widget mounts and `ready` fires.
* Selecting a payment method fires `complete` with the expected `event.detail.value` shape for your flow.
* Closing or dismissing the Widget fires `cancel`.
* 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 `PaymentWidget`. 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}
Payment Widget
```
The SDK must be included in your WebView bundle (e.g. via your build pipeline). Loading it from a CDN is not supported.
### Platform setup
```swift [expandable] theme={null}
import WebKit
class PaymentViewController: 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: "paymentWidgetMessage")
if let url = Bundle.main.url(forResource: "payment-widget", withExtension: "html") {
webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
}
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard message.name == "paymentWidgetMessage",
let messageDict = message.body as? [String: Any],
let type = messageDict["type"] as? String else {
return
}
let data = messageDict["data"]
switch type {
case "complete": handlePaymentComplete(data: data)
case "cancel": handlePaymentCancel()
case "error": handlePaymentError(error: data)
default: print("Unknown Payment Widget message type: \(type)")
}
}
private func handlePaymentComplete(data: Any?) { /* navigate to success */ }
private func handlePaymentCancel() { /* return user to previous screen */ }
private func handlePaymentError(error: Any?) { /* show error UI */ }
deinit {
webView?.configuration.userContentController.removeScriptMessageHandler(forName: "paymentWidgetMessage")
}
}
```
```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 PaymentBridge(), "PaymentBridge");
webView.loadUrl("file:///android_asset/payment-widget.html");
public class PaymentBridge {
@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": handlePaymentComplete(data != null ? data.toString() : null); break;
case "cancel": handlePaymentCancel(); break;
case "error": handlePaymentError(data != null ? data.toString() : null); break;
default: Log.w("Payment", "Unknown Payment Widget message type: " + type);
}
});
} catch (Exception e) {
Log.e("Payment", "Error parsing Payment Widget message", e);
}
}
private void handlePaymentComplete(String data) { /* navigate to success */ }
private void handlePaymentCancel() { /* return user to previous screen */ }
private void handlePaymentError(String error) { /* show error UI */ }
}
```
If you load local assets, ensure the WebView allows file access according to your security requirements (e.g. `setAllowFileAccess(true)` when needed).
```javascript [expandable] theme={null}
import { WebView } from 'react-native-webview';
import { Alert } from 'react-native';
const PaymentScreen = () => {
const handleMessage = (event) => {
try {
const message = JSON.parse(event.nativeEvent.data);
switch (message.type) {
case 'complete': handlePaymentComplete(message.data); break;
case 'cancel': handlePaymentCancel(); break;
case 'error': handlePaymentError(message.data); break;
default: console.log('Unknown message type:', message.type);
}
} catch (error) {
console.error('Error parsing WebView message:', error);
}
};
const handlePaymentComplete = (data) => { /* navigate to success */ };
const handlePaymentCancel = () => { /* return user to previous screen */ };
const handlePaymentError = (error) => { /* show error UI */ };
return (
);
};
```
For iOS, `file://` URLs may be restricted. Consider `source={{ html: '...' }}` or a bundled asset, adjusted per platform.
## Configuration reference
The most common SDK options. See the [SDK reference](./sdk-reference#options) for the full schema and all event types.
| Option | Purpose |
| --------------------- | ------------------------------------------------------------------------------- |
| `paymentMethods` | Filter which payment methods (and assets) appear. Omit to show all. |
| `theme` | Force `light` or `dark` appearance. Omit to follow the user's system setting. |
| `debug` | Verbose console logging. Use during development. |
| `maxAccountsPerAsset` | Cap accounts created per asset for crypto deposit flows (max effective: `100`). |
## 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.
* Read our Developer Guides for step-by-step instructions on implementing specific payment methods with the Widget:
- [Bank deposits](/developer-guides/bank-transfers/deposit/via-payment-widget)
- [Bank withdrawals](/developer-guides/bank-transfers/withdrawal/via-payment-widget)
* [Card deposits](/developer-guides/card-transfers/deposit/via-payment-widget)
* [Card withdrawals](/developer-guides/card-transfers/withdrawal/via-payment-widget)
* [Crypto deposits](/developer-guides/crypto-transfers/deposit/via-payment-widget)
* [Crypto withdrawals](/developer-guides/crypto-transfers/withdrawal/via-payment-widget)
# Payment Widget introduction
Source: https://developer.uphold.com/widgets/payment/introduction
The Uphold Payment Widget is an embeddable, managed solution that lets users securely add payment methods and run transactions across web and native apps.
The Payment Widget is a fully Uphold-managed, embeddable solution that allows users to securely add payin and payout methods.
It's built with developer efficiency and global readiness in mind, with support for multi-platform integration and a growing list of payment methods.
***
Fully managed UI with automated state handling and lightweight SDK to instantiate and control the Widget.
Go live quickly with minimal frontend effort.
Accept cards, bank, and crypto transfers out of the box.
Control which methods users see, with more options on the way.
Embed seamlessly into web apps via iframe and native apps via WebView.
Consistent user experience across all platforms.
SDK emits lifecycle events such as success, error, and cancellation, with minimal error handling required.
Integrate deeply with your app's flow and UI feedback.
## Payment methods
Read our Developer Guides for step-by-step instructions on implementing specific payment methods with the Widget.
* [Bank deposits](/developer-guides/bank-transfers/deposit/via-payment-widget)
* [Bank withdrawals](/developer-guides/bank-transfers/withdrawal/via-payment-widget)
* [Card deposits](/developer-guides/card-transfers/deposit/via-payment-widget)
* [Card withdrawals](/developer-guides/card-transfers/withdrawal/via-payment-widget)
* [Crypto deposits](/developer-guides/crypto-transfers/deposit/via-payment-widget)
* [Crypto withdrawals](/developer-guides/crypto-transfers/withdrawal/via-payment-widget)
# Payment Widget SDK reference
Source: https://developer.uphold.com/widgets/payment/sdk-reference
Reference for the @uphold/enterprise-payment-widget-web-sdk package, including the PaymentWidget class, constructor options, methods, and lifecycle events.
Complete reference documentation for the `@uphold/enterprise-payment-widget-web-sdk` package.
The main class for creating and managing Payment Widget instances is `PaymentWidget`. It requires a `PaymentWidgetSession` object that must be created through the API before instantiating the Widget.
## Constructor
```typescript theme={null}
new PaymentWidget(
session: PaymentWidgetSession,
options?: PaymentWidgetOptions
)
```
**Parameters:**
| Parameter | Type | Required | Description |
| --------- | ---------------------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
| `session` | `PaymentWidgetSession` | Yes | Payment session object obtained from the [Create session](/rest-apis/widgets-api/payment/create-session) endpoint |
| `options` | `PaymentWidgetOptions` | No | Configuration options for the Widget |
**Generic type parameter:**
The constructor accepts an optional generic type parameter that specifies the flow type. When provided, it enables better type inference for event handlers, particularly for the `complete` event.
**Examples:**
```typescript theme={null}
// Generic flow type (default)
const widget = new PaymentWidget(session);
// Specific flow type for better type inference
const depositWidget = new PaymentWidget<'select-for-deposit'>(session);
const withdrawWidget = new PaymentWidget<'select-for-withdrawal'>(session);
const authorizeWidget = new PaymentWidget<'authorize'>(session);
```
### Options
```typescript theme={null}
type PaymentWidgetOptions = {
debug?: boolean;
maxAccountsPerAsset?: number;
paymentMethods?: PaymentMethodOption[];
theme?: ThemeOption;
};
```
| Property | Type | Default | Description |
| --------------------- | ----------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `debug` | `boolean` | `false` | Enable debug mode for additional logging |
| `maxAccountsPerAsset` | `number` | No limit | Limit the number of accounts that can be created per asset. When the limit is reached, the Widget reuses the most recent account instead of creating a new one. Must be greater than `0`. Maximum effective value is `100` |
| `paymentMethods` | `PaymentMethodOption[]` | All available methods | Restrict which payment methods are available to users. See [PaymentMethodOption](#paymentmethodoption) for details |
| `theme` | `ThemeOption` | System preference | Control the visual appearance of the Widget. See [ThemeOption](#themeoption) for details |
**paymentMethods option:**
The `paymentMethods` option allows you to control which payment methods the Widget displays to users. When not specified, all supported payment methods are available.
**Basic usage:**
```typescript theme={null}
const widget = new PaymentWidget(session, {
paymentMethods: [
{ type: 'card' },
{ type: 'bank' },
{ type: 'crypto' }
]
});
```
**Filtering assets:**
For `bank` and `crypto` payment methods, you can filter which assets are available using the `assets` property. This works the same way for both methods:
```typescript theme={null}
const widget = new PaymentWidget(session, {
paymentMethods: [
{ type: 'card' },
{
type: 'bank',
assets: {
include: ['GBP', 'EUR', 'ETH', 'BTC'] // Only these assets will be available
}
},
{
type: 'crypto',
assets: {
include: ['BTC', 'ETH', 'XRP'] // Only these assets will be available
}
}
]
});
```
You can also exclude specific assets while allowing all others:
```typescript theme={null}
const widget = new PaymentWidget(session, {
paymentMethods: [
{
type: 'bank',
assets: {
exclude: ['BTC'] // All assets except BTC
}
},
{
type: 'crypto',
assets: {
exclude: ['DOGE', 'SHIB'] // All assets except these
}
}
]
});
```
If no `assets` filter is provided, all supported assets for that payment method will be available. Use either `include` or `exclude`, not both.
**theme option:**
The `theme` option lets you control the visual appearance of the Widget. By default, the Widget automatically detects and matches the user's browser or OS appearance preference (`light` or `dark`). Use this option when you want to enforce a specific theme regardless of the user's system settings.
```typescript theme={null}
const widget = new PaymentWidget(session, {
theme: { appearance: 'dark' }
});
```
**maxAccountsPerAsset option:**
When generating deposit details, depending on the rail's constraints, the Widget allows users to select a target account for the deposit. The `maxAccountsPerAsset` option controls how many accounts can be created per asset. For example, setting it to 1 ensures only a single account exists per asset, while setting it to 5 allows up to five accounts for the same asset (e.g. one USD account for general use, another for a vacation fund, and so on). Once the limit is reached, the Widget automatically reuses the most recent account instead of creating a new one.
```typescript theme={null}
const widget = new PaymentWidget(session, {
maxAccountsPerAsset: 5
});
```
Values above `100` are capped at `100`.
## Methods
### `mountIframe()`
Mounts the Payment Widget iframe to the specified DOM element.
**Parameters:**
* `element`: The HTML element where the Widget should be mounted
**Example:**
```javascript theme={null}
// Ensure the container has appropriate sizing
const container = document.getElementById('payment-container');
widget.mountIframe(container);
```
The container element should have explicit dimensions set via CSS. The Widget will fill the entire container. Minimum recommended size is 400px width × 600px height for optimal user experience.
### `unmount()`
Unmounts and cleans up the Payment Widget iframe.
**Example:**
```javascript theme={null}
widget.unmount();
```
The Widget will not unmount itself when a flow is finished, either by successful completion, user cancellation, or unrecoverable error, so it must always be called manually.
### `on()`
Registers an event listener for Widget events.
**Parameters:**
* `event`: The event name to listen for
* `callback`: Function to execute when the event is triggered
**Example:**
```javascript theme={null}
widget.on('complete', (event) => {
console.log('Payment completed:', event.detail.value);
});
```
### `off()`
Removes an event listener for Payment Widget events.
**Parameters:**
* `event`: The event name to stop listening for
* `callback`: The specific function to remove (must be the same reference as used in `on()`)
**Example:**
```javascript theme={null}
const handleComplete = (event) => {
console.log('Payment flow completed:', event.detail.value);
};
// Register the listener
widget.on('complete', handleComplete);
// Remove the listener
widget.off('complete', handleComplete);
```
## Events
The Payment Widget emits several events during its lifecycle that you can listen to using the `on()` method.
### `complete`
Fired when the Payment flow is completed.
```typescript theme={null}
type PaymentWidgetCompleteValue =
T extends 'select-for-deposit' ? DepositSelection :
T extends 'select-for-withdrawal' ? WithdrawalSelection :
T extends 'authorize' ? AuthorizeResult :
never;
type PaymentWidgetCompleteEvent = {
detail: {
value: PaymentWidgetCompleteValue;
};
};
```
The `event.detail.value` contains the payment flow result. The structure depends on the flow type.
**Example:**
```typescript theme={null}
widget.on('complete', (event: PaymentWidgetCompleteEvent) => {
const result = event.detail.value;
console.log('Payment flow completed:', result);
// Process the result based on your flow type
// See flow-specific examples below
widget.unmount();
});
```
**Usage in different flows:**
For `select-for-deposit` flows:
```typescript theme={null}
const depositWidget = new PaymentWidget<'select-for-deposit'>(session);
depositWidget.on('complete', (event) => {
const result = event.detail.value;
if (result.via === 'external-account') {
// User selected a saved payment method (e.g., card)
console.log('Selected external account:', result.selection);
} else if (result.via === 'deposit-method') {
// User selected a bank or crypto transfer method
const { depositMethod, account } = result.selection;
if (depositMethod.type === 'bank') {
console.log('Bank transfer details:', depositMethod.details);
} else if (depositMethod.type === 'crypto') {
console.log('Crypto transfer details:', depositMethod.details);
}
console.log('Target account:', account);
}
depositWidget.unmount();
});
```
For `select-for-withdrawal` flows:
```typescript theme={null}
const withdrawWidget = new PaymentWidget<'select-for-withdrawal'>(session);
withdrawWidget.on('complete', (event) => {
const result = event.detail.value;
if (result.via === 'external-account') {
// User selected a saved payment method (e.g., card, bank account)
console.log('Selected external account:', result.selection);
} else if (result.via === 'crypto-network') {
// User provided a crypto withdrawal address
console.log('Crypto withdrawal details:', {
asset: result.selection.asset,
network: result.selection.network,
address: result.selection.address,
reference: result.selection.reference // Destination tag for XRP, memo for XLM, etc.
});
}
withdrawWidget.unmount();
});
```
For `authorize` flows:
```typescript theme={null}
const authorizeWidget = new PaymentWidget<'authorize'>(session);
authorizeWidget.on('complete', (event) => {
const result = event.detail.value;
if (result.trigger.reason === 'transaction-status-changed') {
// Check transaction status - it may have succeeded or failed
if (result.transaction.status === 'completed') {
console.log('Transaction successful:', result.transaction);
} else if (result.transaction.status === 'failed') {
console.error('Transaction failed:', result.transaction);
// Handle failure case
}
} else if (result.trigger.reason === 'max-retries-reached') {
console.log('Polling timeout reached, transaction may still be processing: ', result.transaction);
// Implement additional polling or show a message indicating that the transaction is still processing and to try again later
}
authorizeWidget.unmount();
});
```
### `cancel`
Fired when the user cancels the payment flow.
```typescript theme={null}
type PaymentWidgetCancelEvent = {
detail: {};
}
```
**Example:**
```javascript theme={null}
widget.on('cancel', (event: PaymentWidgetCancelEvent) => {
console.log('Payment cancelled by user');
widget.unmount();
});
```
### `error`
Fired when an unrecoverable error occurs during the payment flow.
```typescript theme={null}
type PaymentWidgetError = {
name: string;
code: string;
message: string;
details?: Record;
cause?: PaymentWidgetError;
httpStatusCode?: number;
}
type PaymentWidgetErrorEvent = {
detail: {
error: PaymentWidgetError;
};
};
```
**Example:**
```javascript theme={null}
widget.on('error', (event: PaymentWidgetErrorEvent) => {
const { error } = event.detail;
console.error('Payment error:', error);
// Handle specific error types
if (error.code === 'entity_not_found') {
// Quote expired - redirect to create new quote
handleExpiredQuote();
} else if (error.code === 'insufficient_balance') {
// Not enough funds - show funding options
handleInsufficientFunds();
} else {
// Generic error handling
showGenericError(error.message);
}
widget.unmount();
});
```
### `ready`
Fired when the Payment Widget has finished loading.
```typescript theme={null}
type PaymentWidgetReadyEvent = {
detail: {};
}
```
**Example:**
```typescript theme={null}
widget.on('ready', (event: PaymentWidgetReadyEvent) => {
console.log('Payment Widget is ready');
});
```
## Types
### PaymentWidgetSession
The session object returned by the [Create session](/rest-apis/widgets-api/payment/create-session) endpoint.
```typescript theme={null}
type PaymentWidgetSession = {
url: string;
token: string;
flow: PaymentWidgetFlow;
}
```
### `PaymentWidgetFlow`
Represents the different flows supported by the Payment Widget.
```typescript theme={null}
type PaymentWidgetFlow = 'select-for-deposit' | 'select-for-withdrawal' | 'authorize';
```
### ThemeOption
Controls the visual appearance of the Widget.
```typescript theme={null}
type ThemeOption = {
appearance: 'light' | 'dark';
};
```
| Property | Type | Description |
| ------------ | ------------------- | ------------------------------------------------------------------------------------------ |
| `appearance` | `'light' \| 'dark'` | Forces the Widget to render in light or dark mode, overriding the user's system preference |
### PaymentMethodOption
Defines the payment methods that can be configured in the Widget options.
```typescript theme={null}
type PaymentMethodOption =
| { type: 'card' }
| { type: 'bank'; assets?: PaymentAssetOptions }
| { type: 'crypto'; assets?: PaymentAssetOptions };
```
| Type | Description |
| -------- | --------------------------------------------------------------- |
| `card` | Credit and debit card payments |
| `bank` | Push bank deposit methods. Supports optional `assets` filtering |
| `crypto` | Crypto deposits. Supports optional `assets` filtering |
### PaymentAssetOptions
Allows filtering which assets are available when using the `bank` or `crypto` payment methods.
```typescript theme={null}
type PaymentAssetOptions = {
include?: string[];
exclude?: string[];
};
```
| Property | Type | Description |
| --------- | ---------- | ----------------------------------------------------------------------------------------- |
| `include` | `string[]` | List of asset codes to include. When specified, only these assets will be available |
| `exclude` | `string[]` | List of asset codes to exclude. When specified, all assets except these will be available |
Use either `include` or `exclude`, not both. Asset codes should be uppercase (e.g., `'BTC'`, `'ETH'`, `'XRP'`).
### Complete event result types
#### DepositSelection
Result structure for `select-for-deposit` flow:
```typescript theme={null}
type ExternalAccountSelection = {
via: 'external-account';
selection: ExternalAccount; // See external accounts API documentation
};
type AccountDepositMethodSelection = {
via: 'deposit-method';
selection: {
depositMethod: AccountDepositMethod; // See account deposit methods API documentation
account: Account; // See accounts API documentation
};
};
type DepositSelection = ExternalAccountSelection | AccountDepositMethodSelection;
```
For complete type definitions, see the API documentation:
* `ExternalAccount`: [External accounts](/rest-apis/core-api/external-accounts/introduction)
* `AccountDepositMethod`: [Account deposit methods](/rest-apis/core-api/accounts/get-account-deposit-method)
* `Account`: [Accounts](/rest-apis/core-api/accounts/introduction)
#### WithdrawalSelection
Result structure for `select-for-withdrawal` flow:
```typescript theme={null}
type ExternalAccountSelection = {
via: 'external-account';
selection: ExternalAccount; // See external accounts API documentation
};
type CryptoNetworkSelection = {
via: 'crypto-network';
selection: {
asset: string; // e.g., 'BTC', 'ETH', 'XRP'
network: string; // e.g., 'bitcoin', 'ethereum', 'xrp-ledger'
address: string; // The destination crypto address
reference?: string; // Destination tag for XRP, memo for XLM, etc.
};
};
type WithdrawalSelection = ExternalAccountSelection | CryptoNetworkSelection;
```
For complete `ExternalAccount` type definition, see the [External accounts](/rest-apis/core-api/external-accounts/introduction) API documentation.
#### AuthorizeResult
Result structure for `authorize` flow:
```typescript theme={null}
type AuthorizeResult = {
transaction: Transaction; // See Transactions API documentation
trigger: {
reason: 'transaction-status-changed' | 'max-retries-reached';
};
}
```
For complete `Transaction` type definition, see the [Transactions](/rest-apis/core-api/transactions/introduction) API documentation.
## Complete usage example
Here's an end-to-end example showing how to use the Payment Widget with all events and type safety:
```typescript theme={null}
import { PaymentWidget } from '@uphold/enterprise-payment-widget-web-sdk';
// Create a Payment Widget session from your backend
const session = await createPaymentWidgetSession();
// Initialize the Widget with type inference and configuration
const widget = new PaymentWidget<'select-for-deposit'>(session, {
debug: true,
paymentMethods: [
{ type: 'card' },
{ type: 'bank' },
{ type: 'crypto', assets: { include: ['BTC', 'ETH', 'XRP'] } }
]
});
// Handle ready event
widget.on('ready', () => {
console.log('Payment Widget is ready');
});
// Handle completion with flow-specific type-safe result
widget.on('complete', (event) => {
const result = event.detail.value;
if (result.via === 'external-account') {
console.log('User selected saved payment method:', result.selection);
} else if (result.via === 'deposit-method') {
console.log('User selected deposit method:', result.selection.depositMethod);
console.log('Target account:', result.selection.account);
}
widget.unmount();
});
// Handle cancellation
widget.on('cancel', () => {
console.log('Payment cancelled by user');
widget.unmount();
});
// Handle errors with specific error handling
widget.on('error', (event) => {
const { error } = event.detail;
console.error('Payment error:', error);
if (error.code === 'entity_not_found') {
handleExpiredQuote();
} else if (error.code === 'insufficient_balance') {
handleInsufficientFunds();
} else {
showGenericError(error.message);
}
widget.unmount();
});
// Mount the Widget to a DOM element
widget.mountIframe(document.getElementById('payment-container'));
```
# Topper Widget introduction
Source: https://developer.uphold.com/widgets/topper/introduction
The Topper Widget has its own dedicated documentation site. Visit the Topper API documentation for installation, configuration, and integration details.
The Topper Widget has a dedicated documentation site.
Please refer to the [Topper API documentation](https://docs.topperpay.com/) for more information.
# Travel Rule Widget installation and setup
Source: https://developer.uphold.com/widgets/travel-rule/installation-and-setup
Install the Uphold Travel Rule Widget SDK and set it up in web and native apps with sessions tied to a quote or transaction with the travel-rule requirement.
This guide walks you through installing the Travel Rule Widget SDK and integrating it into your web or native application.
## Prerequisites
* **Access to [Widgets API](/rest-apis/widgets-api/travel-rule/create-session)** to create widget sessions.
* A quote or transaction with the **travel-rule** requirement.
## Installation
Install the SDK via npm:
```bash theme={null}
npm install @uphold/enterprise-travel-rule-widget-web-sdk
```
## Quick start
The minimal flow has three steps: create a session on your backend, mount the widget on the page, and handle the `complete` event when the user finishes the form.
```javascript theme={null}
import { TravelRuleWidget } from '@uphold/enterprise-travel-rule-widget-web-sdk';
// 1. Create a session via the Widgets API on your backend
const session = await createTravelRuleWidgetSession();
// 2. Mount the widget into a container
const widget = new TravelRuleWidget(session);
widget.mountIframe(document.getElementById('travel-rule-container'));
// 3. Handle the result
widget.on('complete', (event) => {
console.log('Travel Rule completed:', event.detail.value);
widget.unmount();
});
```
The container that hosts the widget must have explicit dimensions in CSS — the widget fills the container bounds. Minimum recommended size is 400px × 600px.
For a more complete example with all event handlers, see [Web app integration](#web-app-integration) below. For the complete API, see the [SDK reference](./sdk-reference).
## Web app integration
Import and initialize the SDK in your application.
Make sure the container that will host the widget has explicit dimensions in CSS. The Widget will fill the container bounds. Minimum recommended size is 400px × 600px.
```javascript [expandable] theme={null}
import { TravelRuleWidget } from '@uphold/enterprise-travel-rule-widget-web-sdk';
// Create the widget instance
const widget = new TravelRuleWidget(session, { debug: true });
// Set up event handlers
widget.on('complete', (event) => {
console.log('Travel Rule form completed:', event.detail.value);
widget.unmount();
});
widget.on('cancel', () => {
console.log('Travel Rule form cancelled by user');
widget.unmount();
});
widget.on('error', (event) => {
console.error('Travel Rule form error:', event.detail.error);
widget.unmount();
});
// Mount the widget's iframe to a DOM element
widget.mountIframe(document.getElementById('tr-container'));
```
The `session` parameter is obtained from the [Create Session](/rest-apis/widgets-api/travel-rule/create-session) endpoint.
## Native app integration
Native mobile applications integrate the Travel Rule Widget using a WebView component that loads an HTML page containing the Travel Rule Widget SDK.
To handle Travel Rule Widget [events](./sdk-reference#events), your native application needs to implement a communication bridge between the WebView and native code. This bridge enables your native app to receive and respond to events from the Travel Rule Widget.
### WebView HTML template
Create an HTML file that includes the Travel Rule Widget SDK in your JS bundle:
```html [expandable] theme={null}
Travel Rule widget
```
The Travel Rule Widget SDK must be included in your WebView bundle (for example, via your build pipeline). Loading the SDK directly from a CDN is not supported.
### Setting up the WebView
Configure the WebView and event bridge for your platform:
```swift [expandable] theme={null}
import WebKit
class TravelRuleViewController: UIViewController, WKScriptMessageHandler {
@IBOutlet weak var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
// Set up single message handler for all widget events
let contentController = webView.configuration.userContentController
contentController.add(self, name: "travelRuleWidgetMessage")
// Load your HTML file with the widget
if let url = Bundle.main.url(forResource: "travel-rule-widget", withExtension: "html") {
webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
}
}
// Handle messages from the WebView
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == "travelRuleWidgetMessage" {
guard let messageDict = message.body as? [String: Any],
let type = messageDict["type"] as? String else {
return
}
let data = messageDict["data"]
switch type {
case "complete":
handleTravelRuleComplete(data: data)
case "cancel":
handleTravelRuleCancel()
case "error":
handleTravelRuleError(error: data)
default:
print("Unknown Travel Rule widget message type: \(type)")
}
}
}
private func handleTravelRuleComplete(data: Any?) {
// Handle successful Travel Rule compliance completion
print("Travel Rule form completed: \(data ?? "no data")")
// Proceed with the crypto transaction
}
private func handleTravelRuleCancel() {
// Handle Travel Rule process cancellation
print("Travel Rule form cancelled by user")
// Navigate back or show cancellation message
}
private func handleTravelRuleError(error: Any?) {
// Handle Travel Rule error
print("Travel Rule form error: \(error ?? "unknown error")")
// Show error message to user
}
}
```
```java [expandable] theme={null}
import android.webkit.WebView;
import android.webkit.WebSettings;
import android.webkit.JavascriptInterface;
import android.util.Log;
import org.json.JSONObject;
// Set up WebView
WebView webView = findViewById(R.id.webview);
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
// Add JavaScript interface to handle events from WebView
webView.addJavascriptInterface(new TravelRuleBridge(), "TravelRuleBridge");
// Load your HTML file with the widget
webView.loadUrl("file:///android_asset/travel-rule-widget.html");
// JavaScript interface class to handle WebView events
public class TravelRuleBridge {
@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":
handleTravelRuleComplete(data != null ? data.toString() : null);
break;
case "cancel":
handleTravelRuleCancel();
break;
case "error":
handleTravelRuleError(data != null ? data.toString() : null);
break;
default:
Log.w("TravelRule", "Unknown Travel Rule widget message type: " + type);
}
});
} catch (Exception e) {
Log.e("TravelRule", "Error parsing Travel Rule widget message", e);
}
}
private void handleTravelRuleComplete(String data) {
// Handle successful Travel Rule compliance completion
Log.d("TravelRule", "Travel Rule form completed: " + data);
// Proceed with the crypto transaction
}
private void handleTravelRuleCancel() {
// Handle Travel Rule process cancellation
Log.d("TravelRule", "Travel Rule form cancelled by user");
// Navigate back or show cancellation message
}
private void handleTravelRuleError(String error) {
// Handle Travel Rule error
Log.e("TravelRule", "Travel Rule form error: " + error);
// Show error message to user
}
}
```
If you load local assets, ensure the WebView allows file access according to your security requirements (e.g., `setAllowFileAccess(true)` when needed).
```javascript [expandable] theme={null}
import { WebView } from 'react-native-webview';
import { Alert } from 'react-native';
const TravelRuleScreen = () => {
const handleMessage = (event) => {
try {
const message = JSON.parse(event.nativeEvent.data);
switch (message.type) {
case 'complete':
handleTravelRuleComplete(message.data);
break;
case 'cancel':
handleTravelRuleCancel();
break;
case 'error':
handleTravelRuleError(message.data);
break;
default:
console.log('Unknown message type:', message.type);
}
} catch (error) {
console.error('Error parsing WebView message:', error);
}
};
const handleTravelRuleComplete = (data) => {
// Handle successful Travel Rule compliance completion
console.log('Travel Rule form completed:', data);
Alert.alert('Success', 'Travel Rule compliance completed!');
// Proceed with the crypto transaction
};
const handleTravelRuleCancel = () => {
// Handle Travel Rule process cancellation
console.log('Travel Rule form cancelled by user');
Alert.alert('Cancelled', 'Travel Rule process was cancelled');
// Navigate back
};
const handleTravelRuleError = (error) => {
// Handle Travel Rule error
console.error('Travel Rule form error:', error);
Alert.alert('Error', 'An error occurred during Travel Rule compliance');
// Show error message or retry option
};
return (
);
};
```
For iOS, `file://` URLs may be restricted. Consider using `source={{ html: '...' }}` or loading a bundled asset and adjusting the URI per platform.
## Troubleshooting
**Widget not displaying**
The widget loads in an iframe, which requires proper Content Security Policy (CSP) configuration. Add the following domains to your CSP directives:
* **Sandbox**: `https://travel-rule-widget.enterprise.sandbox.uphold.com`
* **Production**: `https://travel-rule-widget.enterprise.uphold.com`
Example CSP configuration:
```html theme={null}
```
**Events not firing in native apps**
Verify that:
* JavaScript is enabled in the WebView.
* The message bridge is properly registered before the HTML page loads.
* Event handlers match the platform-specific bridge implementation.
## Next steps
* Review the complete [SDK Reference](./sdk-reference) for all available methods and events.
* See [Handle quote requirements](/developer-guides/crypto-transfers/withdrawal/via-rest-api#handle-quote-requirements) and [Handle on-hold transactions](/developer-guides/crypto-transfers/deposit/via-rest-api#handle-on-hold-transactions) for practical examples of using the Travel Rule Widget in transactions.
# Travel Rule Widget introduction
Source: https://developer.uphold.com/widgets/travel-rule/introduction
The Uphold Travel Rule Widget is an embeddable solution for FATF-compliant collection and exchange of originator and beneficiary info on crypto transactions.
The Travel Rule widget is a fully Uphold-managed, embeddable solution that enables compliant collection and exchange of originator and beneficiary information for crypto transactions — ensuring adherence to FATF Recommendation 16 and global Travel Rule regulations.
## Features
Automates collection and exchange of originator and beneficiary information.
Embeds seamlessly into web and native apps.
Emits lifecycle events to integrate with your application flow.
Lightweight SDK to instantiate and control the widget.
## Using the widget
The Travel Rule widget is designed to be embedded into your application to collect required information from users during crypto transactions.
### Installation
The widget is available as an npm package for web applications. For detailed installation and setup instructions, see [Installation and Setup](./installation-and-setup).
### Integration
To integrate the widget into your application:
1. Create a widget session using the [Create Session](/rest-apis/widgets-api/travel-rule/create-session) endpoint
2. Initialize the widget with the session data
3. Handle lifecycle events (complete, cancel, error)
4. Submit the collected data
For a complete API reference, see [SDK Reference](./sdk-reference).
The widget supports both web applications (via iframe) and native applications (via WebView). See [Native App Integration](./installation-and-setup#native-app-integration) for platform-specific guidance.
## When to use the widget
The widget was specifically designed to address Travel Rule compliance requirements during crypto transactions.
### Quote requirements
When creating a quote for a crypto withdrawal, the response may include a `travel-rule` requirement indicating that Travel Rule information must be collected before the transaction can be executed.
For a complete example, see the [Handle quote requirements](/developer-guides/crypto-transfers/withdrawal/via-rest-api#handle-quote-requirements) section of the crypto withdrawal guide.
### Transaction RFIs
When a crypto deposit transaction is placed on hold with a `travel-rule` request for information (RFI), the widget must be used to collect the required information to allow the transaction to proceed.
For a complete example, see the [Handle on-hold transactions](/developer-guides/crypto-transfers/deposit/via-rest-api#handle-on-hold-transactions) section of the crypto deposit guide.
# Travel Rule Widget SDK reference
Source: https://developer.uphold.com/widgets/travel-rule/sdk-reference
Reference for the @uphold/enterprise-travel-rule-widget-web-sdk package, with the TravelRuleWidget class, constructor options, methods, and event handlers.
Complete reference documentation for the `@uphold/enterprise-travel-rule-widget-web-sdk` package.
The main class for creating and managing Travel Rule Widget instances is `TravelRuleWidget`. It requires a `TravelRuleWidgetSession` object that must be created through the API before instantiating the Widget.
## Constructor
```typescript theme={null}
new TravelRuleWidget(
session: TravelRuleWidgetSession,
options?: TravelRuleWidgetOptions
)
```
**Parameters:**
| Parameter | Type | Required | Description |
| --------- | ------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------- |
| `session` | `TravelRuleWidgetSession` | Yes | Travel rule session object obtained from the [Create session](/rest-apis/widgets-api/travel-rule/create-session) endpoint |
| `options` | `TravelRuleWidgetOptions` | No | Configuration options for the Widget |
**Generic type parameter:**
The constructor accepts an optional generic type parameter that specifies the flow type. When provided, it enables better type inference for event handlers, particularly for the `complete` event.
**Examples:**
```typescript theme={null}
// Generic flow type (default)
const widget = new TravelRuleWidget(session);
// Specific flow type for better type inference
const depositWidget = new TravelRuleWidget<'deposit-form'>(session);
const withdrawalWidget = new TravelRuleWidget<'withdrawal-form'>(session);
```
### Options
```typescript theme={null}
type TravelRuleWidgetOptions = {
debug?: boolean;
};
```
| Property | Type | Default | Description |
| -------- | --------- | ------- | ---------------------------------------- |
| `debug` | `boolean` | `false` | Enable debug mode for additional logging |
## Methods
### `mountIframe()`
Mounts the Travel Rule Widget iframe to the specified DOM element.
**Parameters:**
* `element`: The HTML element where the Widget should be mounted
**Example:**
```javascript theme={null}
// Ensure the container has appropriate sizing
const container = document.getElementById('tr-container');
widget.mountIframe(container);
```
The container element should have explicit dimensions set via CSS. The Widget will fill the entire container. Minimum recommended size is 400px width × 600px height for optimal user experience.
### `unmount()`
Unmounts and cleans up the Travel Rule Widget iframe.
**Example:**
```javascript theme={null}
widget.unmount();
```
The Widget will not unmount itself when a flow is finished, either by successful completion, user cancellation, or unrecoverable error, so it must always be called manually.
### `on()`
Registers an event listener for Widget events.
**Parameters:**
* `event`: The event name to listen for
* `callback`: Function to execute when the event is triggered
**Example:**
```javascript theme={null}
widget.on('complete', (event) => {
console.log('Travel Rule form completed:', event.detail.value);
});
```
### `off()`
Removes an event listener for Travel Rule Widget events.
**Parameters:**
* `event`: The event name to stop listening for
* `callback`: The specific function to remove (must be the same reference as used in `on()`)
**Example:**
```javascript theme={null}
const handleComplete = (event) => {
console.log('Travel Rule form completed:', event.detail.value);
};
// Register the listener
widget.on('complete', handleComplete);
// Remove the listener
widget.off('complete', handleComplete);
```
## Events
The Travel Rule Widget emits several events during its lifecycle that you can listen to using the `on()` method.
### `complete`
Fired when the Travel Rule form is completed successfully.
```typescript theme={null}
type TravelRuleWidgetCompleteEvent = {
detail: {
value: T;
};
};
```
The `event.detail.value` contains the Travel Rule compliance data collected from the user. This is an opaque data structure that should be passed to your backend and used when resolving RFIs or creating transactions.
**Example:**
```typescript theme={null}
widget.on('complete', (event: TravelRuleWidgetCompleteEvent) => {
const travelRuleData = event.detail.value;
console.log('Travel Rule form completed:', travelRuleData);
// Send the data to your backend to resolve the RFI or create the transaction
await submitTravelRuleData(travelRuleData);
widget.unmount();
});
```
**Usage in different flows:**
For deposit flows:
```typescript theme={null}
const depositWidget = new TravelRuleWidget<'deposit-form'>(session);
depositWidget.on('complete', async (event) => {
// Use the travel rule data to resolve the quote requirement
await resolveQuoteRequirement(quoteId, event.detail.value);
depositWidget.unmount();
});
```
For withdrawal flows:
```typescript theme={null}
const withdrawalWidget = new TravelRuleWidget<'withdrawal-form'>(session);
withdrawalWidget.on('complete', async (event) => {
// Use the travel rule data when creating the transaction
await createTransaction({
...transactionParams,
travelRuleData: event.detail.value
});
withdrawalWidget.unmount();
});
```
### `cancel`
Fired when the user cancels the Travel Rule form.
```typescript theme={null}
type TravelRuleWidgetCancelEvent = {
detail: {};
}
```
**Example:**
```javascript theme={null}
widget.on('cancel', (event: TravelRuleWidgetCancelEvent) => {
console.log('Travel Rule form cancelled by user');
widget.unmount();
});
```
### `error`
Fired when an unrecoverable error occurs during the Travel Rule flow.
```typescript theme={null}
type TravelRuleWidgetError = {
name: string;
code: string;
message: string;
details?: Record;
stack?: string;
cause?: TravelRuleWidgetError;
httpStatusCode?: number;
}
type TravelRuleWidgetErrorEvent = {
detail: {
error: TravelRuleWidgetError;
};
};
```
**Example:**
```typescript theme={null}
widget.on('error', (event: TravelRuleWidgetErrorEvent) => {
const { error } = event.detail;
console.error('Travel Rule form error:', error);
// Handle specific error types
if (error.code === 'entity_not_found') {
// Quote or transaction not found - may have expired
handleExpiredSession();
} else if (error.code === 'validation_failed') {
// Form validation error (shouldn't happen in normal flow)
console.error('Validation error:', error.details);
} else {
// Generic error handling
showGenericError(error.message);
}
widget.unmount();
});
```
### `ready`
Fired when the Travel Rule Widget has finished loading and is ready for user interaction.
```typescript theme={null}
type TravelRuleWidgetReadyEvent = {
detail: {};
}
```
**Example:**
```typescript theme={null}
widget.on('ready', (event: TravelRuleWidgetReadyEvent) => {
console.log('Travel Rule form is ready');
});
```
## Types
### TravelRuleWidgetSession
The session object obtained from the [Create session](/rest-apis/widgets-api/travel-rule/create-session) endpoint.
```typescript theme={null}
type TravelRuleWidgetSession = {
url: string;
token: string;
flow: TravelRuleWidgetFlow;
data: TravelRuleWidgetData;
}
```
### TravelRuleWidgetFlow
Represents the different flows supported by the Travel Rule Widget.
```typescript theme={null}
type TravelRuleWidgetFlow = 'deposit-form' | 'withdrawal-form';
```
### TravelRuleWidgetData
The data object included in the session, containing context for the flow.
```typescript theme={null}
type TravelRuleWidgetData = {
provider: 'notabene';
parameters: object;
};
```
### TravelRuleResult
The result object returned upon successful completion of the Travel Rule form.
```typescript theme={null}
type TravelRuleResult = Record;
```
The `event.detail.value` object is an opaque data structure that should be used as-is when resolving RFIs or creating transactions.
## Complete usage example
Here's an end-to-end example showing how to use the Travel Rule Widget with all events and type safety:
```typescript theme={null}
import { TravelRuleWidget } from '@uphold/enterprise-travel-rule-widget-web-sdk';
// Create a Travel Rule Widget session from your backend
const session = await createTravelRuleWidgetSession(quoteId);
// Initialize the Widget with type inference
const widget = new TravelRuleWidget<'deposit-form'>(session, { debug: true });
// Handle ready event
widget.on('ready', () => {
console.log('Travel Rule form is ready');
});
// Handle completion with the Travel Rule data
widget.on('complete', async (event) => {
const travelRuleData = event.detail.value;
console.log('Travel Rule form completed:', travelRuleData);
try {
// Send the data to your backend to resolve the quote requirement
await resolveQuoteRequirement(quoteId, travelRuleData);
// Proceed with the transaction flow
showSuccessMessage('Travel Rule compliance completed');
} catch (error) {
console.error('Failed to submit Travel Rule data:', error);
showErrorMessage('Failed to submit compliance information');
}
widget.unmount();
});
// Handle cancellation
widget.on('cancel', () => {
console.log('Travel Rule form cancelled by user');
showCancellationMessage();
widget.unmount();
});
// Handle errors with specific error handling
widget.on('error', (event) => {
const { error } = event.detail;
console.error('Travel Rule form error:', error);
if (error.code === 'entity_not_found') {
showErrorMessage('Session expired. Please start over.');
} else {
showErrorMessage(error.message);
}
widget.unmount();
});
// Mount the Widget to a DOM element
widget.mountIframe(document.getElementById('tr-container'));
```