# ACH, Fednow/RTP & Wire bank deposit via the Payment Widget Source: https://developer.uphold.com/developer-guides/bank-transfers/deposit/via-payment-widget/ach Accept ACH, Fednow/RTP & Wire bank deposits with the Uphold Payment Widget: create a session, display transfer instructions to the user, and monitor for incoming funds. The Payment Widget handles deposit method selection across all supported US networks — ACH, Wire, FedNow, and RTP — and displays the necessary transfer instructions to the user. Your backend only needs to create the session and monitor for the incoming transfer. No per-network handling is required. The Payment Widget does not create any transaction. Monitoring and processing the incoming transfer must be handled by your backend via webhooks or polling. ## Prerequisites * The user has [completed onboarding](/developer-guides/user-onboarding/overview) and has the required capabilities enabled. ## Walkthrough ```mermaid theme={null} sequenceDiagram autonumber participant Usr as User participant U as Your App participant B as Your Backend participant P as Payment Widget participant A as Uphold participant F as Bank Usr->>U: Request deposit instructions U->>B: Create widget session B->>A: Create widget session (select-for-deposit) A-->>B: { session } B-->>U: { session } U->>P: Initialize widget Usr->>P: Select deposit method P-->>Usr: Display bank transfer instructions Usr->>F: Initiate bank transfer F-->>A: Incoming transfer received A-->>B: webhook: transaction.created (processing) F-->>A: Settlement confirmed A-->>B: webhook: transaction.status-changed (completed/failed) B-->>Usr: Notify the user ``` *** ## Select deposit method The widget lets the user select a deposit method and view transfer instructions that work across all supported US networks — ACH, Wire, FedNow, and RTP. ### Create a widget session Call the [Create widget session](/rest-apis/widgets-api/payment/create-session) endpoint to create a session for the `select-for-deposit` flow. ```http theme={null} POST /widgets/payment/sessions { "flow": "select-for-deposit" } ``` A successful response returns a `session` object with a `token` and `url` to initialize the widget. ```json theme={null} { "session": { "flow": "select-for-deposit", "url": "https://payment.enterprise.uphold.com/", "token": "GEbRxBN...edjnXbL" } } ``` ### Set up the widget Initialize the widget for the `select-for-deposit` flow using the session data returned from the API. ```javascript theme={null} import { PaymentWidget } from '@uphold/enterprise-payment-widget-web-sdk'; const initializeDepositWidget = async (session) => { // Initialize the widget const widget = new PaymentWidget<'select-for-deposit'>(session, { debug: true }); // Set up event handlers widget.on('ready', () => { console.log('Ready'); }); widget.on('complete', (event) => { console.log('Complete', JSON.stringify(event.detail.value)); }); widget.on('cancel', () => { console.log('Cancelled'); widget.unmount(); }); widget.on('error', (event) => { console.error('Error', event.detail.error); widget.unmount(); }); // Mount the widget widget.mountIframe(document.getElementById('payment-container')); }; ``` The example above is for web applications. For native apps using a WebView, you'll need a bridge for events as outlined in [Installation & setup](/widgets/payment/installation-and-setup#native-application-integration). ### Handle the complete event The `complete` event fires when the user selects a deposit method and completes the flow. ```javascript theme={null} widget.on('complete', (event) => { const { via, selection } = event.detail.value; if (via === 'deposit-method') { const { depositMethod, account } = selection; handleBankDeposit(depositMethod, account); } widget.unmount(); }); ``` The event payload contains two primary properties: * `via` — is set to `deposit-method` when the user selects a deposit method and completes the flow. * `selection` — contains the `account` that will receive the funds and the `depositMethod` configuration with transfer instructions. The widget presents these details directly to the user, so you don't need to display them separately. Across supported US rails, the transfer instruction fields such as routing and account details are shared, while `depositMethod.details.network` indicates the rail selected for this deposit method. No per-network handling is required. In the API, `"fednow"` represents both the FedNow and RTP networks. The optional `secondaryNetworks` field lists additional supported rails for the same account and transfer instructions, and it's only present when more than one network is supported. ```json theme={null} { "type": "bank", "status": "ok", "details": { "network": "ach", "asset": "USD", "routingNumber": "021000021", "accountNumber": "123456789", "beneficiary": "John Doe", "bankName": "Example Bank", "bankAddress": { "line1": "456 Bank Avenue", "line2": "Fort Lee, NJ 07024" }, "secondaryNetworks": [ "fednow", "wire" ] } } ``` ### Handle cancellations The `cancel` event fires when the user closes the widget without selecting a deposit method. ```javascript theme={null} widget.on('cancel', () => { widget.unmount(); // Redirect back or show a cancellation message }); ``` ### Handle errors The `error` event fires when an error occurs during the deposit method selection process. ```javascript theme={null} widget.on('error', (event) => { console.error('Widget error:', event.detail.error); widget.unmount(); // Show a user-friendly error message }); ``` The Payment Widget handles most errors internally. For unrecoverable errors, the widget fires an `error` event. It is the host application's responsibility to handle these events, present an error message to the user, and unmount the widget. ## Monitor for the incoming transfer The widget presents the deposit instructions to the user but does not monitor for the incoming transfer. Your application must do this via webhooks or polling. * Webhook events (recommended): * [core.transaction.created](/rest-apis/core-api/transactions/webhooks/transaction-created) * `status: processing` → bank transfer received, pending posting * [core.transaction.status-changed](/rest-apis/core-api/transactions/webhooks/transaction-status-changed) * `status: completed` → funds settled * `status: failed` → irrecoverable error * Polling (fallback): [Get transaction](/rest-apis/core-api/transactions/get-transaction) ## Sample transaction The sample below shows an ACH deposit. Transactions for other supported networks have the same structure — only `origin.node.network` differs: `"ach"`, `"fednow"` (FedNow and RTP), or `"wire"`. In a successful ACH bank deposit, the origin is represented as a `bank-address` node. The destination is the user's default USD account. ```json [expandable] theme={null} { "transaction": { "id": "a2b3c4d5-e6f7-8a9b-c0d1-e2f3a4b5c6d7", "origin": { "asset": "USD", "amount": "500.00", "node": { "type": "bank-address", "network": "ach" } }, "destination": { "asset": "USD", "amount": "500.00", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "status": "completed", "quotedAt": "2025-06-15T14:00:00Z", "createdAt": "2025-06-15T14:10:00Z", "updatedAt": "2025-06-15T14:30:00Z", "denomination": { "asset": "USD", "amount": "500.00", "target": "origin" } } } ``` ## Notify the user Display an in-app confirmation when the transaction is `completed`, and send an email if applicable. You now support ACH, Wire, RTP, and FedNow push bank deposits via the Payment Widget. # FPS bank deposit via the Payment Widget Source: https://developer.uphold.com/developer-guides/bank-transfers/deposit/via-payment-widget/fps Accept GBP Faster Payments (FPS) deposits with the Uphold Payment Widget. Create a session, display bank instructions, and monitor for incoming funds. The Payment Widget handles FPS deposit method selection and displays the necessary transfer instructions to the user. Your backend only needs to create the session and monitor for the incoming transfer. The Payment Widget does not create any transaction. Monitoring and processing the incoming transfer must be handled by your backend via webhooks or polling. ## Prerequisites * The user has [completed onboarding](/developer-guides/user-onboarding/overview) and has the required capabilities enabled. ## Walkthrough ```mermaid theme={null} sequenceDiagram autonumber participant Usr as User participant U as Your App participant B as Your Backend participant P as Payment Widget participant A as Uphold participant F as Bank Usr->>U: Request deposit instructions U->>B: Create widget session B->>A: Create widget session (select-for-deposit) A-->>B: { session } B-->>U: { session } U->>P: Initialize widget Usr->>P: Select deposit method P-->>Usr: Display bank transfer instructions Usr->>F: Initiate bank transfer F-->>A: Incoming transfer received A->>A: Link origin as external account A-->>B: webhook: transaction.created (processing) F-->>A: Settlement confirmed A-->>B: webhook: transaction.status-changed (completed/failed) B-->>Usr: Notify the user ``` *** ## Select deposit method The widget lets the user select an FPS deposit method and view the transfer instructions. ### Create a widget session Call the [Create widget session](/rest-apis/widgets-api/payment/create-session) endpoint to create a session for the `select-for-deposit` flow. ```http theme={null} POST /widgets/payment/sessions { "flow": "select-for-deposit" } ``` A successful response returns a `session` object with a `token` and `url` to initialize the widget. ```json theme={null} { "session": { "flow": "select-for-deposit", "url": "https://payment.enterprise.uphold.com/", "token": "GEbRxBN...edjnXbL" } } ``` ### Set up the widget Initialize the widget for the `select-for-deposit` flow using the session data returned from the API. ```javascript theme={null} import { PaymentWidget } from '@uphold/enterprise-payment-widget-web-sdk'; const initializeDepositWidget = async (session) => { // Initialize the widget const widget = new PaymentWidget<'select-for-deposit'>(session, { debug: true }); // Set up event handlers widget.on('ready', () => { console.log('Ready'); }); widget.on('complete', (event) => { console.log('Complete', JSON.stringify(event.detail.value)); }); widget.on('cancel', () => { console.log('Cancelled'); widget.unmount(); }); widget.on('error', (event) => { console.error('Error', event.detail.error); widget.unmount(); }); // Mount the widget widget.mountIframe(document.getElementById('payment-container')); }; ``` The example above is for web applications. For native apps using a WebView, you'll need a bridge for events as outlined in [Installation & setup](/widgets/payment/installation-and-setup#native-application-integration). ### Handle the complete event The `complete` event fires when the user selects a deposit method and completes the flow. ```javascript theme={null} widget.on('complete', (event) => { const { via, selection } = event.detail.value; if (via === 'deposit-method') { const { depositMethod, account } = selection; handleBankDeposit(depositMethod, account); } widget.unmount(); }); ``` The event payload contains two primary properties: * `via` — is set to `deposit-method` when the user selects a deposit method and completes the flow. * `selection` — contains the `account` that will receive the funds and the `depositMethod` configuration with transfer instructions. The widget presents these details directly to the user, so you don't need to display them separately. The FPS `depositMethod.details` contains: ```json theme={null} { "type": "bank", "status": "ok", "details": { "network": "fps", "asset": "GBP", "sortCode": "123456", "accountNumber": "12345678", "reference": "UH12345678", "beneficiary": "John Doe", "bankName": "Example Bank", "bankAddress": { "line1": "123 Bank Street", "line2": "London, United Kingdom" } } } ``` ### Handle cancellations The `cancel` event fires when the user closes the widget without selecting a deposit method. ```javascript theme={null} widget.on('cancel', () => { widget.unmount(); // Redirect back or show a cancellation message }); ``` ### Handle errors The `error` event fires when an error occurs during the deposit method selection process. ```javascript theme={null} widget.on('error', (event) => { console.error('Widget error:', event.detail.error); widget.unmount(); // Show a user-friendly error message }); ``` The Payment Widget handles most errors internally. For unrecoverable errors, the widget fires an `error` event. It is the host application's responsibility to handle these events, present an error message to the user, and unmount the widget. ## Monitor for the incoming transfer The widget presents the deposit instructions to the user but does not monitor for the incoming transfer. Your application must do this via webhooks or polling. * Webhook events (recommended): * [core.transaction.created](/rest-apis/core-api/transactions/webhooks/transaction-created) * `status: processing` → bank transfer received, pending posting * [core.transaction.status-changed](/rest-apis/core-api/transactions/webhooks/transaction-status-changed) * `status: completed` → funds settled * `status: failed` → irrecoverable error * Polling (fallback): [Get transaction](/rest-apis/core-api/transactions/get-transaction) ## Sample transaction In a successful FPS deposit, the origin is represented as an `external-account` node. This is because the origin bank details are automatically registered as an external account. The destination is the account selected when the deposit instructions were generated, or the user's default GBP account if the transfer was sent without a reference, or the selected account doesn't exist anymore. ```json [expandable] theme={null} { "transaction": { "id": "b1bbbc0f-dae2-4e94-9e6d-4b9d5a1f3c1f", "origin": { "asset": "GBP", "amount": "250.00", "node": { "type": "external-account", "id": "aa6e6efa-8d73-497c-8278-0347f459bd68", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "destination": { "asset": "GBP", "amount": "250.00", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "status": "completed", "quotedAt": "2025-01-10T11:02:39Z", "createdAt": "2025-01-10T11:12:39Z", "updatedAt": "2025-01-10T11:13:08Z", "denomination": { "asset": "GBP", "amount": "250.00", "target": "origin" } } } ``` The new external account can be then retrieved with [Get external account](/rest-apis/core-api/external-accounts/get-external-account) to show the sender's bank details alongside the transaction, or to withdraw funds to the same account in the future. ```json [expandable] theme={null} { "externalAccount": { "id": "aa6e6efa-8d73-497c-8278-0347f459bd68", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "type": "bank", "status": "ok", "label": "GBP Bank Account", "asset": "GBP", "network": "fps", "features": [ "withdraw" ], "details": { "accountNumber": "12345678", "sortCode": "123456" }, "createdAt": "2025-01-10T11:12:39Z", "updatedAt": "2025-01-10T11:13:08Z" } } ``` ## Notify the user Display an in-app confirmation when the transaction is `completed`, and send an email if applicable. You now support FPS bank transfer deposits via the Payment Widget. # ACH bank deposit via the REST API Source: https://developer.uphold.com/developer-guides/bank-transfers/deposit/via-rest-api/ach Support ACH bank deposits in USD using the Uphold REST API: configure the deposit method, share routing details, and monitor for incoming funds. This guide walks you through the steps to support ACH bank deposits using the REST API — from generating deposit instructions to monitoring for the incoming transfer. ## Prerequisites * The user has [completed onboarding](/developer-guides/user-onboarding/overview) and has the required capabilities enabled. ## Walkthrough ```mermaid theme={null} sequenceDiagram autonumber participant Usr as User participant U as Your App participant B as Your Backend participant A as Uphold participant F as Bank Usr->>U: Request deposit instructions U->>B: Set up deposit method B->>A: Set up account deposit method A-->>B: { depositMethod } B-->>U: { depositMethod } U-->>Usr: Display bank instructions Usr->>F: Initiate bank transfer F-->>A: Incoming transfer received A-->>B: webhook: transaction.created (processing) F-->>A: Settlement confirmed A-->>B: webhook: transaction.status-changed (completed/failed) B-->>Usr: Notify the user ``` *** ## Check available rails Call [List Rails](/rest-apis/core-api/assets/list-rails) to verify ACH supports the `deposit` feature before proceeding. ```http theme={null} GET /core/rails?type=bank&network=ach&asset=USD ``` A successful response includes the rail details. The `constraints` field indicates that US rail deposits can only be credited to the user's default USD account, regardless of the network used. ```json theme={null} { "rails": [ { "type": "bank", "network": "ach", "method": "bank-transfer", "asset": "USD", "decimals": 2, "features": [ "deposit", "withdraw" ], "constraints": [ { "rule": "allowed-deposit-accounts", "allowed": "default-only" } ] } ] } ``` ## Select destination account USD bank deposits can only be credited to the user's default USD account. ### Find the default account Call [List default accounts](/rest-apis/core-api/accounts/list-default-accounts) to retrieve it. ```http theme={null} GET /core/accounts/defaults?asset=USD ``` ```json theme={null} { "accounts": [ { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "ownerId": "1e32fca3-23f7-40ed-bc1b-de10c790182d", "label": "My USD account", "asset": "USD", "balance": { "total": "100.00", "available": "100.00" } } ] } ``` ### Create a new account If the user has no default USD account, create one with [Create account](/rest-apis/core-api/accounts/create-account) before proceeding. ```http theme={null} POST /core/accounts { "label": "My USD account", "asset": "USD" } ``` ```json theme={null} { "account": { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "ownerId": "1e32fca3-23f7-40ed-bc1b-de10c790182d", "label": "My USD account", "asset": "USD", "balance": { "total": "0", "available": "0" } } } ``` ## Generate deposit method For US bank rails, the same account and routing number are shared across all networks (ACH, FedNow / RTP, and Wire), so the same deposit method is generated and can be used for any of these networks. Call [Set up account deposit method](/rest-apis/core-api/accounts/set-up-account-deposit-method) with the target account id and the desired network. For subsequent calls, use [Get account deposit method](/rest-apis/core-api/accounts/get-account-deposit-method) instead. ```http theme={null} PUT /core/accounts/{accountId}/deposit-method?type=bank&network=ach&asset=USD ``` Use `network=fednow` or `network=wire` to generate instructions branded for those networks instead. A successful response includes the bank details for the user to initiate the transfer. ```json theme={null} { "depositMethod": { "type": "bank", "status": "ok", "details": { "network": "ach", "asset": "USD", "routingNumber": "021000021", "accountNumber": "123456789", "accountType": "checking", "beneficiary": "John Doe", "bankName": "Cross River Bank", "bankAddress": { "line1": "2 Riverview Dr", "line2": "Fort Lee, NJ 07024" }, "secondaryNetworks": [ "fednow", "wire" ] } } } ``` The deposit method may initially return `status: processing` while the details are being prepared. Call [Get account deposit method](/rest-apis/core-api/accounts/get-account-deposit-method) to make sure the deposit method is ready (`status: ok`) before displaying instructions to the user. Provide the **routing number**, **account number** and **account type**. Include the **beneficiary name**, **bank name**, and **bank address** to help the user confirm the legitimacy of the instructions. No reference is provided because all deposits are credited to the default USD account. ## Monitor for the incoming transfer Prefer **webhooks** for real-time updates, or fall back to **polling** if webhooks are not feasible. * Webhook events (recommended): * [core.transaction.created](/rest-apis/core-api/transactions/webhooks/transaction-created) * `status: processing` → bank transfer received, pending posting * [core.transaction.status-changed](/rest-apis/core-api/transactions/webhooks/transaction-status-changed) * `status: completed` → funds settled * `status: failed` → irrecoverable error * Polling (fallback): [Get transaction](/rest-apis/core-api/transactions/get-transaction) ## Sample transaction In a successful ACH bank deposit, the origin is represented as a `bank-address` node. The destination is the user's default USD account. ```json [expandable] theme={null} { "transaction": { "id": "a2b3c4d5-e6f7-8a9b-c0d1-e2f3a4b5c6d7", "origin": { "asset": "USD", "amount": "500.00", "node": { "type": "bank-address", "network": "ach" } }, "destination": { "asset": "USD", "amount": "500.00", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "status": "completed", "quotedAt": "2025-06-15T14:00:00Z", "createdAt": "2025-06-15T14:10:00Z", "updatedAt": "2025-06-15T14:30:00Z", "denomination": { "asset": "USD", "amount": "500.00", "target": "origin" } } } ``` ## Notify the user Display an in-app confirmation when the transaction is `completed`, and send an email if applicable. You now support ACH bank transfer deposits via the REST API. # FedNow / RTP bank deposit via the REST API Source: https://developer.uphold.com/developer-guides/bank-transfers/deposit/via-rest-api/fednow Send near-instant USD withdrawals via FedNow and RTP using the Uphold REST API: create an external bank account, generate a quote, submit the payout, and monitor settlement. This guide walks you through the steps to support FedNow / RTP bank deposits using the REST API. FedNow / RTP uses the same bank account and routing number as ACH — the deposit method setup is identical. ## Prerequisites * The user has [completed onboarding](/developer-guides/user-onboarding/overview) and has the required capabilities enabled. ## Walkthrough ```mermaid theme={null} sequenceDiagram autonumber participant Usr as User participant U as Your App participant B as Your Backend participant A as Uphold participant F as Bank Usr->>U: Request deposit instructions U->>B: Set up deposit method B->>A: Set up account deposit method A-->>B: { depositMethod } B-->>U: { depositMethod } U-->>Usr: Display bank instructions Usr->>F: Initiate FedNow / RTP transfer F-->>A: Incoming transfer received A-->>B: Transaction created (processing) F-->>A: Settlement confirmed A-->>B: Transaction completed/failed B-->>Usr: Notify the user ``` *** ## Check available rails Call [List Rails](/rest-apis/core-api/assets/list-rails) to verify FedNow supports the `deposit` feature before proceeding. ```http theme={null} GET /core/rails?type=bank&network=fednow&asset=USD ``` A successful response includes the rail details. The `constraints` field indicates deposits are credited to the user's default USD account. ```json theme={null} { "rails": [ { "type": "bank", "network": "fednow", "method": "bank-transfer", "asset": "USD", "decimals": 2, "features": [ "deposit" ], "constraints": [ { "rule": "allowed-deposit-accounts", "allowed": "default-only" } ] } ] } ``` ## Select destination account USD bank deposits can only be credited to the user's default USD account. ### Find the default account Call [List default accounts](/rest-apis/core-api/accounts/list-default-accounts) to retrieve it. ```http theme={null} GET /core/accounts/defaults?asset=USD ``` ```json theme={null} { "accounts": [ { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "ownerId": "1e32fca3-23f7-40ed-bc1b-de10c790182d", "label": "My USD account", "asset": "USD", "balance": { "total": "100.00", "available": "100.00" } } ] } ``` ### Create a new account If the user has no default USD account, create one with [Create account](/rest-apis/core-api/accounts/create-account) before proceeding. ```http theme={null} POST /core/accounts { "label": "My USD account", "asset": "USD" } ``` ```json theme={null} { "account": { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "ownerId": "1e32fca3-23f7-40ed-bc1b-de10c790182d", "label": "My USD account", "asset": "USD", "balance": { "total": "0", "available": "0" } } } ``` ## Generate deposit method For US bank rails, the same account and routing number are shared across all networks (ACH, FedNow / RTP, and Wire), so the same deposit method is generated and can be used for any of these networks. Call [Set up account deposit method](/rest-apis/core-api/accounts/set-up-account-deposit-method) with the target account id and the desired network. For subsequent calls, use [Get account deposit method](/rest-apis/core-api/accounts/get-account-deposit-method) instead. ```http theme={null} PUT /core/accounts/{accountId}/deposit-method?type=bank&network=fednow&asset=USD ``` A successful response includes the bank details. These are the same routing and account numbers as ACH — the sender uses them to initiate a FedNow / RTP push. ```json theme={null} { "depositMethod": { "type": "bank", "status": "ok", "details": { "network": "fednow", "asset": "USD", "routingNumber": "021000021", "accountNumber": "123456789", "accountType": "checking", "beneficiary": "John Doe", "bankName": "Cross River Bank", "bankAddress": { "line1": "2 Riverview Dr", "line2": "Fort Lee, NJ 07024" } } } } ``` The deposit method may initially return `status: processing` while the details are being prepared. Call [Get account deposit method](/rest-apis/core-api/accounts/get-account-deposit-method) to confirm it is ready (`status: ok`) before displaying instructions to the user. Provide the **routing number**, **account number** and **account type**. Include the **beneficiary name**, **bank name**, and **bank address** to help the user confirm the legitimacy of the instructions. ## Monitor for the incoming transfer Prefer **webhooks** for real-time updates, or fall back to **polling** if webhooks are not feasible. * Webhook events (recommended): * [core.transaction.created](/rest-apis/core-api/transactions/webhooks/transaction-created) * `status: processing` → bank transfer received, pending posting * [core.transaction.status-changed](/rest-apis/core-api/transactions/webhooks/transaction-status-changed) * `status: completed` → funds settled * `status: failed` → irrecoverable error * Polling (fallback): [Get transaction](/rest-apis/core-api/transactions/get-transaction) ## Sample transaction In a successful FedNow / RTP bank deposit, the origin is represented as a `bank-address` node. The destination is the user's default USD account. ```json [expandable] theme={null} { "transaction": { "id": "a2b3c4d5-e6f7-8a9b-c0d1-e2f3a4b5c6d7", "origin": { "asset": "USD", "amount": "500.00", "node": { "type": "bank-address", "network": "fednow" } }, "destination": { "asset": "USD", "amount": "500.00", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "status": "completed", "quotedAt": "2025-06-15T14:00:00Z", "createdAt": "2025-06-15T14:10:00Z", "updatedAt": "2025-06-15T14:30:00Z", "denomination": { "asset": "USD", "amount": "500.00", "target": "origin" } } } ``` ## Notify the user Display an in-app confirmation when the transaction is `completed`, and send an email if applicable. You now support FedNow / RTP bank deposits via the REST API. # FPS bank deposit via the REST API Source: https://developer.uphold.com/developer-guides/bank-transfers/deposit/via-rest-api/fps Generate GBP Faster Payments (FPS) deposit instructions and monitor incoming transfers using the Uphold REST API, with automatic origin account linking after first deposit. This guide walks you through the steps to support FPS bank deposits using the REST API — from generating deposit instructions to monitoring for the incoming transfer. ## Prerequisites * The user has [completed onboarding](/developer-guides/user-onboarding/overview) and has the required capabilities enabled. ## Walkthrough ```mermaid theme={null} sequenceDiagram autonumber participant Usr as User participant U as Your App participant B as Your Backend participant A as Uphold participant F as Bank Usr->>U: Choose account to fund U->>B: Set up deposit method B->>A: Set up account deposit method A-->>B: { depositMethod } B-->>U: { depositMethod } U-->>Usr: Display bank instructions Usr->>F: Initiate bank transfer F-->>A: Incoming transfer received A->>A: Link origin as external account A-->>B: webhook: transaction.created (processing) F-->>A: Settlement confirmed A-->>B: webhook: transaction.status-changed (completed/failed) B-->>Usr: Notify the user ``` *** ## Check available rails Call [List Rails](/rest-apis/core-api/assets/list-rails) to verify FPS supports the `deposit` feature before proceeding. ```http theme={null} GET /core/rails?type=bank&network=fps&asset=GBP ``` A successful response includes the rail details and its features. ```json theme={null} { "rails": [ { "type": "bank", "network": "fps", "method": "bank-transfer", "asset": "GBP", "decimals": 2, "features": [ "deposit", "withdraw" ] } ] } ``` ## Select destination account FPS push bank deposits can target any account. If the selected account is not in GBP, the deposited amount will be converted to the destination account's currency at the time of settlement. Make sure the destination asset has the necessary [features enabled](/rest-apis/core-api/assets/introduction#features-and-deposits-/-withdrawals). ### Find an existing account Call [List accounts](/rest-apis/core-api/accounts/list-accounts) to retrieve the user's accounts and let them pick the one they want to fund. ```http theme={null} GET /core/accounts ``` ```json theme={null} { "accounts": [ { "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "label": "My GBP account", "asset": "GBP", "balance": { "total": "500.00", "available": "500.00" } } ] } ``` ### Create a new account If the user has no accounts, create one with [Create account](/rest-apis/core-api/accounts/create-account) before proceeding. ```http theme={null} POST /core/accounts { "label": "My GBP account", "asset": "GBP" } ``` ```json theme={null} { "account": { "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "label": "My GBP account", "asset": "GBP", "balance": { "total": "0", "available": "0" } } } ``` ## Terms and disclosures Before generating deposit instructions, you must collect the user's acceptance of the applicable terms of service and display the required regulatory disclaimer. ### Accept terms of service The user must accept the `unique-account-number-viban` terms of service before FPS deposit instructions can be generated. Display the following copy to the user and prompt them to continue: > By selecting continue you agree to the LHV's [Principles of Processing Customer Data](https://www.lhv.ee/en/principles-of-processing-customer-data) and the [Personal Deposit Accounts Terms and Conditions](https://uphold.com/en-gb/legal/unique-account-number-terms-of-use). Once the user confirms, call [Accept terms of service](/rest-apis/core-api/terms-of-service/accept-terms-of-service) to register their acceptance. ```http theme={null} POST /core/terms-of-service/unique-account-number-viban/accept ``` ### EMD disclaimer This disclaimer is required to comply with FCA regulations. Display it to users based in GB, the first time they link a credit/debit card or generate bank deposit details — it only needs to be shown once: > Uphold Europe Limited is an EMD Agent of Optimus Cards UK Limited (FRN: 902034). All received funds are held in a designated safekeeping account with a regulated bank and kept separate from Uphold's own assets. These funds are not protected by the UK Financial Services Compensation Scheme. ## Generate deposit method Call [Set up account deposit method](/rest-apis/core-api/accounts/set-up-account-deposit-method) with the target account id and the desired network. For subsequent calls, use [Get account deposit method](/rest-apis/core-api/accounts/get-account-deposit-method) instead. ```http theme={null} PUT /core/accounts/{accountId}/deposit-method?type=bank&network=fps&asset=GBP ``` A successful response includes the necessary bank details and reference for the user to initiate the transfer. ```json theme={null} { "depositMethod": { "type": "bank", "status": "ok", "details": { "network": "fps", "asset": "GBP", "sortCode": "123456", "accountNumber": "12345678", "reference": "UH12345678", "beneficiary": "John Doe", "bankName": "Example Bank", "bankAddress": { "line1": "123 Bank Street", "line2": "London, United Kingdom" } } } } ``` The deposit method may initially return `status: processing` while the details are being prepared. Call [Get account deposit method](/rest-apis/core-api/accounts/get-account-deposit-method) to make sure the deposit method is ready (`status: ok`) before displaying instructions to the user. Provide the **sort code**, **account number**, and **reference** to the user. Include the **beneficiary name**, **bank name**, and **bank address** to help the user confirm the legitimacy of the instructions. Deposits sent without the reference are still credited, but funds will be deposited into the user's default GBP account. ## Monitor for the incoming transfer Prefer **webhooks** for real-time updates, or fall back to **polling** if webhooks are not feasible. * Webhook events (recommended): * [core.transaction.created](/rest-apis/core-api/transactions/webhooks/transaction-created) * `status: processing` → bank transfer received, pending posting * [core.transaction.status-changed](/rest-apis/core-api/transactions/webhooks/transaction-status-changed) * `status: completed` → funds settled * `status: failed` → irrecoverable error * Polling (fallback): [Get transaction](/rest-apis/core-api/transactions/get-transaction) ## Sample transaction In a successful FPS deposit, the origin is represented as an `external-account` node. This is because the origin bank details are automatically registered as an external account. The destination is the account selected when the deposit instructions were generated, or the user's default GBP account if the transfer was sent without a reference, or the selected account doesn't exist anymore. ```json [expandable] theme={null} { "transaction": { "id": "b1bbbc0f-dae2-4e94-9e6d-4b9d5a1f3c1f", "origin": { "asset": "GBP", "amount": "250.00", "node": { "type": "external-account", "id": "aa6e6efa-8d73-497c-8278-0347f459bd68", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "destination": { "asset": "GBP", "amount": "250.00", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "status": "completed", "quotedAt": "2025-01-10T11:02:39Z", "createdAt": "2025-01-10T11:12:39Z", "updatedAt": "2025-01-10T11:13:08Z", "denomination": { "asset": "GBP", "amount": "250.00", "target": "origin" } } } ``` The new external account can be then retrieved with [Get external account](/rest-apis/core-api/external-accounts/get-external-account) to show the sender's bank details alongside the transaction, or to withdraw funds to the same account in the future. ```json [expandable] theme={null} { "externalAccount": { "id": "aa6e6efa-8d73-497c-8278-0347f459bd68", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "type": "bank", "status": "ok", "label": "GBP Bank Account", "asset": "GBP", "network": "fps", "features": [ "withdraw" ], "details": { "accountNumber": "12345678", "sortCode": "123456" }, "createdAt": "2025-01-10T11:12:39Z", "updatedAt": "2025-01-10T11:13:08Z" } } ``` ## Notify the user Display an in-app confirmation when the transaction is `completed`, and send an email if applicable. You now support FPS bank transfer deposits via the REST API. # SEPA bank deposit via the REST API Source: https://developer.uphold.com/developer-guides/bank-transfers/deposit/via-rest-api/sepa Generate EUR SEPA deposit instructions and monitor incoming transfers using the Uphold REST API, with automatic origin account linking after first deposit. This guide walks you through the steps to support SEPA bank deposits using the REST API — from generating deposit instructions to monitoring for the incoming transfer. ## Prerequisites * The user has [completed onboarding](/developer-guides/user-onboarding/overview) and has the required capabilities enabled. ## Walkthrough ```mermaid theme={null} sequenceDiagram autonumber participant Usr as User participant U as Your App participant B as Your Backend participant A as Uphold participant F as Bank Usr->>U: Choose account to fund U->>B: Set up deposit method B->>A: Set up account deposit method A-->>B: { depositMethod } B-->>U: { depositMethod } U-->>Usr: Display bank instructions Usr->>F: Initiate bank transfer F-->>A: Incoming transfer received A->>A: Link origin as external account A-->>B: webhook: transaction.created (processing) F-->>A: Settlement confirmed A-->>B: webhook: transaction.status-changed (completed/failed) B-->>Usr: Notify the user ``` *** ## Check available rails Call [List Rails](/rest-apis/core-api/assets/list-rails) to verify SEPA supports the `deposit` feature before proceeding. ```http theme={null} GET /core/rails?type=bank&network=sepa&asset=EUR ``` A successful response includes the rail details and its features. ```json theme={null} { "rails": [ { "type": "bank", "network": "sepa", "method": "bank-transfer", "asset": "EUR", "decimals": 2, "features": [ "deposit", "withdraw" ] } ] } ``` ## Select destination account SEPA deposits can target any account. If the selected account is not in EUR, the deposited amount will be converted to the destination account's currency at the time of settlement. Make sure the destination asset has the necessary [features enabled](/rest-apis/core-api/assets/introduction#features-and-deposits-/-withdrawals). ### Find an existing account Call [List accounts](/rest-apis/core-api/accounts/list-accounts) to retrieve the user's accounts and let them pick the one they want to fund. ```http theme={null} GET /core/accounts ``` ```json theme={null} { "accounts": [ { "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "label": "My EUR account", "asset": "EUR", "balance": { "total": "500.00", "available": "500.00" } } ] } ``` ### Create a new account If the user has no accounts, create one with [Create account](/rest-apis/core-api/accounts/create-account) before proceeding. ```http theme={null} POST /core/accounts { "label": "My EUR account", "asset": "EUR" } ``` ```json theme={null} { "account": { "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "label": "My EUR account", "asset": "EUR", "balance": { "total": "0", "available": "0" } } } ``` ## Terms and disclosures Before generating deposit instructions, you must collect the user's acceptance of the applicable terms of service and display the required regulatory disclaimer. ### Accept terms of service The user must accept the `unique-account-number-viban` terms of service before SEPA deposit instructions can be generated. Display the following copy to the user and prompt them to continue: > By selecting continue you agree to the LHV's [Principles of Processing Customer Data](https://www.lhv.ee/en/principles-of-processing-customer-data) and the [Personal Deposit Accounts Terms and Conditions](https://uphold.com/en-gb/legal/unique-account-number-terms-of-use). Once the user confirms, call [Accept terms of service](/rest-apis/core-api/terms-of-service/accept-terms-of-service) to register their acceptance. ```http theme={null} POST /core/terms-of-service/unique-account-number-viban/accept ``` ### EMD disclaimer This disclaimer is required to comply with FCA regulations. Display it to users based in GB, the first time they link a credit/debit card or generate bank deposit details — it only needs to be shown once: > Uphold Europe Limited is an EMD Agent of Optimus Cards UK Limited (FRN: 902034). All received funds are held in a designated safekeeping account with a regulated bank and kept separate from Uphold's own assets. These funds are not protected by the UK Financial Services Compensation Scheme. ## Generate deposit method Call [Set up account deposit method](/rest-apis/core-api/accounts/set-up-account-deposit-method) with the target account id and the desired network. For subsequent calls, use [Get account deposit method](/rest-apis/core-api/accounts/get-account-deposit-method) instead. ```http theme={null} PUT /core/accounts/{accountId}/deposit-method?type=bank&network=sepa&asset=EUR ``` A successful response includes the necessary bank details and reference for the user to initiate the transfer. ```json theme={null} { "depositMethod": { "type": "bank", "status": "ok", "details": { "network": "sepa", "asset": "EUR", "beneficiary": "John Doe", "bankName": "Example Bank", "bankAddress": { "line1": "123 Bank Street", "line2": "Vienna, Austria" }, "bic": "EXAAAT2K", "iban": "AT487954841229809844", "reference": "UH12345678" } } } ``` The deposit method may initially return `status: processing` while the details are being prepared. Call [Get account deposit method](/rest-apis/core-api/accounts/get-account-deposit-method) to make sure the deposit method is ready (`status: ok`) before displaying instructions to the user. Provide the **IBAN**, **BIC**, and **reference** to the user. Include the **beneficiary name**, **bank name**, and **bank address** to help the user confirm the legitimacy of the instructions. Deposits sent without the reference are still credited, but funds will be deposited into the user's default EUR account. ## Monitor for the incoming transfer Prefer **webhooks** for real-time updates, or fall back to **polling** if webhooks are not feasible. * Webhook events (recommended): * [core.transaction.created](/rest-apis/core-api/transactions/webhooks/transaction-created) * `status: processing` → bank transfer received, pending posting * [core.transaction.status-changed](/rest-apis/core-api/transactions/webhooks/transaction-status-changed) * `status: completed` → funds settled * `status: failed` → irrecoverable error * Polling (fallback): [Get transaction](/rest-apis/core-api/transactions/get-transaction) ## Sample transaction In a successful SEPA deposit, the origin is represented as an `external-account` node. This is because the origin bank details are automatically registered as an external account. The destination is the account selected when the deposit instructions were generated, or the user's default EUR account if the transfer was sent without a reference, or the selected account doesn't exist anymore. ```json [expandable] theme={null} { "transaction": { "id": "b1bbbc0f-dae2-4e94-9e6d-4b9d5a1f3c1f", "origin": { "asset": "EUR", "amount": "250.00", "node": { "type": "external-account", "id": "cc8e8abc-0f95-4b9f-8490-2569a67bd80e", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "destination": { "asset": "EUR", "amount": "250.00", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "status": "completed", "quotedAt": "2025-01-10T11:02:39Z", "createdAt": "2025-01-10T11:12:39Z", "updatedAt": "2025-01-10T11:13:08Z", "denomination": { "asset": "EUR", "amount": "250.00", "target": "origin" } } } ``` The new external account can be then retrieved with [Get external account](/rest-apis/core-api/external-accounts/get-external-account) to show the sender's bank details alongside the transaction, or to withdraw funds to the same account in the future. ```json [expandable] theme={null} { "externalAccount": { "id": "cc8e8abc-0f95-4b9f-8490-2569a67bd80e", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "type": "bank", "status": "ok", "label": "EUR Bank Account", "asset": "EUR", "network": "sepa", "features": [ "withdraw" ], "details": { "iban": "AT487954841229809844", "bic": "EXAAAT2K" }, "createdAt": "2025-01-10T11:12:39Z", "updatedAt": "2025-01-10T11:13:08Z" } } ``` ## Notify the user Display an in-app confirmation when the transaction is `completed`, and send an email if applicable. You now support SEPA bank transfer deposits via the REST API. # Wire bank deposit via the REST API Source: https://developer.uphold.com/developer-guides/bank-transfers/deposit/via-rest-api/wire Support USD wire transfer deposits using the Uphold REST API. Wire shares routing and account details with ACH and FedNow for one deposit method setup. This guide walks you through the steps to support Wire bank deposits using the REST API. Wire uses the same bank account and routing number as ACH — the deposit method setup is identical. ## Prerequisites * The user has [completed onboarding](/developer-guides/user-onboarding/overview) and has the required capabilities enabled. ## Walkthrough ```mermaid theme={null} sequenceDiagram autonumber participant Usr as User participant U as Your App participant B as Your Backend participant A as Uphold participant F as Bank Usr->>U: Request deposit instructions U->>B: Set up deposit method B->>A: Set up account deposit method A-->>B: { depositMethod } B-->>U: { depositMethod } U-->>Usr: Display bank instructions Usr->>F: Initiate wire transfer F-->>A: Incoming transfer received A-->>B: webhook: transaction.created (processing) F-->>A: Settlement confirmed A-->>B: webhook: transaction.status-changed (completed/failed) B-->>Usr: Notify the user ``` *** ## Check available rails Call [List Rails](/rest-apis/core-api/assets/list-rails) to verify Wire supports the `deposit` feature before proceeding. ```http theme={null} GET /core/rails?type=bank&network=wire&asset=USD ``` A successful response includes the rail details. The `constraints` field indicates deposits are credited to the user's default USD account. ```json theme={null} { "rails": [ { "type": "bank", "network": "wire", "method": "bank-transfer", "asset": "USD", "decimals": 2, "features": [ "deposit" ], "constraints": [ { "rule": "allowed-deposit-accounts", "allowed": "default-only" } ] } ] } ``` ## Select destination account USD bank deposits can only be credited to the user's default USD account. ### Find the default account Call [List default accounts](/rest-apis/core-api/accounts/list-default-accounts) to retrieve it. ```http theme={null} GET /core/accounts/defaults?asset=USD ``` ```json theme={null} { "accounts": [ { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "ownerId": "1e32fca3-23f7-40ed-bc1b-de10c790182d", "label": "My USD account", "asset": "USD", "balance": { "total": "100.00", "available": "100.00" } } ] } ``` ### Create a new account If the user has no default USD account, create one with [Create account](/rest-apis/core-api/accounts/create-account) before proceeding. ```http theme={null} POST /core/accounts { "label": "My USD account", "asset": "USD" } ``` ```json theme={null} { "account": { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "ownerId": "1e32fca3-23f7-40ed-bc1b-de10c790182d", "label": "My USD account", "asset": "USD", "balance": { "total": "0", "available": "0" } } } ``` ## Generate deposit method For US bank rails, the same account and routing number are shared across all networks (ACH, FedNow / RTP, and Wire), so the same deposit method is generated and can be used for any of these networks. Call [Set up account deposit method](/rest-apis/core-api/accounts/set-up-account-deposit-method) with the target account id and the desired network. For subsequent calls, use [Get account deposit method](/rest-apis/core-api/accounts/get-account-deposit-method) instead. ```http theme={null} PUT /core/accounts/{accountId}/deposit-method?type=bank&network=wire&asset=USD ``` A successful response includes the bank details. These are the same routing and account numbers as ACH — the sender uses them to initiate a wire transfer. ```json theme={null} { "depositMethod": { "type": "bank", "status": "ok", "details": { "network": "wire", "asset": "USD", "routingNumber": "021000021", "accountNumber": "123456789", "accountType": "checking", "beneficiary": "John Doe", "bankName": "Cross River Bank", "bankAddress": { "line1": "2 Riverview Dr", "line2": "Fort Lee, NJ 07024" } } } } ``` The deposit method may initially return `status: processing` while the details are being prepared. Call [Get account deposit method](/rest-apis/core-api/accounts/get-account-deposit-method) to confirm it is ready (`status: ok`) before displaying instructions to the user. Provide the **routing number**, **account number** and **account type**. Include the **beneficiary name**, **bank name**, and **bank address** to help the user confirm the legitimacy of the instructions. ## Monitor for the incoming transfer Prefer **webhooks** for real-time updates, or fall back to **polling** if webhooks are not feasible. * Webhook events (recommended): * [core.transaction.created](/rest-apis/core-api/transactions/webhooks/transaction-created) * `status: processing` → bank transfer received, pending posting * [core.transaction.status-changed](/rest-apis/core-api/transactions/webhooks/transaction-status-changed) * `status: completed` → funds settled * `status: failed` → irrecoverable error * Polling (fallback): [Get transaction](/rest-apis/core-api/transactions/get-transaction) ## Sample transaction In a successful Wire bank deposit, the origin is represented as a `bank-address` node. The destination is the user's default USD account. ```json [expandable] theme={null} { "transaction": { "id": "a2b3c4d5-e6f7-8a9b-c0d1-e2f3a4b5c6d7", "origin": { "asset": "USD", "amount": "500.00", "node": { "type": "bank-address", "network": "wire" } }, "destination": { "asset": "USD", "amount": "500.00", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "status": "completed", "quotedAt": "2025-06-15T14:00:00Z", "createdAt": "2025-06-15T14:10:00Z", "updatedAt": "2025-06-15T14:30:00Z", "denomination": { "asset": "USD", "amount": "500.00", "target": "origin" } } } ``` ## Notify the user Display an in-app confirmation when the transaction is `completed`, and send an email if applicable. You now support Wire bank deposits via the REST API. # Bank transfer integration overview (FPS, SEPA, ACH, Wire) Source: https://developer.uphold.com/developer-guides/bank-transfers/overview Connect to local banking rails across the UK, EU, and US — FPS, SEPA, ACH, FedNow/RTP, and Wire — with deposit and withdrawal coverage and quote flows. Connect to local banking rails across the UK, EU, and US. ## Available networks Five banking rails are available across the UK, EU, and US. Settlement speed and features vary by network. All networks support **Individual** accounts; availability for **Business** accounts depends on the specific rail and your contract. ### Coverage | Network | Region | Currency | Settlement | | ---------------- | ------- | -------- | ------------------------------ | | **FPS** | UK / EU | GBP | Near-instant (24/7) | | **SEPA** | UK / EU | EUR | Near-instant or 1 business day | | **ACH** | US | USD | 1–3 business days | | **FedNow / RTP** | US | USD | Near-instant (24/7) | | **Wire** | US | USD | Same or next day | ### Supported features | Network | Deposit | Withdrawal | | ---------------- | ------------------------------- | ----------------------------------- | | **FPS** | Supported | Supported | | **SEPA** | Supported | Supported | | **ACH** | Supported | Supported | | **FedNow / RTP** | Supported | Supported | | **Wire** | Supported | Not supported | ## Key concepts * **Push deposits are external transactions** — only push deposits are supported. The sender initiates the transfer from their bank, and the platform creates the transaction automatically when funds are detected. No quote is required. * **Withdrawals are quote-based** — the user must confirm a quote before the transaction is created and the payout is submitted. * **Origin linking** — for [FPS](/developer-guides/bank-transfers/deposit/via-rest-api/fps) and [SEPA](/developer-guides/bank-transfers/deposit/via-rest-api/sepa), the origin bank account is automatically linked as an [external account](/rest-apis/core-api/external-accounts/introduction) after the first deposit. For [ACH](/developer-guides/bank-transfers/withdrawal/via-rest-api/ach), users must explicitly add one. ## Network specifics | Network | ToS required | Deposit target | Notes | | ---------------- | ----------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------ | | **FPS** | `unique-account-number-viban` | Any account | — | | **SEPA** | `unique-account-number-viban` | Any account | — | | **ACH** | None | Default USD account | Shares routing details with FedNow / RTP and Wire | | **FedNow / RTP** | None | Default USD account | Not all banks supported; shares routing details with ACH and Wire; withdrawal incurs an additional fee | | **Wire** | None | Default USD account | Shares routing details with ACH and FedNow / RTP | ## Testing in sandbox Use the [Simulate bank deposit](/rest-apis/core-api/accounts/test-helpers/simulate-bank-deposit) test helper to simulate an incoming deposit in sandbox for FPS and SEPA. Call it with the deposit method details and specify the network (`"fps"` or `"sepa"`), and the platform will create a completed transaction as if the user had sent the transfer from their bank. Sandbox deposit simulation for USD rails (ACH, FedNow / RTP, Wire) is not yet available. This feature is coming soon. ## Start building Set up deposit instructions and handle incoming funds programmatically. Let users deposit funds with a low-code, embeddable widget. Create quotes and submit payouts to your users' bank accounts. Let users withdraw funds with a low-code, embeddable widget. # ACH bank withdrawal via the Payment Widget Source: https://developer.uphold.com/developer-guides/bank-transfers/withdrawal/via-payment-widget/ach Send ACH bank withdrawals with the Uphold Payment Widget for account selection, then create the quote and transaction via the REST API to complete payout. The Payment Widget handles ACH bank account selection and creation. Your backend creates the session, then continues with the REST API to create a quote and transaction once the user has a destination bank account. The Payment Widget does not create any transaction. Your backend must create the quote and transaction via the REST API after the user selects their bank account. ## Prerequisites * The user has [completed onboarding](/developer-guides/user-onboarding/overview) and has the required capabilities enabled. ## Walkthrough ```mermaid theme={null} sequenceDiagram autonumber participant Usr as User participant U as Your App participant B as Your Backend participant P as Payment Widget participant A as Uphold participant F as Bank Usr->>U: Start bank withdrawal U->>B: List accounts B->>A: GET /core/accounts A-->>B: { accounts } B-->>U: { accounts } Usr->>U: Choose source account U->>B: Create widget session B->>A: Create widget session (select-for-withdrawal) A-->>B: { session } B-->>U: { session } U->>P: Initialize widget Usr->>P: Select bank account P-->>U: complete { via: "external-account", selection } U->>B: Request quote B->>A: Create quote A-->>B: { quote } B-->>U: { quote } Usr->>U: Confirm quote U->>B: Create transaction B->>A: Create transaction A-->>B: { transaction } A-->>B: webhook: transaction.created (processing) A->>F: Submit bank transfer F-->>A: Settlement confirmed A-->>B: webhook: transaction.status-changed (completed/failed) B-->>Usr: Notify the user ``` *** ## Select source account ACH withdrawals can be sourced from any account. If the selected account is not in USD, the balance will be converted at the time of the transaction using Uphold's prevailing rate. Make sure the origin asset has the necessary [features enabled](/rest-apis/core-api/assets/introduction#features-and-deposits-/-withdrawals). Call [List accounts](/rest-apis/core-api/accounts/list-accounts) to retrieve the user's accounts and let them pick the one to withdraw from. ```http theme={null} GET /core/accounts ``` ```json theme={null} { "accounts": [ { "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "label": "My USD account", "asset": "USD", "balance": { "total": "500.00", "available": "500.00" } } ] } ``` *** ## Select a bank account The widget lets the user select an existing ACH bank account or add a new one by providing their routing and account number. ### Create a widget session Call the [Create widget session](/rest-apis/widgets-api/payment/create-session) endpoint to create a session for the `select-for-withdrawal` flow. ```http theme={null} POST /widgets/payment/sessions { "flow": "select-for-withdrawal" } ``` A successful response returns a `session` object with a `token` and `url` to initialize the widget. ```json theme={null} { "session": { "flow": "select-for-withdrawal", "url": "https://payment.enterprise.uphold.com/", "token": "GEbRxBN...edjnXbL" } } ``` ### Set up the widget Initialize the widget for the `select-for-withdrawal` flow using the session data returned from the API. ```javascript theme={null} import { PaymentWidget } from '@uphold/enterprise-payment-widget-web-sdk'; const initializeWithdrawalWidget = async (session) => { // Initialize the widget const widget = new PaymentWidget<'select-for-withdrawal'>(session, { debug: true }); // Set up event handlers widget.on('ready', () => { console.log('Ready'); }); widget.on('complete', (event) => { console.log('Complete', JSON.stringify(event.detail.value)); }); widget.on('cancel', () => { console.log('Cancelled'); widget.unmount(); }); widget.on('error', (event) => { console.error('Error', event.detail.error); widget.unmount(); }); // Mount the widget widget.mountIframe(document.getElementById('payment-container')); }; ``` The example above is for web applications. For native apps using a WebView, you'll need a bridge for events as outlined in [Installation & setup](/widgets/payment/installation-and-setup#native-application-integration). ### Handle the complete event When the user completes the selection, the `complete` event fires with `via: "external-account"` and a `selection` containing the chosen external account. ```javascript theme={null} widget.on('complete', (event) => { const { via, selection } = event.detail.value; if (via === 'external-account' && selection.type === 'bank') { // selection is the external account the user selected or just added handleBankWithdrawal(selection); } widget.unmount(); }); ``` The event payload: * `via` — set to `external-account` when the user selects or adds a bank account. * `selection` — an [external account](/rest-apis/core-api/external-accounts/introduction) object with the selected bank details. * `selection.network` — the network to use for the withdrawal (`"ach"` or `"fednow"` if `secondaryNetworks` includes it). Pass this value in the quote request. ```json theme={null} { "via": "external-account", "selection": { "id": "bb7f7fab-9e84-5a8e-9389-1458f56ac79", "type": "bank", "network": "ach", "label": "My USD Checking Account", "secondaryNetworks": ["fednow"] } } ``` ### Handle cancellations The `cancel` event fires when the user closes the widget without selecting a bank account. ```javascript theme={null} widget.on('cancel', () => { widget.unmount(); // Redirect back or show a cancellation message }); ``` ### Handle errors The `error` event fires when an error occurs during the bank account selection process. ```javascript theme={null} widget.on('error', (event) => { console.error('Widget error:', event.detail.error); widget.unmount(); // Show a user-friendly error message }); ``` The Payment Widget handles most errors internally. For unrecoverable errors, the widget fires an `error` event. It is the host application's responsibility to handle these events, present an error message to the user, and unmount the widget. ## Create a quote Create a quote with [Create quote](/rest-apis/core-api/transactions/create-quote). Pass `network: "ach"` on the destination node to route the transfer via ACH, or leave it out to route via the external account's default network. If the external account has `secondaryNetworks`, the user can choose which one to use for the transfer. The quote must be created with the selected network to ensure accurate fees and expiration time. ```http theme={null} POST /core/transactions/quote { "origin": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8" }, "destination": { "type": "external-account", "id": "bb7f7fab-9e84-5a8e-9389-1458f56ac79", "network": "ach" }, "denomination": { "asset": "USD", "amount": "500.00", "target": "origin" } } ``` A successful response returns a `quote` object with fees and expiration. ```json [expandable] theme={null} { "quote": { "id": "734111d9-ace0-5b3c-bb4e-7b7b55b8a7b1", "origin": { "amount": "500.00", "asset": "USD", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" }, "rate": "1" }, "destination": { "amount": "500.00", "asset": "USD", "node": { "type": "external-account", "id": "bb7f7fab-9e84-5a8e-9389-1458f56ac79", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "network": "ach" }, "rate": "1" }, "denomination": { "asset": "USD", "amount": "500.00", "target": "origin", "rate": "1" }, "fees": [], "expiresAt": "2024-07-24T15:22:39Z" } } ``` Quotes typically **expire** quickly. Prompt for user confirmation within the expiry window and requote if needed. ## Confirm and create transaction After the user confirms the withdrawal, call [Create transaction](/rest-apis/core-api/transactions/create-transaction) with the quote ID to execute the transfer. ```http theme={null} POST /core/transactions { "quoteId": "734111d9-ace0-5b3c-bb4e-7b7b55b8a7b1" } ``` In a successful ACH withdrawal, the origin is the user's `account` and the destination is the `external-account` representing the user's bank. The transaction status is initially `processing` and updates to `completed` once the transfer settles. ```json [expandable] theme={null} { "transaction": { "id": "e4f5a6b7-2c3d-4e6f-a09b-8c7d6e5f4a3c", "origin": { "asset": "USD", "amount": "500.00", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "destination": { "asset": "USD", "amount": "500.00", "node": { "type": "external-account", "id": "bb7f7fab-9e84-5a8e-9389-1458f56ac79", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "network": "ach" } }, "status": "processing", "quotedAt": "2025-01-10T14:22:15Z", "createdAt": "2025-01-10T14:22:45Z", "updatedAt": "2025-01-10T14:22:45Z", "denomination": { "asset": "USD", "amount": "500.00", "target": "origin" } } } ``` ## Monitor for settlement The widget does not monitor for settlement. Your application must do this via webhooks or polling. * Webhook events (recommended): * [core.transaction.created](/rest-apis/core-api/transactions/webhooks/transaction-created) * `status: processing` → payout submitted to the bank network * [core.transaction.status-changed](/rest-apis/core-api/transactions/webhooks/transaction-status-changed) * `status: completed` → funds delivered to the user's bank * `status: failed` → irrecoverable error * Polling (fallback): [Get transaction](/rest-apis/core-api/transactions/get-transaction) ## Notify the user Display an in-app confirmation when the transaction is `completed`, and send an email if applicable. You now support ACH bank transfer withdrawals via the Payment Widget. # FPS bank withdrawal via the Payment Widget Source: https://developer.uphold.com/developer-guides/bank-transfers/withdrawal/via-payment-widget/fps Send GBP Faster Payments (FPS) withdrawals with the Uphold Payment Widget for bank account selection, then create the quote and transaction via REST API. The Payment Widget handles FPS bank account selection. Your backend creates the session, then continues with the REST API to create a quote and transaction once the user selects their destination bank account. The Payment Widget does not create any transaction. Your backend must create the quote and transaction via the REST API after the user selects their bank account. ## Prerequisites * The user has [completed onboarding](/developer-guides/user-onboarding/overview) and has the required capabilities enabled. ## Walkthrough ```mermaid theme={null} sequenceDiagram autonumber participant Usr as User participant U as Your App participant B as Your Backend participant P as Payment Widget participant A as Uphold participant F as Bank Usr->>U: Start bank withdrawal U->>B: List accounts B->>A: GET /core/accounts A-->>B: { accounts } B-->>U: { accounts } Usr->>U: Choose source account U->>B: Create widget session B->>A: Create widget session (select-for-withdrawal) A-->>B: { session } B-->>U: { session } U->>P: Initialize widget Usr->>P: Select bank account P-->>U: complete { via: "external-account", selection } U->>B: Request quote B->>A: Create quote A-->>B: { quote } B-->>U: { quote } Usr->>U: Confirm quote U->>B: Create transaction B->>A: Create transaction A-->>B: { transaction } A-->>B: webhook: transaction.created (processing) A->>F: Submit bank transfer F-->>A: Settlement confirmed A-->>B: webhook: transaction.status-changed (completed/failed) B-->>Usr: Notify the user ``` *** ## Select source account FPS withdrawals can be sourced from any account. If the selected account is not in GBP, the balance will be converted at the time of the transaction using Uphold's prevailing rate. Make sure the origin asset has the necessary [features enabled](/rest-apis/core-api/assets/introduction#features-and-deposits-/-withdrawals). Call [List accounts](/rest-apis/core-api/accounts/list-accounts) to retrieve the user's accounts and let them pick the one to withdraw from. ```http theme={null} GET /core/accounts ``` ```json theme={null} { "accounts": [ { "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "label": "My GBP account", "asset": "GBP", "balance": { "total": "500.00", "available": "500.00" } } ] } ``` *** ## Select a bank account The widget lets the user select an existing FPS-linked bank account. ### Create a widget session Call the [Create widget session](/rest-apis/widgets-api/payment/create-session) endpoint to create a session for the `select-for-withdrawal` flow. ```http theme={null} POST /widgets/payment/sessions { "flow": "select-for-withdrawal" } ``` A successful response returns a `session` object with a `token` and `url` to initialize the widget. ```json theme={null} { "session": { "flow": "select-for-withdrawal", "url": "https://payment.enterprise.uphold.com/", "token": "GEbRxBN...edjnXbL" } } ``` ### Set up the widget Initialize the widget for the `select-for-withdrawal` flow using the session data returned from the API. ```javascript theme={null} import { PaymentWidget } from '@uphold/enterprise-payment-widget-web-sdk'; const initializeWithdrawalWidget = async (session) => { // Initialize the widget const widget = new PaymentWidget<'select-for-withdrawal'>(session, { debug: true }); // Set up event handlers widget.on('ready', () => { console.log('Ready'); }); widget.on('complete', (event) => { console.log('Complete', JSON.stringify(event.detail.value)); }); widget.on('cancel', () => { console.log('Cancelled'); widget.unmount(); }); widget.on('error', (event) => { console.error('Error', event.detail.error); widget.unmount(); }); // Mount the widget widget.mountIframe(document.getElementById('payment-container')); }; ``` The example above is for web applications. For native apps using a WebView, you'll need a bridge for events as outlined in [Installation & setup](/widgets/payment/installation-and-setup#native-application-integration). ### Handle the complete event The `complete` event fires when the user selects a bank account and completes the flow. ```javascript theme={null} widget.on('complete', (event) => { const { via, selection } = event.detail.value; if (via === 'external-account' && selection.type === 'bank') { // selection is the external account the user selected handleBankWithdrawal(selection); } widget.unmount(); }); ``` The event payload contains two primary properties: * `via` — set to `external-account` when the user selects a saved bank account. * `selection` — an [external account](/rest-apis/core-api/external-accounts/introduction) object with the selected bank details. ```json theme={null} { "via": "external-account", "selection": { "id": "aa6e6efa-8d73-497c-8278-0347f459bd68", "type": "bank", "network": "fps", "label": "My GBP Account" } } ``` ### Handle cancellations The `cancel` event fires when the user closes the widget without selecting a bank account. ```javascript theme={null} widget.on('cancel', () => { widget.unmount(); // Redirect back or show a cancellation message }); ``` ### Handle errors The `error` event fires when an error occurs during the bank account selection process. ```javascript theme={null} widget.on('error', (event) => { console.error('Widget error:', event.detail.error); widget.unmount(); // Show a user-friendly error message }); ``` The Payment Widget handles most errors internally. For unrecoverable errors, the widget fires an `error` event. It is the host application's responsibility to handle these events, present an error message to the user, and unmount the widget. ## Create a quote To initiate the withdrawal, call [Create quote](/rest-apis/core-api/transactions/create-quote) with the origin as the user's account and the destination as the selected FPS external account. Specify the amount and asset for the withdrawal. ```http theme={null} POST /core/transactions/quote { "origin": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8" }, "destination": { "type": "external-account", "id": "aa6e6efa-8d73-497c-8278-0347f459bd68" }, "denomination": { "asset": "GBP", "amount": "250.00", "target": "origin" } } ``` A successful response returns a `quote` object with details about the withdrawal, including fees and expiration. ```json [expandable] theme={null} { "quote": { "id": "623000c8-9bdf-4a2b-aa3d-6a6b44a7f6a0", "origin": { "amount": "250.00", "asset": "GBP", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" }, "rate": "1" }, "destination": { "amount": "250.00", "asset": "GBP", "node": { "type": "external-account", "id": "aa6e6efa-8d73-497c-8278-0347f459bd68", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" }, "rate": "1" }, "denomination": { "asset": "GBP", "amount": "250.00", "target": "origin", "rate": "1" }, "fees": [], "expiresAt": "2024-07-24T15:22:39Z" } } ``` Quotes typically **expire** quickly. Prompt for user confirmation within the expiry window and requote if needed. ## Confirm and create transaction After the user confirms the withdrawal, call [Create transaction](/rest-apis/core-api/transactions/create-transaction) with the quote ID to execute the transfer. ```http theme={null} POST /core/transactions { "quoteId": "623000c8-9bdf-4a2b-aa3d-6a6b44a7f6a0" } ``` In a successful FPS withdrawal, the origin is the user's `account` and the destination is the `external-account` representing the user's bank. The transaction status is initially `processing` and updates to `completed` once the transfer settles. ```json [expandable] theme={null} { "transaction": { "id": "d3e4f5a6-1b2c-4d5e-9f8a-7b6c5d4e3f2a", "origin": { "asset": "GBP", "amount": "250.00", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "destination": { "asset": "GBP", "amount": "250.00", "node": { "type": "external-account", "id": "aa6e6efa-8d73-497c-8278-0347f459bd68", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "network": "fps" } }, "status": "processing", "quotedAt": "2025-01-10T14:22:15Z", "createdAt": "2025-01-10T14:22:45Z", "updatedAt": "2025-01-10T14:22:45Z", "denomination": { "asset": "GBP", "amount": "250.00", "target": "origin" } } } ``` ## Monitor for settlement The widget does not monitor for settlement. Your application must do this via webhooks or polling. * Webhook events (recommended): * [core.transaction.created](/rest-apis/core-api/transactions/webhooks/transaction-created) * `status: processing` → payout submitted to the bank network * [core.transaction.status-changed](/rest-apis/core-api/transactions/webhooks/transaction-status-changed) * `status: completed` → funds delivered to the user's bank * `status: failed` → irrecoverable error * Polling (fallback): [Get transaction](/rest-apis/core-api/transactions/get-transaction) ## Notify the user Display an in-app confirmation when the transaction is `completed`, and send an email if applicable. You now support FPS bank transfer withdrawals via the Payment Widget. # ACH bank withdrawal via the REST API Source: https://developer.uphold.com/developer-guides/bank-transfers/withdrawal/via-rest-api/ach Send USD ACH bank withdrawals with the Uphold REST API: create an external bank account, generate a quote, submit the payout, and monitor settlement. This guide walks you through the steps to support ACH bank withdrawals using the REST API — from creating an external account to monitoring for settlement. ## Prerequisites * The user has [completed onboarding](/developer-guides/user-onboarding/overview) and has the required capabilities enabled. ## Walkthrough ```mermaid theme={null} sequenceDiagram autonumber participant Usr as User participant U as Your App participant B as Your Backend participant A as Uphold participant F as Bank Usr->>U: Start withdrawal U->>B: Find bank account B->>A: GET /core/external-accounts A-->>B: { externalAccounts } B-->>U: { externalAccounts } Usr->>U: Select or link bank account opt No existing bank account U->>B: Link bank account B->>A: Create external account A-->>B: { externalAccount } end Usr->>U: Choose source account and amount U->>B: Request quote B->>A: Create quote (account -> external account) A-->>B: { quote } B-->>U: { quote } Usr->>U: Confirm quote U->>B: Create transaction B->>A: Create transaction A-->>B: { transaction } A-->>B: webhook: transaction.created (processing) A->>F: Submit bank transfer F-->>A: Settlement confirmed A-->>B: webhook: transaction.status-changed (completed/failed) B-->>Usr: Notify the user ``` *** ## Check available rails Before creating a quote, verify the ACH rail is available for withdrawals. Call [List Rails](/rest-apis/core-api/assets/list-rails) for `USD` to confirm the `ach` network has the `withdraw` feature. ```http theme={null} GET /core/rails?type=bank&network=ach&asset=USD ``` ```json theme={null} { "rails": [ { "type": "bank", "network": "ach", "method": "bank-transfer", "asset": "USD", "decimals": 2, "features": [ "deposit", "withdraw" ] } ] } ``` ## Select source account ACH withdrawals can be sourced from any account. If the selected account is not in USD, the balance will be converted at the time of the transaction using Uphold's prevailing rate. Make sure the origin asset has the necessary [features enabled](/rest-apis/core-api/assets/introduction#features-and-deposits-/-withdrawals). Call [List accounts](/rest-apis/core-api/accounts/list-accounts) to retrieve the user's accounts and let them pick the one to withdraw from. ```http theme={null} GET /core/accounts ``` ```json theme={null} { "accounts": [ { "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "label": "My USD account", "asset": "USD", "balance": { "total": "500.00", "available": "500.00" } } ] } ``` ## Select a bank account ACH bank accounts are not linked automatically. Users must provide their routing and account number to add one. Select an existing linked account or add a new one. ### Find an existing bank account Call [List external accounts](/rest-apis/core-api/external-accounts/list-external-accounts) and filter for accounts with `type: "bank"`. If no USD bank account exists, proceed to link one. ```http theme={null} GET /core/external-accounts ``` Make sure the selected account has `status: "ok"` and `"withdraw"` in `features`. ```json theme={null} { "externalAccounts": [ { "id": "bb7f7fab-9e84-5a8e-9389-1458f56ac79", "type": "bank", "status": "ok", "network": "ach", "label": "My USD Checking Account", "features": [ "withdraw" ], "secondaryNetworks": ["fednow"] } ] } ``` ### Link a new bank account Create a USD bank account by calling [Create external account](/rest-apis/core-api/external-accounts/create-external-account) with the user's bank details. ```http theme={null} POST /core/external-accounts { "type": "bank", "asset": "USD", "network": "ach", "label": "My USD Checking Account", "details": { "routingNumber": "121000248", "accountNumber": "9876543210", "accountType": "checking", "address": { "country": "US", "subdivision": "US-CA", "city": "San Francisco", "line1": "123 Main Street", "postalCode": "94102" } } } ``` ```json theme={null} { "externalAccount": { "id": "bb7f7fab-9e84-5a8e-9389-1458f56ac79", "type": "bank", "status": "ok", "network": "ach", "label": "My USD Checking Account", "features": [ "withdraw" ], "secondaryNetworks": ["fednow"] } } ``` The `secondaryNetworks` field is present only when the destination bank supports FedNow. If it appears, you may use `"fednow"` as the `network` in the quote to settle funds faster. ## Create a quote Create a quote with [Create quote](/rest-apis/core-api/transactions/create-quote). Pass `network: "ach"` on the destination node to route the transfer via ACH, or leave it out to route via the external account's default network. If the external account has `secondaryNetworks`, the user can choose which one to use for the transfer. The quote must be created with the selected network to ensure accurate fees and expiration time. ```http theme={null} POST /core/transactions/quote { "origin": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8" }, "destination": { "type": "external-account", "id": "bb7f7fab-9e84-5a8e-9389-1458f56ac79", "network": "ach" }, "denomination": { "asset": "USD", "amount": "500.00", "target": "origin" } } ``` A successful response returns a `quote` object with fees and expiration. ```json [expandable] theme={null} { "quote": { "id": "734111d9-ace0-5b3c-bb4e-7b7b55b8a7b1", "origin": { "amount": "500.00", "asset": "USD", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" }, "rate": "1" }, "destination": { "amount": "500.00", "asset": "USD", "node": { "type": "external-account", "id": "bb7f7fab-9e84-5a8e-9389-1458f56ac79", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "network": "ach" }, "rate": "1" }, "denomination": { "asset": "USD", "amount": "500.00", "target": "origin", "rate": "1" }, "fees": [], "expiresAt": "2024-07-24T15:22:39Z" } } ``` Quotes typically **expire** quickly. Prompt for user confirmation within the expiry window and requote if needed. ## Confirm and create transaction After the user confirms the withdrawal, call [Create transaction](/rest-apis/core-api/transactions/create-transaction) with the quote ID to execute the transfer. ```http theme={null} POST /core/transactions { "quoteId": "734111d9-ace0-5b3c-bb4e-7b7b55b8a7b1" } ``` In a successful ACH withdrawal, the origin is the user's `account` and the destination is the `external-account` representing the user's bank. The transaction status is initially `processing` and updates to `completed` once the transfer settles. ```json [expandable] theme={null} { "transaction": { "id": "e4f5a6b7-2c3d-4e6f-a09b-8c7d6e5f4a3c", "origin": { "asset": "USD", "amount": "500.00", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "destination": { "asset": "USD", "amount": "500.00", "node": { "type": "external-account", "id": "bb7f7fab-9e84-5a8e-9389-1458f56ac79", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "network": "ach" } }, "status": "processing", "quotedAt": "2025-01-10T14:22:15Z", "createdAt": "2025-01-10T14:22:45Z", "updatedAt": "2025-01-10T14:22:45Z", "denomination": { "asset": "USD", "amount": "500.00", "target": "origin" } } } ``` ## Monitor for settlement Prefer **webhooks** for real-time updates, or fall back to **polling** if webhooks are not feasible. * Webhook events (recommended): * [core.transaction.created](/rest-apis/core-api/transactions/webhooks/transaction-created) * `status: processing` → payout submitted to the bank network * [core.transaction.status-changed](/rest-apis/core-api/transactions/webhooks/transaction-status-changed) * `status: completed` → funds delivered to the user's bank * `status: failed` → irrecoverable error * Polling (fallback): [Get transaction](/rest-apis/core-api/transactions/get-transaction) ## Notify the user Display an in-app confirmation when the transaction is `completed`, and send an email if applicable. You now support ACH bank transfer withdrawals via the REST API. # FedNow / RTP bank withdrawal via the REST API Source: https://developer.uphold.com/developer-guides/bank-transfers/withdrawal/via-rest-api/fednow Send near-instant USD withdrawals via FedNow and RTP using the Uphold REST API, including verifying the destination bank's support for these rails. This guide walks you through the steps to support FedNow / RTP withdrawals using the REST API — from verifying the destination bank supports FedNow / RTP to monitoring for settlement. ## Prerequisites * The user has [completed onboarding](/developer-guides/user-onboarding/overview) and has the required capabilities enabled. FedNow withdrawals incur an additional fee. Make sure to present the quote to the user for confirmation before creating the transaction. ## Walkthrough ```mermaid theme={null} sequenceDiagram autonumber participant Usr as User participant U as Your App participant B as Your Backend participant A as Uphold participant F as Bank Usr->>U: Start withdrawal U->>B: Find bank account B->>A: GET /core/external-accounts A-->>B: { externalAccounts } B-->>U: { externalAccounts } Usr->>U: Select bank account opt No eligible bank account U->>B: Link bank account B->>A: Create external account A-->>B: { externalAccount } end U->>B: Request quote B->>A: Create quote (network: fednow) A-->>B: { quote } B-->>U: { quote } Usr->>U: Confirm quote U->>B: Create transaction B->>A: Create transaction A-->>B: { transaction } A-->>B: webhook: transaction.created (processing) A->>F: Submit FedNow transfer F-->>A: Settlement confirmed A-->>B: webhook: transaction.status-changed (completed/failed) B-->>Usr: Notify the user ``` *** ## Check available rails Call [List Rails](/rest-apis/core-api/assets/list-rails) to confirm the FedNow rail has the `withdraw` feature enabled. ```http theme={null} GET /core/rails?type=bank&network=fednow&asset=USD ``` ```json theme={null} { "rails": [ { "type": "bank", "network": "fednow", "method": "bank-transfer", "asset": "USD", "decimals": 2, "features": [ "deposit", "withdraw" ] } ] } ``` ## Select source account FedNow withdrawals can be sourced from any account. If the selected account is not in USD, the balance will be converted at the time of the transaction using Uphold's prevailing rate. Make sure the origin asset has the necessary [features enabled](/rest-apis/core-api/assets/introduction#features-and-deposits-/-withdrawals). Call [List accounts](/rest-apis/core-api/accounts/list-accounts) to retrieve the user's accounts and let them pick the one to withdraw from. ```http theme={null} GET /core/accounts ``` ```json theme={null} { "accounts": [ { "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "label": "My USD account", "asset": "USD", "balance": { "total": "500.00", "available": "500.00" } } ] } ``` ## Select a bank account The destination bank account must support FedNow or RTP, indicated by `"fednow"` in `secondaryNetworks`. Select an existing eligible account or add a new one. ### Find an existing bank account Call [List external accounts](/rest-apis/core-api/external-accounts/list-external-accounts) and filter for accounts with `type: "bank"`. If no eligible account exists, proceed to link one. ```http theme={null} GET /core/external-accounts ``` Make sure the selected account has `status: "ok"`, `"withdraw"` in `features`, and `"fednow"` in `secondaryNetworks`. ```json theme={null} { "externalAccounts": [ { "id": "bb7f7fab-9e84-5a8e-9389-1458f56ac79", "type": "bank", "status": "ok", "network": "ach", "label": "My USD Checking Account", "features": [ "withdraw" ], "secondaryNetworks": ["fednow"] } ] } ``` ### Link a new bank account If no eligible account exists, add one by calling [Create external account](/rest-apis/core-api/external-accounts/create-external-account) with the user's bank details. The `secondaryNetworks` field in the response will confirm whether the bank supports FedNow or RTP. ```http theme={null} POST /core/external-accounts { "type": "bank", "asset": "USD", "network": "ach", "label": "My USD Checking Account", "details": { "routingNumber": "121000248", "accountNumber": "9876543210", "accountType": "checking", "address": { "country": "US", "subdivision": "US-CA", "city": "San Francisco", "line1": "123 Main Street", "postalCode": "94102" } } } ``` ```json theme={null} { "externalAccount": { "id": "bb7f7fab-9e84-5a8e-9389-1458f56ac79", "type": "bank", "status": "ok", "network": "ach", "label": "My USD Checking Account", "features": [ "withdraw" ], "secondaryNetworks": ["fednow"] } } ``` If `secondaryNetworks` does not include `"fednow"`, the destination bank does not support FedNow or RTP. Use ACH instead. ## Create a quote Create a quote with [Create quote](/rest-apis/core-api/transactions/create-quote). Pass `network: "fednow"` on the destination node to route the transfer via FedNow. ```http theme={null} POST /core/transactions/quote { "origin": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8" }, "destination": { "type": "external-account", "id": "bb7f7fab-9e84-5a8e-9389-1458f56ac79", "network": "fednow" }, "denomination": { "asset": "USD", "amount": "500.00", "target": "origin" } } ``` The response includes the fees for the FedNow transfer. Present the full quote to the user before proceeding. ```json [expandable] theme={null} { "quote": { "id": "734111d9-ace0-5b3c-bb4e-7b7b55b8a7b1", "origin": { "amount": "500.00", "asset": "USD", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" }, "rate": "1" }, "destination": { "amount": "500.00", "asset": "USD", "node": { "type": "external-account", "id": "bb7f7fab-9e84-5a8e-9389-1458f56ac79", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "network": "fednow" }, "rate": "1" }, "denomination": { "asset": "USD", "amount": "500.00", "target": "origin", "rate": "1" }, "fees": [], "expiresAt": "2024-07-24T15:22:39Z" } } ``` Quotes typically **expire** quickly. Prompt for user confirmation within the expiry window and requote if needed. ## Confirm and create transaction After the user confirms the withdrawal, call [Create transaction](/rest-apis/core-api/transactions/create-transaction) with the quote ID to execute the transfer. ```http theme={null} POST /core/transactions { "quoteId": "734111d9-ace0-5b3c-bb4e-7b7b55b8a7b1" } ``` In a successful FedNow / RTP withdrawal, the origin is the user's `account` and the destination is the `external-account` representing the user's bank. The transaction status is initially `processing` and updates to `completed` once the transfer settles. ```json [expandable] theme={null} { "transaction": { "id": "e4f5a6b7-2c3d-4e6f-a09b-8c7d6e5f4a3c", "origin": { "asset": "USD", "amount": "500.00", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "destination": { "asset": "USD", "amount": "500.00", "node": { "type": "external-account", "id": "bb7f7fab-9e84-5a8e-9389-1458f56ac79", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "network": "fednow" } }, "status": "processing", "quotedAt": "2025-01-10T14:22:15Z", "createdAt": "2025-01-10T14:22:45Z", "updatedAt": "2025-01-10T14:22:45Z", "denomination": { "asset": "USD", "amount": "500.00", "target": "origin" } } } ``` ## Monitor for settlement Prefer **webhooks** for real-time updates, or fall back to **polling** if webhooks are not feasible. * Webhook events (recommended): * [core.transaction.created](/rest-apis/core-api/transactions/webhooks/transaction-created) * `status: processing` → payout submitted to the bank network * [core.transaction.status-changed](/rest-apis/core-api/transactions/webhooks/transaction-status-changed) * `status: completed` → funds delivered to the user's bank * `status: failed` → irrecoverable error * Polling (fallback): [Get transaction](/rest-apis/core-api/transactions/get-transaction) ## Notify the user Display an in-app confirmation when the transaction is `completed`, and send an email if applicable. You now support FedNow / RTP withdrawals via the REST API. # FPS bank withdrawal via the REST API Source: https://developer.uphold.com/developer-guides/bank-transfers/withdrawal/via-rest-api/fps Send GBP Faster Payments (FPS) withdrawals using the Uphold REST API: list linked external accounts, generate and confirm a quote, and monitor for settlement. This guide walks you through the steps to support FPS bank withdrawals using the REST API — from listing external accounts to monitoring for settlement. ## Prerequisites * The user has [completed onboarding](/developer-guides/user-onboarding/overview) and has the required capabilities enabled. ## Walkthrough ```mermaid theme={null} sequenceDiagram autonumber participant Usr as User participant U as Your App participant B as Your Backend participant A as Uphold participant F as Bank Usr->>U: Start withdrawal U->>B: Find bank account B->>A: GET /core/external-accounts A-->>B: { externalAccounts } B-->>U: { externalAccounts } Usr->>U: Select bank account Usr->>U: Choose source account and amount U->>B: Request quote B->>A: Create quote (account -> external account) A-->>B: { quote } B-->>U: { quote } Usr->>U: Confirm quote U->>B: Create transaction B->>A: Create transaction A-->>B: { transaction } A-->>B: webhook: transaction.created (processing) A->>F: Submit bank transfer F-->>A: Settlement confirmed A-->>B: webhook: transaction.status-changed (completed/failed) B-->>Usr: Notify the user ``` *** ## Check available rails Before creating a quote, verify the FPS rail is available for withdrawals. Call [List Rails](/rest-apis/core-api/assets/list-rails) for `GBP` to confirm the `fps` network has the `withdraw` feature. ```http theme={null} GET /core/rails?type=bank&network=fps&asset=GBP ``` The rail always exists in the system, but the `withdraw` feature is only present if it's enabled for the user. ```json theme={null} { "rails": [ { "type": "bank", "network": "fps", "method": "bank-transfer", "asset": "GBP", "decimals": 2, "features": [ "deposit", "withdraw" ] } ] } ``` ## Select source account FPS withdrawals can be sourced from any account. If the selected account is not in GBP, the balance will be converted at the time of the transaction using Uphold's prevailing rate. Make sure the origin asset has the necessary [features enabled](/rest-apis/core-api/assets/introduction#features-and-deposits-/-withdrawals). Call [List accounts](/rest-apis/core-api/accounts/list-accounts) to retrieve the user's accounts and let them pick the one to withdraw from. ```http theme={null} GET /core/accounts ``` ```json theme={null} { "accounts": [ { "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "label": "My GBP account", "asset": "GBP", "balance": { "total": "500.00", "available": "500.00" } } ] } ``` ## Select a bank account FPS bank accounts are registered automatically as external accounts when a user makes their first FPS deposit — they cannot be added manually. If the user has no FPS-linked accounts yet, direct them to complete an [FPS deposit](/developer-guides/bank-transfers/deposit/via-rest-api/fps) first. Call [List external accounts](/rest-apis/core-api/external-accounts/list-external-accounts) and filter for `bank` type and `fps` network to retrieve the user's linked bank accounts available for withdrawal. ```http theme={null} GET /core/external-accounts ``` Present the accounts to the user and make sure the selected one has `status: "ok"` and `"withdraw"` in `features`. ```json theme={null} { "externalAccounts": [ { "id": "aa6e6efa-8d73-497c-8278-0347f459bd68", "type": "bank", "status": "ok", "network": "fps", "label": "My GBP Account", "features": [ "withdraw" ] } ] } ``` ## Create a quote To initiate the withdrawal, call [Create quote](/rest-apis/core-api/transactions/create-quote) with the origin as the user's account and the destination as the selected FPS external account. Specify the amount and asset for the withdrawal. ```http theme={null} POST /core/transactions/quote { "origin": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8" }, "destination": { "type": "external-account", "id": "aa6e6efa-8d73-497c-8278-0347f459bd68" }, "denomination": { "asset": "GBP", "amount": "250.00", "target": "origin" } } ``` A successful response returns a `quote` object with details about the withdrawal, including fees and expiration. ```json [expandable] theme={null} { "quote": { "id": "623000c8-9bdf-4a2b-aa3d-6a6b44a7f6a0", "origin": { "amount": "250.00", "asset": "GBP", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" }, "rate": "1" }, "destination": { "amount": "250.00", "asset": "GBP", "node": { "type": "external-account", "id": "aa6e6efa-8d73-497c-8278-0347f459bd68", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" }, "rate": "1" }, "denomination": { "asset": "GBP", "amount": "250.00", "target": "origin", "rate": "1" }, "fees": [], "expiresAt": "2024-07-24T15:22:39Z" } } ``` Quotes typically **expire** quickly. Prompt for user confirmation within the expiry window and requote if needed. ## Confirm and create transaction After the user confirms the withdrawal, call [Create transaction](/rest-apis/core-api/transactions/create-transaction) with the quote ID to execute the transfer. ```http theme={null} POST /core/transactions { "quoteId": "623000c8-9bdf-4a2b-aa3d-6a6b44a7f6a0" } ``` In a successful FPS withdrawal, the origin is the user's `account` and the destination is the `external-account` representing the user's bank. The transaction status is initially `processing` and updates to `completed` once the transfer settles. ```json [expandable] theme={null} { "transaction": { "id": "d3e4f5a6-1b2c-4d5e-9f8a-7b6c5d4e3f2a", "origin": { "asset": "GBP", "amount": "250.00", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "destination": { "asset": "GBP", "amount": "250.00", "node": { "type": "external-account", "id": "aa6e6efa-8d73-497c-8278-0347f459bd68", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "network": "fps" } }, "status": "processing", "quotedAt": "2025-01-10T14:22:15Z", "createdAt": "2025-01-10T14:22:45Z", "updatedAt": "2025-01-10T14:22:45Z", "denomination": { "asset": "GBP", "amount": "250.00", "target": "origin" } } } ``` ## Monitor for settlement Prefer **webhooks** for real-time updates, or fall back to **polling** if webhooks are not feasible. * Webhook events (recommended): * [core.transaction.created](/rest-apis/core-api/transactions/webhooks/transaction-created) * `status: processing` → payout submitted to the bank network * [core.transaction.status-changed](/rest-apis/core-api/transactions/webhooks/transaction-status-changed) * `status: completed` → funds delivered to the user's bank * `status: failed` → irrecoverable error * Polling (fallback): [Get transaction](/rest-apis/core-api/transactions/get-transaction) ## Notify the user Display an in-app confirmation when the transaction is `completed`, and send an email if applicable. You now support FPS bank transfer withdrawals via the REST API. # SEPA bank withdrawal via the REST API Source: https://developer.uphold.com/developer-guides/bank-transfers/withdrawal/via-rest-api/sepa Send EUR SEPA bank withdrawals using the Uphold REST API: list linked external accounts, generate and confirm a quote, and monitor settlement. This guide walks you through the steps to support SEPA bank withdrawals using the REST API — from listing external accounts to monitoring for settlement. ## Prerequisites * The user has [completed onboarding](/developer-guides/user-onboarding/overview) and has the required capabilities enabled. ## Walkthrough ```mermaid theme={null} sequenceDiagram autonumber participant Usr as User participant U as Your App participant B as Your Backend participant A as Uphold participant F as Bank Usr->>U: Start withdrawal U->>B: Find bank account B->>A: GET /core/external-accounts A-->>B: { externalAccounts } B-->>U: { externalAccounts } Usr->>U: Select bank account Usr->>U: Choose source account and amount U->>B: Request quote B->>A: Create quote (account -> external account) A-->>B: { quote } B-->>U: { quote } Usr->>U: Confirm quote U->>B: Create transaction B->>A: Create transaction A-->>B: { transaction } A-->>B: webhook: transaction.created (processing) A->>F: Submit bank transfer F-->>A: Settlement confirmed A-->>B: webhook: transaction.status-changed (completed/failed) B-->>Usr: Notify the user ``` *** ## Check available rails Before creating a quote, verify the SEPA rail is available for withdrawals. Call [List Rails](/rest-apis/core-api/assets/list-rails) for `EUR` to confirm the `sepa` network has the `withdraw` feature. ```http theme={null} GET /core/rails?type=bank&network=sepa&asset=EUR ``` The rail always exists in the system, but the `withdraw` feature is only present if it's enabled for the user. ```json theme={null} { "rails": [ { "type": "bank", "network": "sepa", "method": "bank-transfer", "asset": "EUR", "decimals": 2, "features": [ "deposit", "withdraw" ] } ] } ``` ## Select source account SEPA withdrawals can be sourced from any account. If the selected account is not in EUR, the balance will be converted at the time of the transaction using Uphold's prevailing rate. Make sure the origin asset has the necessary [features enabled](/rest-apis/core-api/assets/introduction#features-and-deposits-/-withdrawals). Call [List accounts](/rest-apis/core-api/accounts/list-accounts) to retrieve the user's accounts and let them pick the one to withdraw from. ```http theme={null} GET /core/accounts ``` ```json theme={null} { "accounts": [ { "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "label": "My EUR account", "asset": "EUR", "balance": { "total": "500.00", "available": "500.00" } } ] } ``` ## Select a bank account SEPA bank accounts are registered automatically as external accounts when a user makes their first SEPA deposit — they cannot be added manually. If the user has no SEPA-linked accounts yet, direct them to complete a [SEPA deposit](/developer-guides/bank-transfers/deposit/via-rest-api/sepa) first. Call [List external accounts](/rest-apis/core-api/external-accounts/list-external-accounts) and filter for accounts with `type: "bank"`. ```http theme={null} GET /core/external-accounts ``` Make sure the selected account has `status: "ok"` and `"withdraw"` in `features`. ```json theme={null} { "externalAccounts": [ { "id": "cc8e8abc-0f95-4b9f-8490-2569a67bd80e", "type": "bank", "status": "ok", "network": "sepa", "label": "My EUR Account", "features": [ "withdraw" ], "details": { "iban": "AT487954841229809844", "bic": "EXAAAT2K" } } ] } ``` ## Create a quote To initiate the withdrawal, call [Create quote](/rest-apis/core-api/transactions/create-quote) with the origin as the user's account and the destination as the selected SEPA external account. Specify the amount and asset for the withdrawal. ```http theme={null} POST /core/transactions/quote { "origin": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8" }, "destination": { "type": "external-account", "id": "cc8e8abc-0f95-4b9f-8490-2569a67bd80e" }, "denomination": { "asset": "EUR", "amount": "250.00", "target": "origin" } } ``` A successful response returns a `quote` object with details about the withdrawal, including fees and expiration. ```json [expandable] theme={null} { "quote": { "id": "623000c8-9bdf-4a2b-aa3d-6a6b44a7f6a0", "origin": { "amount": "250.00", "asset": "EUR", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" }, "rate": "1" }, "destination": { "amount": "250.00", "asset": "EUR", "node": { "type": "external-account", "id": "cc8e8abc-0f95-4b9f-8490-2569a67bd80e", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" }, "rate": "1" }, "denomination": { "asset": "EUR", "amount": "250.00", "target": "origin", "rate": "1" }, "fees": [], "expiresAt": "2024-07-24T15:22:39Z" } } ``` Quotes typically **expire** quickly. Prompt for user confirmation within the expiry window and requote if needed. ## Confirm and create transaction After the user confirms the withdrawal, call [Create transaction](/rest-apis/core-api/transactions/create-transaction) with the quote ID to execute the transfer. ```http theme={null} POST /core/transactions { "quoteId": "623000c8-9bdf-4a2b-aa3d-6a6b44a7f6a0" } ``` In a successful SEPA withdrawal, the origin is the user's `account` and the destination is the `external-account` representing the user's bank. The transaction status is initially `processing` and updates to `completed` once the transfer settles. ```json [expandable] theme={null} { "transaction": { "id": "d3e4f5a6-1b2c-4d5e-9f8a-7b6c5d4e3f2a", "origin": { "asset": "EUR", "amount": "250.00", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "destination": { "asset": "EUR", "amount": "250.00", "node": { "type": "external-account", "id": "cc8e8abc-0f95-4b9f-8490-2569a67bd80e", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "network": "sepa" } }, "status": "processing", "quotedAt": "2025-01-10T14:22:15Z", "createdAt": "2025-01-10T14:22:45Z", "updatedAt": "2025-01-10T14:22:45Z", "denomination": { "asset": "EUR", "amount": "250.00", "target": "origin" } } } ``` ## Monitor for settlement Prefer **webhooks** for real-time updates, or fall back to **polling** if webhooks are not feasible. * Webhook events (recommended): * [core.transaction.created](/rest-apis/core-api/transactions/webhooks/transaction-created) * `status: processing` → payout submitted to the bank network * [core.transaction.status-changed](/rest-apis/core-api/transactions/webhooks/transaction-status-changed) * `status: completed` → funds delivered to the user's bank * `status: failed` → irrecoverable error * Polling (fallback): [Get transaction](/rest-apis/core-api/transactions/get-transaction) ## Notify the user Display an in-app confirmation when the transaction is `completed`, and send an email if applicable. You now support SEPA bank transfer withdrawals via the REST API. # Card deposit via the Payment Widget Source: https://developer.uphold.com/developer-guides/card-transfers/deposit/via-payment-widget Accept card deposits with the Uphold Payment Widget for card selection and 3DS authorization. The Payment Widget handles card linking and selection for deposits via the **Select for Deposit flow**, where users can add a new card or pick an existing one directly in the widget. After the user confirms a card deposit quote, the Payment Widget creates the transaction and handles any 3DS challenge the issuer requires via the **Authorize flow**. ## Prerequisites * The user has [completed onboarding](/developer-guides/user-onboarding/overview) and has the required capabilities enabled. ## Walkthrough ```mermaid theme={null} sequenceDiagram autonumber participant Usr as User participant U as Your App participant B as Your Backend participant P as Payment Widget participant A as Uphold Usr->>U: Start card deposit U->>B: Create widget session B->>A: Create widget session (select-for-deposit) A-->>B: { session } B-->>U: { session } U->>P: Initialize widget Usr->>P: Select card account P-->>U: complete { via, selection } U->>B: List accounts B->>A: GET /core/accounts A-->>B: { accounts } B-->>U: { accounts } Usr->>U: Choose destination account and amount U->>B: Request quote B->>A: Create quote (card → account) A-->>B: { quote } B-->>U: { quote } Usr->>U: Confirm U->>B: Create widget session B->>A: Create widget session (authorize) A-->>B: { session } B-->>U: { session } U->>P: Initialize widget P->>A: Create transaction A-->>P: { transaction (with confirmationUrl if authorization required) } A-->>B: webhook: transaction.created (processing) opt confirmationUrl present Usr->>P: Complete authorization challenge end P-->>U: complete { transaction, trigger } A-->>B: webhook: transaction.status-changed (completed/failed) B-->>Usr: Notify the user ``` *** ## Link or select a card account The widget session lets the user link a new card or select an existing one. ### Create a widget session Call [Create widget session](/rest-apis/widgets-api/payment/create-session) to start the `select-for-deposit` flow. ```http theme={null} POST /widgets/payment/sessions { "flow": "select-for-deposit" } ``` ```json theme={null} { "session": { "flow": "select-for-deposit", "url": "https://payment.enterprise.uphold.com/", "token": "GEbRxBN...edjnXbL" } } ``` ### Set up the widget ```javascript theme={null} import { PaymentWidget } from '@uphold/enterprise-payment-widget-web-sdk'; const initializeDepositWidget = async (session) => { const widget = new PaymentWidget<'select-for-deposit'>(session, { debug: true }); widget.on('complete', (event) => { console.log('Complete', JSON.stringify(event.detail.value)); }); widget.on('cancel', () => { console.log('Cancelled'); widget.unmount(); }); widget.on('error', (event) => { console.error('Error', event.detail.error); widget.unmount(); }); widget.mountIframe(document.getElementById('payment-container')); }; ``` For native apps using a WebView, you'll need a bridge for events as outlined in [Installation & setup](/widgets/payment/installation-and-setup#native-application-integration). ### Handle the complete event The `complete` event fires after the user selects a card. The event payload includes the selected card external account. ```javascript theme={null} widget.on('complete', (event) => { const { via, selection } = event.detail.value; if (via === 'external-account') { // selection is the selected card external account // use selection.id as the origin in the quote handleCardSelected(selection); } widget.unmount(); }); ``` Once you have the selected card, prompt the user to select a destination account, then create a quote. ### Handle cancellations ```javascript theme={null} widget.on('cancel', () => { widget.unmount(); // Return the user to the previous screen }); ``` ### Handle errors The `error` event fires for critical unrecoverable errors. Card-specific errors (duplicate card, country mismatch, card limits) are handled by the widget internally. ```javascript theme={null} widget.on('error', (event) => { console.error('Widget error:', event.detail.error); widget.unmount(); // Show a user-friendly error message }); ``` The Payment Widget handles most errors internally. For unrecoverable errors, the widget fires an `error` event. It is the host application's responsibility to handle these events, present an error message to the user, and unmount the widget. *** ## Select destination account Card deposits can target any account. If the selected account is not in the card's currency, the amount will be converted at settlement using Uphold's prevailing rate. Make sure the destination asset has the necessary [features enabled](/rest-apis/core-api/assets/introduction#features-and-deposits-/-withdrawals). ### Find an existing account Call [List accounts](/rest-apis/core-api/accounts/list-accounts) to retrieve the user's accounts and let them pick the one they want to fund. ```http theme={null} GET /core/accounts ``` ```json theme={null} { "accounts": [ { "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "label": "My GBP account", "asset": "GBP", "balance": { "total": "500.00", "available": "500.00" } } ] } ``` ### Create a new account If the user has no accounts, create one with [Create account](/rest-apis/core-api/accounts/create-account) before proceeding. ```http theme={null} POST /core/accounts { "label": "My GBP account", "asset": "GBP" } ``` ```json theme={null} { "account": { "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "label": "My GBP account", "asset": "GBP", "balance": { "total": "0", "available": "0" } } } ``` *** ## Create a quote Call [Create quote](/rest-apis/core-api/transactions/create-quote) with the selected card external account as origin and the destination account. ```http theme={null} POST /core/transactions/quote { "origin": { "type": "external-account", "id": "7d5928c5-8ac4-4b0d-8b45-f332ba6a9de7" }, "destination": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8" }, "denomination": { "asset": "GBP", "amount": "250.00", "target": "origin" } } ``` ```json [expandable] theme={null} { "quote": { "id": "623000c8-9bdf-4a2b-aa3d-6a6b44a7f6a0", "origin": { "amount": "250.00", "asset": "GBP", "node": { "type": "external-account", "id": "7d5928c5-8ac4-4b0d-8b45-f332ba6a9de7", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" }, "rate": "1" }, "destination": { "amount": "250.00", "asset": "GBP", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" }, "rate": "1" }, "denomination": { "asset": "GBP", "amount": "250.00", "target": "origin", "rate": "1" }, "fees": [], "expiresAt": "2024-07-24T15:22:39Z" } } ``` Quotes typically **expire** quickly. Prompt for user confirmation within the expiry window and requote if needed. ## Authorize and create the transaction After the user confirms the card deposit quote, hand off to the Payment Widget Authorize flow. This flow creates the transaction, completes any 3DS challenge the issuer requires, and polls until a terminal status is reached — so the same flow works whether or not authorization is needed. ### Create an authorize session Call [Create widget session](/rest-apis/widgets-api/payment/create-session) with `flow: "authorize"` and the `quoteId`. ```http theme={null} POST /widgets/payment/sessions { "flow": "authorize", "data": { "quoteId": "" } } ``` ```json theme={null} { "session": { "flow": "authorize", "url": "https://payment.enterprise.uphold.com/", "token": "GEbRxBN...edjnXbL" } } ``` ### Set up the widget Initialize the widget with the session. The widget creates the transaction, handles the 3DS redirect, and polls until a terminal status is reached. ```javascript theme={null} import { PaymentWidget } from '@uphold/enterprise-payment-widget-web-sdk'; const initializeAuthorizeWidget = async (session) => { const widget = new PaymentWidget<'authorize'>(session, { debug: true }); widget.on('complete', (event) => { const { transaction, trigger } = event.detail.value; console.log('Complete', transaction.status, trigger.reason); widget.unmount(); }); widget.on('cancel', () => { console.log('Cancelled'); widget.unmount(); }); widget.on('error', (event) => { console.error('Error', event.detail.error); widget.unmount(); }); widget.mountIframe(document.getElementById('payment-container')); }; ``` For native apps using a WebView, you'll need a bridge for events as outlined in [Installation & setup](/widgets/payment/installation-and-setup#native-application-integration). ### Handle the complete event The `complete` event does not guarantee success. Always check `transaction.status` and `trigger.reason`. ```javascript theme={null} widget.on('complete', (event) => { const { transaction, trigger } = event.detail.value; if (trigger.reason === 'transaction-status-changed') { if (transaction.status === 'completed') { // Show success } else if (transaction.status === 'failed') { // Map transaction.statusDetails.reason to a user-facing message } } else if (trigger.reason === 'max-retries-reached') { // Widget stopped polling — continue monitoring via webhooks or polling } widget.unmount(); }); ``` Failure reasons in `transaction.statusDetails.reason`: | Reason | Description | | ----------------------------------- | --------------------------------------------- | | `card-declined-by-bank` | The card was declined by the issuing bank | | `card-expired` | The card has expired | | `card-permanently-declined-by-bank` | The card was permanently declined | | `card-unauthorized` | The card authorization was not completed | | `card-unsupported` | The card is not supported for this operation | | `insufficient-funds` | The origin account has insufficient funds | | `provider-maximum-limit-exceeded` | The transaction exceeds provider limits | | `velocity` | The transaction was blocked by velocity rules | ### Handle cancellations The `cancel` event fires when the user navigates back without completing authorization. ```javascript theme={null} widget.on('cancel', () => { widget.unmount(); // Return the user to the previous screen }); ``` ### Handle errors The `error` event fires for critical unrecoverable errors. Card-specific errors (duplicate card, country mismatch, card limits) are handled by the widget internally. ```javascript theme={null} widget.on('error', (event) => { const { code, details } = event.detail.error; console.error('Widget error:', code, details); widget.unmount(); // Show a user-friendly error message }); ``` Error codes in `event.detail.error.code`: | Code | Description | | ------------------------- | ----------------------------------------------------------------------------- | | `entity_not_found` | The quote was not found or has expired | | `insufficient_balance` | The origin account has insufficient balance | | `operation_not_allowed` | The operation is not permitted (e.g. duplicate withdrawal, card unauthorized) | | `user_capability_failure` | The user lacks the required capability for this operation | The Payment Widget handles most errors internally. For unrecoverable errors, the widget fires an `error` event. It is the host application's responsibility to handle these events, present an error message to the user, and unmount the widget. ### Complete implementation example ```javascript [expandable] theme={null} import { PaymentWidget } from '@uphold/enterprise-payment-widget-web-sdk'; const initializeAuthorizeWidget = async (session) => { const widget = new PaymentWidget<'authorize'>(session, { debug: true }); widget.on('complete', (event) => { const { transaction, trigger } = event.detail.value; handleAuthorizeComplete(transaction, trigger); widget.unmount(); }); widget.on('cancel', () => { widget.unmount(); // Return the user to the previous screen }); widget.on('error', (event) => { const { code, details } = event.detail.error; handleAuthorizeError(code, details); widget.unmount(); }); widget.mountIframe(document.getElementById('payment-container')); }; const handleAuthorizeComplete = (transaction, trigger) => { if (trigger.reason === 'transaction-status-changed') { if (transaction.status === 'completed') { // Show success — transaction is settled } else if (transaction.status === 'failed') { handleTransactionFailure(transaction.statusDetails.reason); } } else if (trigger.reason === 'max-retries-reached') { // Widget stopped polling — continue monitoring via webhooks or polling } }; const handleTransactionFailure = (reason) => { switch (reason) { case 'card-declined-by-bank': case 'card-permanently-declined-by-bank': // Prompt the user to try a different card break; case 'card-expired': // Prompt the user to update their card break; case 'card-unauthorized': // 3DS authentication was not completed break; case 'card-unsupported': // Card type is not supported for this operation break; case 'insufficient-funds': // User does not have enough funds break; case 'provider-maximum-limit-exceeded': case 'velocity': // Transaction blocked by limits — inform the user break; default: // Unhandled reason — show a generic error message break; } }; const handleAuthorizeError = (code, details) => { switch (code) { case 'entity_not_found': // Quote expired — prompt user to start over break; case 'insufficient_balance': // Origin account has insufficient balance break; case 'operation_not_allowed': // Operation not permitted (e.g. duplicate withdrawal, card unauthorized) break; case 'user_capability_failure': // User lacks the required capability break; default: // Unexpected error — show a generic error message break; } }; ``` In a successful card deposit, the origin is the `external-account` representing the card and the destination is the user's `account`. ```json [expandable] theme={null} { "transaction": { "id": "f5a6b7c8-3d4e-4f7a-b00c-9d8e7f6a5b4c", "origin": { "asset": "GBP", "amount": "250.00", "node": { "type": "external-account", "id": "7d5928c5-8ac4-4b0d-8b45-f332ba6a9de7", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "destination": { "asset": "GBP", "amount": "250.00", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "status": "completed", "quotedAt": "2025-01-10T11:02:39Z", "createdAt": "2025-01-10T11:12:39Z", "updatedAt": "2025-01-10T11:13:08Z", "denomination": { "asset": "GBP", "amount": "250.00", "target": "origin" } } } ``` *** ## Monitor for settlement Card deposit transactions may remain in `processing` while the payment settles. Monitor until the transaction reaches a terminal state. * **Webhook events** (recommended): * [core.transaction.created](/rest-apis/core-api/transactions/webhooks/transaction-created) — `status: processing` → transaction created, pending settlement * [core.transaction.status-changed](/rest-apis/core-api/transactions/webhooks/transaction-status-changed) — `status: completed` → funds settled; `status: failed` → irrecoverable error * **Polling** (fallback): [Get transaction](/rest-apis/core-api/transactions/get-transaction) ## Notify the user Display an in-app confirmation when the transaction is `completed`, and send an email if applicable. You now support card deposits via the Payment Widget. # Card deposit via the REST API Source: https://developer.uphold.com/developer-guides/card-transfers/deposit/via-rest-api Support card deposits using the Uphold REST API: link a card as an external account, create a quote, handle 3DS, and create the deposit transaction. This guide walks you through supporting card deposits using the REST API. ## Prerequisites * The user has [completed onboarding](/developer-guides/user-onboarding/overview) and has the required capabilities enabled. ## Walkthrough ```mermaid theme={null} sequenceDiagram autonumber participant Usr as User participant U as Your App participant B as Your Backend participant A as Uphold Usr->>U: Start card deposit U->>B: List card accounts B->>A: GET /core/external-accounts A-->>B: { externalAccounts } B-->>U: { externalAccounts } opt No existing card account Usr->>U: Add card details U->>B: Link card account B->>A: Create card external account A-->>B: { externalAccount } B-->>U: { externalAccount } end Usr->>U: Choose card account alt First time (EMD disclaimer) U->>Usr: Display EMD disclaimer Usr->>U: Acknowledge end U->>B: List accounts B->>A: GET /core/accounts A-->>B: { accounts } B-->>U: { accounts } Usr->>U: Choose destination account and amount U->>B: Request quote B->>A: Create quote (card → account) A-->>B: { quote } B-->>U: { quote } Usr->>U: Confirm U->>B: Create transaction (with returnUrl) B->>A: Create transaction A-->>B: { transaction (with confirmationUrl if authorization required) } B-->>U: { transaction } A-->>B: webhook: transaction.created (processing) alt confirmationUrl present (authorization required) U->>Usr: Redirect to confirmationUrl Usr->>A: Complete authorization challenge A->>Usr: Redirect to returnUrl end A-->>B: webhook: transaction.status-changed (completed/failed) B-->>Usr: Notify the user ``` *** ## Check available rails Call [List Rails](/rest-apis/core-api/assets/list-rails) to verify card deposit is available. ```http theme={null} GET /core/rails?type=card ``` ```json theme={null} { "rails": [ { "type": "card", "network": "visa", "method": "debit-card", "asset": "GBP", "decimals": 2, "features": [ "deposit", "withdraw" ] } ] } ``` ## Link or select a card account Let the user link a new card or select an existing one. ### Find an existing card account Call [List external accounts](/rest-apis/core-api/external-accounts/list-external-accounts) to fetch the user's saved cards. ```http theme={null} GET /core/external-accounts ``` Make sure the selected card has `status: "ok"` and in `features`. ```json theme={null} { "externalAccounts": [ { "id": "7d5928c5-8ac4-4b0d-8b45-f332ba6a9de7", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "type": "card", "status": "ok", "label": "My Visa Card", "asset": "GBP", "network": "visa", "features": [ "deposit", "withdraw" ], "details": { "type": "debit", "last4Digits": "5119", "expiryDate": { "month": 12, "year": 2028 }, "octSupport": "supported" } } ] } ``` ### Link a new card account If the user wants to use a card not yet on file, call [Create external account](/rest-apis/core-api/external-accounts/create-external-account) with the card details. ```http theme={null} POST /core/external-accounts { "type": "card", "label": "My Visa Card", "number": "4921817844445119", "securityCode": "123", "expiryDate": { "month": 12, "year": 2028 } } ``` The response initially returns `status: "processing"` while the card is validated. ```json theme={null} { "externalAccount": { "id": "7d5928c5-8ac4-4b0d-8b45-f332ba6a9de7", "type": "card", "status": "processing", "label": "My Visa Card" } } ``` Once validated, the status transitions to `ok` and the full details are available. ```json theme={null} { "externalAccount": { "id": "7d5928c5-8ac4-4b0d-8b45-f332ba6a9de7", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "type": "card", "status": "ok", "label": "My Visa Card", "asset": "GBP", "network": "visa", "features": [ "deposit", "withdraw" ], "details": { "type": "debit", "last4Digits": "5119", "expiryDate": { "month": 12, "year": 2028 }, "octSupport": "supported" } } } ``` Monitor the external account status via [Get external account](/rest-apis/core-api/external-accounts/get-external-account) or the `external-account.status-changed` webhook until `status` is `ok` before proceeding. ### EMD disclaimer This disclaimer is required to comply with FCA regulations. Display it to users based in GB, the first time they link a credit/debit card or generate bank deposit details — it only needs to be shown once: > Uphold Europe Limited is an EMD Agent of Optimus Cards UK Limited (FRN: 902034). All received funds are held in a designated safekeeping account with a regulated bank and kept separate from Uphold's own assets. These funds are not protected by the UK Financial Services Compensation Scheme. ## Select destination account Card deposits can target any account. If the selected account is not in the card's currency, the amount will be converted at settlement using Uphold's prevailing rate. Make sure the destination asset has the necessary [features enabled](/rest-apis/core-api/assets/introduction#features-and-deposits-/-withdrawals). ### Find an existing account Call [List accounts](/rest-apis/core-api/accounts/list-accounts) to retrieve the user's accounts and let them pick the one they want to fund. ```http theme={null} GET /core/accounts ``` ```json theme={null} { "accounts": [ { "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "label": "My GBP account", "asset": "GBP", "balance": { "total": "500.00", "available": "500.00" } } ] } ``` ### Create a new account If the user has no accounts, create one with [Create account](/rest-apis/core-api/accounts/create-account) before proceeding. ```http theme={null} POST /core/accounts { "label": "My GBP account", "asset": "GBP" } ``` ```json theme={null} { "account": { "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "label": "My GBP account", "asset": "GBP", "balance": { "total": "0", "available": "0" } } } ``` ## Create a quote Call [Create quote](/rest-apis/core-api/transactions/create-quote) with the card external account as origin and the destination account. ```http theme={null} POST /core/transactions/quote { "origin": { "type": "external-account", "id": "7d5928c5-8ac4-4b0d-8b45-f332ba6a9de7" }, "destination": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8" }, "denomination": { "asset": "GBP", "amount": "250.00", "target": "origin" } } ``` ```json [expandable] theme={null} { "quote": { "id": "623000c8-9bdf-4a2b-aa3d-6a6b44a7f6a0", "origin": { "amount": "250.00", "asset": "GBP", "node": { "type": "external-account", "id": "7d5928c5-8ac4-4b0d-8b45-f332ba6a9de7", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" }, "rate": "1" }, "destination": { "amount": "250.00", "asset": "GBP", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" }, "rate": "1" }, "denomination": { "asset": "GBP", "amount": "250.00", "target": "origin", "rate": "1" }, "fees": [], "expiresAt": "2024-07-24T15:22:39Z" } } ``` Quotes typically **expire** quickly. Prompt for user confirmation within the expiry window and requote if needed. ## Confirm and create transaction Once the user confirms, call [Create transaction](/rest-apis/core-api/transactions/create-transaction) with the quote ID. Always include a `returnUrl` in params for card-origin transactions — whether the card requires authorization is decided per transaction by the issuer and isn't known until the transaction response. You may include stateful data in query parameters of the `returnUrl`, such as the `quoteId`, so you can preserve context and resume the transaction flow upon return. ```http theme={null} POST /core/transactions { "quoteId": "623000c8-9bdf-4a2b-aa3d-6a6b44a7f6a0", "params": { "returnUrl": "https://example.com/redirect?quoteId=623000c8-9bdf-4a2b-aa3d-6a6b44a7f6a0" } } ``` If authorization is required, the response will include a `confirmationUrl` on the origin node. Redirect the user to that URL to complete the challenge. After completion, the user will be redirected back to your `returnUrl`. ```json theme={null} { "transaction": { "id": "623000c8-9bdf-4a2b-aa3d-6a6b44a7f6a0", "status": "processing", "origin": { "node": { "type": "external-account", "id": "7d5928c5-8ac4-4b0d-8b45-f332ba6a9de7", "confirmationUrl": "https://authentication-devices.sandbox.checkout.com/sessions-interceptor/sid_...", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } } } } ``` In a successful card deposit, the origin is the `external-account` representing the card and the destination is the user's `account`. ```json [expandable] theme={null} { "transaction": { "id": "f5a6b7c8-3d4e-4f7a-b00c-9d8e7f6a5b4c", "origin": { "asset": "GBP", "amount": "250.00", "node": { "type": "external-account", "id": "7d5928c5-8ac4-4b0d-8b45-f332ba6a9de7", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "destination": { "asset": "GBP", "amount": "250.00", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "status": "completed", "quotedAt": "2025-01-10T11:02:39Z", "createdAt": "2025-01-10T11:12:39Z", "updatedAt": "2025-01-10T11:13:08Z", "denomination": { "asset": "GBP", "amount": "250.00", "target": "origin" } } } ``` ## Monitor for settlement Card deposit transactions may remain in `processing` while the payment settles. Monitor until the transaction reaches a terminal state. * **Webhook events** (recommended): * [core.transaction.created](/rest-apis/core-api/transactions/webhooks/transaction-created) — `status: processing` → transaction created, pending settlement * [core.transaction.status-changed](/rest-apis/core-api/transactions/webhooks/transaction-status-changed) — `status: completed` → funds settled; `status: failed` → irrecoverable error * **Polling** (fallback): [Get transaction](/rest-apis/core-api/transactions/get-transaction) ## Notify the user Display an in-app confirmation when the transaction is `completed`, and send an email if applicable. You now support card deposits via the REST API. # Card transfer integration overview (debit and credit cards) Source: https://developer.uphold.com/developer-guides/card-transfers/overview Let users fund accounts and cash out with debit or credit cards in EUR, GBP, or USD. Covers card linking, 3DS authorization, and card limits. Allow users to fund or cash out with their debit/credit cards, globally, in EUR, GBP, or USD. ## Key concepts * **External account lifecycle** — when a card is added, its status starts as `processing` and transitions to `ok` once validated, or `failed` if the card is invalid or declined. If using the Payment Widget, card-linking specific errors (duplicates, country mismatch, card limits) are handled internally. * **3DS authorization** — when the transaction response includes a `confirmationUrl` on the card node, 3DS needs to be fulfilled. Either handle the redirect in your app or use the Payment Widget to manage the authorization flow. * **Card limits** — users may be subject to active card limits and unique card limits. These are enforced as capability restrictions. ## Testing in sandbox Use [Test cards](/rest-apis/core-api/accounts/test-helpers/fund-sandbox-accounts#test-cards) to simulate card deposits and withdrawals in the Sandbox environment. The table lists card numbers by network, type, and country, along with reserved amounts that trigger specific error responses. ## Start building Set up card deposits and handle 3DS authentication programmatically. Let users add their card and deposit funds with a low-code, embeddable widget. Create quotes and submit payouts to your users' cards. Let users withdraw funds to their card with a low-code, embeddable widget. # Card withdrawal via the Payment Widget Source: https://developer.uphold.com/developer-guides/card-transfers/withdrawal/via-payment-widget Send card withdrawals with the Uphold Payment Widget for card selection, then create the quote and transaction directly via the REST API to complete payout. The Payment Widget handles card selection for withdrawals via the **Select for Withdrawal flow**. Your backend then creates the quote and the transaction directly via the REST API. The Payment Widget handles card selection only. Your backend must create the transaction via the REST API. ## Prerequisites * The user has [completed onboarding](/developer-guides/user-onboarding/overview) and has the required capabilities enabled. ## Walkthrough ```mermaid theme={null} sequenceDiagram autonumber participant Usr as User participant U as Your App participant B as Your Backend participant P as Payment Widget participant A as Uphold Usr->>U: Start card withdrawal U->>B: List accounts B->>A: GET /core/accounts A-->>B: { accounts } B-->>U: { accounts } Usr->>U: Choose source account U->>B: Create widget session B->>A: Create widget session (select-for-withdrawal) A-->>B: { session } B-->>U: { session } U->>P: Initialize widget Usr->>P: Select card account P-->>U: complete { via, selection } Usr->>U: Choose amount U->>B: Request quote B->>A: Create quote (account → card) A-->>B: { quote } B-->>U: { quote } Usr->>U: Confirm U->>B: Create transaction B->>A: Create transaction A-->>B: { transaction } A-->>B: webhook: transaction.created (processing) A-->>B: webhook: transaction.status-changed (completed/failed) B-->>Usr: Notify the user ``` *** ## Select source account Card withdrawals can be sourced from any account. If the selected account is not in the card's currency, the balance will be converted at the time of the transaction using Uphold's prevailing rate. Make sure the origin asset has the necessary [features enabled](/rest-apis/core-api/assets/introduction#features-and-deposits-/-withdrawals). Call [List accounts](/rest-apis/core-api/accounts/list-accounts) to retrieve the user's accounts and let them pick the one to withdraw from. ```http theme={null} GET /core/accounts ``` ```json theme={null} { "accounts": [ { "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "label": "My GBP account", "asset": "GBP", "balance": { "total": "500.00", "available": "500.00" } } ] } ``` *** ## Link or select a card account The widget session lets the user link a new card or select an existing one. ### Create a widget session Call [Create widget session](/rest-apis/widgets-api/payment/create-session) to start the `select-for-withdrawal` flow. ```http theme={null} POST /widgets/payment/sessions { "flow": "select-for-withdrawal" } ``` ```json theme={null} { "session": { "flow": "select-for-withdrawal", "url": "https://payment.enterprise.uphold.com/", "token": "GEbRxBN...edjnXbL" } } ``` ### Set up the widget ```javascript theme={null} import { PaymentWidget } from '@uphold/enterprise-payment-widget-web-sdk'; const initializeWithdrawalWidget = async (session) => { const widget = new PaymentWidget<'select-for-withdrawal'>(session, { debug: true }); widget.on('complete', (event) => { console.log('Complete', JSON.stringify(event.detail.value)); }); widget.on('cancel', () => { console.log('Cancelled'); widget.unmount(); }); widget.on('error', (event) => { console.error('Error', event.detail.error); widget.unmount(); }); widget.mountIframe(document.getElementById('payment-container')); }; ``` For native apps using a WebView, you'll need a bridge for events as outlined in [Installation & setup](/widgets/payment/installation-and-setup#native-application-integration). ### Handle the complete event The `complete` event fires after the user selects a card. The event payload includes the selected card external account. ```javascript theme={null} widget.on('complete', (event) => { const { via, selection } = event.detail.value; if (via === 'external-account') { // selection is the selected card external account // use selection.id as the destination in the quote handleCardSelected(selection); } widget.unmount(); }); ``` Once you have the selected card, prompt the user to select a source account, then create a quote. ### Handle cancellations ```javascript theme={null} widget.on('cancel', () => { widget.unmount(); // Return the user to the previous screen }); ``` ### Handle errors The `error` event fires for critical unrecoverable errors. Card-specific errors (duplicate card, country mismatch, card limits) are handled by the widget internally. ```javascript theme={null} widget.on('error', (event) => { console.error('Widget error:', event.detail.error); widget.unmount(); // Show a user-friendly error message }); ``` The Payment Widget handles most errors internally. For unrecoverable errors, the widget fires an `error` event. It is the host application's responsibility to handle these events, present an error message to the user, and unmount the widget. *** ## Create a quote Call [Create quote](/rest-apis/core-api/transactions/create-quote) with the origin account and the selected card external account as destination. ```http theme={null} POST /core/transactions/quote { "origin": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8" }, "destination": { "type": "external-account", "id": "7d5928c5-8ac4-4b0d-8b45-f332ba6a9de7" }, "denomination": { "asset": "GBP", "amount": "250.00", "target": "origin" } } ``` A successful response includes the quote details. Present the quote to the user for confirmation before proceeding. ```json [expandable] theme={null} { "quote": { "id": "a91f3c72-1e4b-4c8a-b3e9-9f2d8e4b7c1a", "origin": { "amount": "250.00", "asset": "GBP", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" }, "rate": "1" }, "destination": { "amount": "250.00", "asset": "GBP", "node": { "type": "external-account", "id": "7d5928c5-8ac4-4b0d-8b45-f332ba6a9de7", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" }, "rate": "1" }, "denomination": { "asset": "GBP", "amount": "250.00", "target": "origin", "rate": "1" }, "fees": [], "expiresAt": "2024-07-24T15:22:39Z" } } ``` Quotes typically **expire** quickly. Prompt for user confirmation within the expiry window and requote if needed. *** ## Confirm and create transaction Once the user confirms, call [Create transaction](/rest-apis/core-api/transactions/create-transaction) with the quote ID. ```http theme={null} POST /core/transactions { "quoteId": "a91f3c72-1e4b-4c8a-b3e9-9f2d8e4b7c1a" } ``` In a successful card withdrawal, the origin is the user's `account` and the destination is the `external-account` representing the card. The transaction status is initially `processing` and updates to `completed` once the transfer settles. ```json [expandable] theme={null} { "transaction": { "id": "a1b2c3d4-5e6f-4a8b-9c0d-1e2f3a4b5c6d", "origin": { "asset": "GBP", "amount": "250.00", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "destination": { "asset": "GBP", "amount": "250.00", "node": { "type": "external-account", "id": "7d5928c5-8ac4-4b0d-8b45-f332ba6a9de7", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "status": "completed", "quotedAt": "2025-01-10T14:22:15Z", "createdAt": "2025-01-10T14:22:45Z", "updatedAt": "2025-01-10T14:22:45Z", "denomination": { "asset": "GBP", "amount": "250.00", "target": "origin" } } } ``` *** ## Monitor for settlement Card withdrawal transactions may remain in `processing` while the payment settles. Monitor until the transaction reaches a terminal state. * **Webhook events** (recommended): * [core.transaction.created](/rest-apis/core-api/transactions/webhooks/transaction-created) — `status: processing` → transaction created, pending settlement * [core.transaction.status-changed](/rest-apis/core-api/transactions/webhooks/transaction-status-changed) — `status: completed` → funds settled; `status: failed` → irrecoverable error * **Polling** (fallback): [Get transaction](/rest-apis/core-api/transactions/get-transaction) ## Notify the user Display an in-app confirmation when the transaction is `completed`, and send an email if applicable. You now support card withdrawals via the Payment Widget. # Card withdrawal via the REST API Source: https://developer.uphold.com/developer-guides/card-transfers/withdrawal/via-rest-api Send card withdrawals with the Uphold REST API: link or list a card external account, create a quote, and submit the withdrawal transaction to the user. This guide walks you through supporting card withdrawals using the REST API. ## Prerequisites * The user has [completed onboarding](/developer-guides/user-onboarding/overview) and has the required capabilities enabled. * A **funded account** to debit the funds from. ## Walkthrough ```mermaid theme={null} sequenceDiagram autonumber participant Usr as User participant U as Your App participant B as Your Backend participant A as Uphold Usr->>U: Start card withdrawal U->>B: List accounts B->>A: GET /core/accounts A-->>B: { accounts } B-->>U: { accounts } Usr->>U: Choose source account U->>B: List card accounts B->>A: GET /core/external-accounts A-->>B: { externalAccounts } B-->>U: { externalAccounts } opt No existing card account Usr->>U: Add card details U->>B: Link card account B->>A: Create card external account A-->>B: { externalAccount } end Usr->>U: Choose card and amount alt First time (EMD disclaimer) U->>Usr: Display EMD disclaimer Usr->>U: Acknowledge end U->>B: Request quote B->>A: Create quote (account → card) A-->>B: { quote } B-->>U: { quote } Usr->>U: Confirm U->>B: Create transaction B->>A: Create transaction A-->>B: { transaction } A-->>B: webhook: transaction.created (processing) A-->>B: webhook: transaction.status-changed (completed/failed) B-->>Usr: Notify the user ``` *** ## Check available rails Call [List Rails](/rest-apis/core-api/assets/list-rails) to verify card withdrawal is available. ```http theme={null} GET /core/rails?type=card ``` ```json theme={null} { "rails": [ { "type": "card", "network": "visa", "method": "debit-card", "asset": "GBP", "decimals": 2, "features": [ "deposit", "withdraw" ] } ] } ``` ## Select source account Card withdrawals can be sourced from any account. If the selected account is not in the card's currency, the balance will be converted at the time of the transaction using Uphold's prevailing rate. Make sure the origin asset has the necessary [features enabled](/rest-apis/core-api/assets/introduction#features-and-deposits-/-withdrawals). Call [List accounts](/rest-apis/core-api/accounts/list-accounts) to retrieve the user's accounts and let them pick the one to withdraw from. ```http theme={null} GET /core/accounts ``` ```json theme={null} { "accounts": [ { "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "label": "My GBP account", "asset": "GBP", "balance": { "total": "500.00", "available": "500.00" } } ] } ``` ## Link or select a card account Let the user link a new card or select an existing one. ### Find an existing card account Call [List external accounts](/rest-apis/core-api/external-accounts/list-external-accounts) to fetch the user's saved cards. ```http theme={null} GET /core/external-accounts ``` Make sure the selected card has `status: "ok"` and in `features`. ```json theme={null} { "externalAccounts": [ { "id": "7d5928c5-8ac4-4b0d-8b45-f332ba6a9de7", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "type": "card", "status": "ok", "label": "My Visa Card", "asset": "GBP", "network": "visa", "features": [ "deposit", "withdraw" ], "details": { "type": "debit", "last4Digits": "5119", "expiryDate": { "month": 12, "year": 2028 }, "octSupport": "supported" } } ] } ``` ### Link a new card account If the user wants to use a card not yet on file, call [Create external account](/rest-apis/core-api/external-accounts/create-external-account) with the card details. ```http theme={null} POST /core/external-accounts { "type": "card", "label": "My Visa Card", "number": "4921817844445119", "securityCode": "123", "expiryDate": { "month": 12, "year": 2028 } } ``` The response initially returns `status: "processing"` while the card is validated. ```json theme={null} { "externalAccount": { "id": "7d5928c5-8ac4-4b0d-8b45-f332ba6a9de7", "type": "card", "status": "processing", "label": "My Visa Card" } } ``` Once validated, the status transitions to `ok` and the full details are available. ```json theme={null} { "externalAccount": { "id": "7d5928c5-8ac4-4b0d-8b45-f332ba6a9de7", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "type": "card", "status": "ok", "label": "My Visa Card", "asset": "GBP", "network": "visa", "features": [ "deposit", "withdraw" ], "details": { "type": "debit", "last4Digits": "5119", "expiryDate": { "month": 12, "year": 2028 }, "octSupport": "supported" } } } ``` Monitor the external account status via [Get external account](/rest-apis/core-api/external-accounts/get-external-account) or the `external-account.status-changed` webhook until `status` is `ok` before proceeding. ### EMD disclaimer This disclaimer is required to comply with FCA regulations. Display it to users based in GB, the first time they link a credit/debit card or generate bank deposit details — it only needs to be shown once: > Uphold Europe Limited is an EMD Agent of Optimus Cards UK Limited (FRN: 902034). All received funds are held in a designated safekeeping account with a regulated bank and kept separate from Uphold's own assets. These funds are not protected by the UK Financial Services Compensation Scheme. ## Create a quote Call [Create quote](/rest-apis/core-api/transactions/create-quote) with the origin account and the card external account as destination. ```http theme={null} POST /core/transactions/quote { "origin": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8" }, "destination": { "type": "external-account", "id": "7d5928c5-8ac4-4b0d-8b45-f332ba6a9de7" }, "denomination": { "asset": "GBP", "amount": "250.00", "target": "origin" } } ``` ```json [expandable] theme={null} { "quote": { "id": "a91f3c72-1e4b-4c8a-b3e9-9f2d8e4b7c1a", "origin": { "amount": "250.00", "asset": "GBP", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" }, "rate": "1" }, "destination": { "amount": "250.00", "asset": "GBP", "node": { "type": "external-account", "id": "7d5928c5-8ac4-4b0d-8b45-f332ba6a9de7", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" }, "rate": "1" }, "denomination": { "asset": "GBP", "amount": "250.00", "target": "origin", "rate": "1" }, "fees": [], "expiresAt": "2024-07-24T15:22:39Z" } } ``` Quotes typically **expire** quickly. Prompt for user confirmation within the expiry window and requote if needed. ## Confirm and create transaction Once the user confirms, call [Create transaction](/rest-apis/core-api/transactions/create-transaction) with the quote ID. ```http theme={null} POST /core/transactions { "quoteId": "a91f3c72-1e4b-4c8a-b3e9-9f2d8e4b7c1a" } ``` In a successful card withdrawal, the origin is the user's `account` and the destination is the `external-account` representing the card. The transaction status is initially `processing` and updates to `completed` once the transfer settles. ```json [expandable] theme={null} { "transaction": { "id": "a1b2c3d4-5e6f-4a8b-9c0d-1e2f3a4b5c6d", "origin": { "asset": "GBP", "amount": "250.00", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "destination": { "asset": "GBP", "amount": "250.00", "node": { "type": "external-account", "id": "7d5928c5-8ac4-4b0d-8b45-f332ba6a9de7", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "status": "completed", "quotedAt": "2025-01-10T14:22:15Z", "createdAt": "2025-01-10T14:22:45Z", "updatedAt": "2025-01-10T14:22:45Z", "denomination": { "asset": "GBP", "amount": "250.00", "target": "origin" } } } ``` ## Notify the user Display an in-app confirmation when the transaction is `completed`, and send an email if applicable. You now support card withdrawals via the REST API. # Crypto deposit via the Payment Widget Source: https://developer.uphold.com/developer-guides/crypto-transfers/deposit/via-payment-widget Accept crypto deposits with the Uphold Payment Widget for network selection and address display, then monitor incoming transactions on your backend. The Payment Widget handles crypto network selection and displays the deposit address to the user. Your backend only needs to create the session and monitor for the incoming transfer. The Payment Widget does not create any transaction. Monitoring and processing the incoming transfer must be handled by your backend via webhooks or polling. ## Prerequisites * The user has [completed onboarding](/developer-guides/user-onboarding/overview) and has the required capabilities enabled. ## Walkthrough ```mermaid theme={null} sequenceDiagram autonumber participant Usr as User participant U as Your App participant B as Your Backend participant P as Payment Widget participant W as Travel Rule Widget participant A as Uphold participant C as Blockchain Network Usr->>U: Request deposit instructions U->>B: Create widget session B->>A: Create widget session (select-for-deposit) A-->>B: { session } B-->>U: { session } U->>P: Initialize widget Usr->>P: Select deposit method P-->>Usr: Display crypto deposit instructions Usr->>C: Send crypto transfer C-->>A: Incoming transfer received A-->>B: webhook: transaction.created (processing) C-->>A: Confirmations reached alt on-hold (pending RFI) A-->>B: webhook: transaction.status-changed (on-hold) B->>A: GET /core/transactions/{id}/requests-for-information A-->>B: { requestsForInformation } B->>A: POST /widgets/travel-rule/sessions A-->>B: { session } B-->>U: { session } U->>W: Initialize widget Usr->>W: Submit Travel Rule information W-->>U: complete { travelRule } U->>B: { travelRule } B->>A: PATCH /core/transactions/{id}/requests-for-information/{rfiId} A-->>B: RFI resolved end A-->>B: webhook: transaction.status-changed (completed/failed) B-->>Usr: Notify the user ``` *** ## Select deposit method The widget lets the user select a crypto network and view the deposit address. ### Create a widget session Call the [Create widget session](/rest-apis/widgets-api/payment/create-session) endpoint to create a session for the `select-for-deposit` flow. ```http theme={null} POST /widgets/payment/sessions { "flow": "select-for-deposit" } ``` A successful response returns a `session` object with a `token` and `url` to initialize the widget. ```json theme={null} { "session": { "flow": "select-for-deposit", "url": "https://payment.enterprise.uphold.com/", "token": "GEbRxBN...edjnXbL" } } ``` ### Set up the widget Initialize the widget for the `select-for-deposit` flow using the session data returned from the API. ```javascript theme={null} import { PaymentWidget } from '@uphold/enterprise-payment-widget-web-sdk'; const initializeDepositWidget = async (session) => { const widget = new PaymentWidget<'select-for-deposit'>(session, { debug: true }); widget.on('ready', () => { console.log('Ready'); }); widget.on('complete', (event) => { console.log('Complete', JSON.stringify(event.detail.value)); }); widget.on('cancel', () => { console.log('Cancelled'); widget.unmount(); }); widget.on('error', (event) => { console.error('Error', event.detail.error); widget.unmount(); }); widget.mountIframe(document.getElementById('payment-container')); }; ``` The example above is for web applications. For native apps using a WebView, you'll need a bridge for events as outlined in [Installation & setup](/widgets/payment/installation-and-setup#native-application-integration). ### Handle the complete event The `complete` event fires when the user selects a deposit method and the widget displays the deposit address. ```javascript theme={null} widget.on('complete', (event) => { const { via, selection } = event.detail.value; if (via === 'deposit-method') { const { depositMethod, account } = selection; if (depositMethod.type === 'crypto') { handleCryptoDeposit(depositMethod, account); } } widget.unmount(); }); ``` The event payload: * `via` — set to `deposit-method` when the user completes the crypto network selection. * `selection.account` — the account that will receive the deposit. * `selection.depositMethod` — the deposit method with crypto transfer instructions. The widget presents the deposit address and reference directly to the user. The `depositMethod.details` contains: ```json theme={null} { "type": "crypto", "status": "ok", "details": { "network": "xrp-ledger", "asset": "XRP", "address": "rfBtmHiLwwWH5maH2PT78GxubrSydRF9aY", "reference": "3457810109" } } ``` ### Handle cancellations The `cancel` event fires when the user closes the widget without completing the selection. ```javascript theme={null} widget.on('cancel', () => { widget.unmount(); // Redirect back or show a cancellation message }); ``` ### Handle errors The `error` event fires when an error occurs during the flow. ```javascript theme={null} widget.on('error', (event) => { console.error('Widget error:', event.detail.error); widget.unmount(); // Show a user-friendly error message }); ``` ## Monitor for the incoming transfer The widget presents the deposit instructions to the user but does not monitor for the incoming transfer. Your application must do this via webhooks or polling. Prefer **webhooks** for real-time updates, or fall back to **polling** if webhooks are not feasible. * Webhook events (recommended): * [core.transaction.created](/rest-apis/core-api/transactions/webhooks/transaction-created) * `status: processing` → detected on-chain but not yet confirmed * [core.transaction.status-changed](/rest-apis/core-api/transactions/webhooks/transaction-status-changed) * `status: completed` → necessary confirmations reached * `status: on-hold` → transaction checks paused (e.g., pending RFIs) * `status: failed` → irrecoverable error * Polling (fallback): [Get transaction](/rest-apis/core-api/transactions/get-transaction) ## Sample transaction In a successful crypto deposit, the origin is represented as a `crypto-address` node reflecting the sender's on-chain address. The destination is the account that was set up to receive the deposit. ```json [expandable] theme={null} { "transaction": { "id": "223c24c5-76c6-4553-91bc-5af519441f03", "origin": { "asset": "BTC", "amount": "0.00121023", "rate": "1.00", "node": { "type": "crypto-address", "network": "bitcoin", "address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", "execution": { "mode": "onchain", "transactionHash": "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b" } } }, "destination": { "asset": "BTC", "amount": "0.00121023", "rate": "1.00", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "fees": [], "status": "completed", "quotedAt": "2024-07-24T15:02:39Z", "createdAt": "2024-07-24T15:22:39Z", "updatedAt": "2024-07-24T15:33:08Z", "denomination": { "asset": "BTC", "amount": "0.00121023", "rate": "1.00", "target": "origin" } } } ``` ### Execution modes Crypto deposits are processed using different execution modes depending on the environment and configuration: * **On-chain execution**: The transaction is processed directly on the blockchain network. The `origin.node.execution.mode` will be `onchain` and include a `transactionHash` as shown in the example above. * **Off-chain execution**: When both the sender and recipient are Uphold users, the transaction is processed internally within Uphold's infrastructure. This eliminates network fees and is faster than on-chain processing. The `origin.node.execution.mode` will be `offchain`. The `execution` object for these transactions includes additional properties: `accountOwnerId` (the sender user ID) and `accountId` (the sender account ID). * **Simulated execution (Test Helpers)**: Used in development environments for testing purposes ([Simulate crypto deposit](/rest-apis/core-api/accounts/test-helpers/simulate-crypto-deposit)). The transaction appears as processed but does not affect actual blockchain state (user balances will be affected though). The `origin.node.execution.mode` will be `simulated`. ## Handle on-hold transactions If the crypto deposit is placed `on-hold` with reason `pending-requests-for-information`, resolve the pending RFIs before the deposit can complete. ### List pending RFIs Call [List requests for information](/rest-apis/core-api/transactions/rfis/list-requests-for-information) to retrieve the pending RFIs for the transaction. ```http theme={null} GET /core/transactions/{transactionId}/requests-for-information ``` ```json theme={null} { "requestsForInformation": [ { "id": "3f6d0c1e-a1bf-4b25-9802-2a3ee492d3c8", "type": "travel-rule", "status": "pending", "data": {}, "createdAt": "2024-07-24T15:22:39Z", "updatedAt": "2024-07-24T15:22:39Z" } ] } ``` ### Travel Rule Travel Rule is a regulatory requirement that mandates the collection and transmission of information about the originator and beneficiary of certain crypto transactions. Use the [Travel Rule widget](/widgets/travel-rule) to collect the required information. #### Create the widget session Call [Create session](/rest-apis/widgets-api/travel-rule/create-session) with `"flow": "deposit-form"` and the RFI id. ```http theme={null} POST /widgets/travel-rule/sessions { "flow": "deposit-form", "data": { "requestForInformationId": "3f6d0c1e-a1bf-4b25-9802-2a3ee492d3c8" } } ``` ```json theme={null} { "session": { "flow": "deposit-form", "url": "https://widgets.uphold.com/travel-rule/sessions/xyz789", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "data": { "provider": "notabene", "parameters": {} } } } ``` #### Open the widget The example below is for web applications. For native apps using a WebView, see [Native app integration](/widgets/travel-rule/installation-and-setup#native-app-integration). ```javascript theme={null} import { TravelRuleWidget } from '@uphold/enterprise-travel-rule-widget-web-sdk'; const widget = new TravelRuleWidget<'deposit-form'>(session, { debug: true }); widget.on('complete', (event) => { const { value: travelRule } = event.detail; // Send travelRule data to your backend to update the RFI widget.unmount(); }); widget.on('cancel', error => ...); widget.on('error', error => ...); widget.mountIframe(document.getElementById('tr-deposit')); ``` #### Update the RFI Once the widget emits `complete`, call [Update request for information](/rest-apis/core-api/transactions/rfis/update-request-for-information) with the Travel Rule data. ```http theme={null} PUT /core/transactions/{transactionId}/requests-for-information/{requestForInformationId} { "data": { // Travel Rule data from complete event } } ``` ```json theme={null} { "requestForInformation": { "id": "3f6d0c1e-a1bf-4b25-9802-2a3ee492d3c8", "type": "travel-rule", "status": "ok", "data": {}, "createdAt": "2024-07-24T15:22:39Z", "updatedAt": "2024-07-24T15:25:12Z" } } ``` Once the RFI is resolved, the transaction resumes processing automatically. ## Notify the user Display an in-app confirmation when the transaction is `completed`, and send an email if applicable. You now support crypto deposits via the Payment Widget. # Crypto deposit via the REST API Source: https://developer.uphold.com/developer-guides/crypto-transfers/deposit/via-rest-api Support crypto deposits with the Uphold REST API: configure the deposit method, generate a network address, and monitor for incoming on-chain transfers. This guide walks you through supporting crypto deposits using the REST API. ## Prerequisites * The user has [completed onboarding](/developer-guides/user-onboarding/overview) and has the required capabilities enabled. Some networks require **destination tags/memos** (e.g., XRP, XLM). If a `reference` is returned with the `address`, you can check its type by calling [Get network](/rest-apis/core-api/assets/get-network) endpoint. Educate the user on the importance of providing this information when making the deposit, as it is essential for crediting their account correctly. ## Walkthrough ```mermaid theme={null} sequenceDiagram autonumber participant Usr as User participant U as Your App participant B as Your Backend participant W as Travel Rule Widget participant A as Uphold participant C as Blockchain Network Usr->>U: Request deposit instructions U->>B: List accounts B->>A: GET /core/accounts A-->>B: { accounts } B-->>U: { accounts } Usr->>U: Choose destination account U->>B: Generate deposit method B->>A: PUT /core/accounts/{accountId}/deposit-method A-->>B: { depositMethod } B-->>U: { depositMethod } U-->>Usr: Display deposit instructions Usr->>C: Send crypto transfer C-->>A: Detect inbound tx to address A-->>B: webhook: transaction.created (processing) C-->>A: Confirmations reached alt on-hold (pending RFI) A-->>B: webhook: transaction.status-changed (on-hold) B->>A: GET /core/transactions/{id}/requests-for-information A-->>B: { requestsForInformation } B->>A: POST /widgets/travel-rule/sessions A-->>B: { session } B-->>U: { session } U->>W: Initialize widget Usr->>W: Submit Travel Rule information W-->>U: complete { travelRule } U->>B: { travelRule } B->>A: PATCH /core/transactions/{id}/requests-for-information/{rfiId} A-->>B: RFI resolved end A-->>B: webhook: transaction.status-changed (completed/failed) B-->>Usr: Notify the user ``` ## Check available rails Call [List rails](/rest-apis/core-api/assets/list-rails) to confirm the asset and network the user wants to deposit from, and verify the `deposit` feature is enabled. ```http theme={null} GET /core/rails?type=crypto&asset=BTC ``` A successful response lists all rails for the asset. Each rail includes a `features` array — only proceed with networks that include `"deposit"`. For assets supported by multiple networks, require an explicit network selection from the user. ```json theme={null} { "rails": [ { "type": "crypto", "network": "bitcoin", "method": "crypto-transaction", "asset": "BTC", "decimals": 8, "features": [ "deposit", "withdraw" ] }, { "type": "crypto", "network": "lightning", "method": "crypto-transaction", "asset": "BTC", "decimals": 8, "features": [] } ] } ``` ## Select destination account Crypto deposits can target any account. If the selected account is not in the deposited asset, the amount will be converted at settlement using Uphold's prevailing rate. Make sure the destination asset has the necessary [features enabled](/rest-apis/core-api/assets/introduction#features-and-deposits-/-withdrawals). ### Find an existing account Call [List accounts](/rest-apis/core-api/accounts/list-accounts) to retrieve the user's accounts and let them pick the one they want to fund. ```http theme={null} GET /core/accounts ``` ```json theme={null} { "accounts": [ { "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "label": "My BTC account", "asset": "BTC", "balance": { "total": "0.05", "available": "0.05" } } ] } ``` ### Create a new account If the user has no accounts, create one with [Create account](/rest-apis/core-api/accounts/create-account) before proceeding. ```http theme={null} POST /core/accounts { "label": "My BTC account", "asset": "BTC" } ``` ```json theme={null} { "account": { "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "label": "My BTC account", "asset": "BTC", "balance": { "total": "0", "available": "0" } } } ``` ## Generate deposit method Call [Set up account deposit method](/rest-apis/core-api/accounts/set-up-account-deposit-method) with the target account id, asset, and network. ```http theme={null} PUT /core/accounts/{accountId}/deposit-method?type=crypto&asset=BTC&network=bitcoin ``` A successful response includes the `address` and, if applicable, a `reference` (e.g., destination tag, memo). ```json theme={null} { "depositMethod": { "type": "crypto", "status": "ok", "details": { "network": "bitcoin", "asset": "BTC", "address": "tb1qgu0gacn9pqpnvlqclvdwyz4gfgxz8pptfz4emt" } } } ``` The deposit method may initially return `status: processing` while the address is being prepared. Call [Get account deposit method](/rest-apis/core-api/accounts/get-account-deposit-method) to confirm it is ready (`status: ok`) before displaying instructions to the user. Render the address clearly and, if a `reference` is present, display it prominently and treat it as required input. We suggest also displaying a QR code to reduce input errors. ## Monitor for the incoming transfer Prefer **webhooks** for real-time updates, or fall back to **polling** if webhooks are not feasible. * Webhook events (recommended): * [core.transaction.created](/rest-apis/core-api/transactions/webhooks/transaction-created) * `status: processing` → detected on-chain but not yet confirmed * [core.transaction.status-changed](/rest-apis/core-api/transactions/webhooks/transaction-status-changed) * `status: completed` → necessary confirmations reached * `status: on-hold` → transaction checks paused (e.g., pending RFIs) * `status: failed` → irrecoverable error * Polling (fallback): [Get transaction](/rest-apis/core-api/transactions/get-transaction) ## Sample transaction In a successful crypto deposit, the origin is represented as a `crypto-address` node reflecting the sender's on-chain address. The destination is the account that was set up to receive the deposit. ```json [expandable] theme={null} { "transaction": { "id": "223c24c5-76c6-4553-91bc-5af519441f03", "origin": { "asset": "BTC", "amount": "0.00121023", "rate": "1.00", "node": { "type": "crypto-address", "network": "bitcoin", "address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", "execution": { "mode": "onchain", "transactionHash": "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b" } } }, "destination": { "asset": "BTC", "amount": "0.00121023", "rate": "1.00", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "fees": [], "status": "completed", "quotedAt": "2024-07-24T15:02:39Z", "createdAt": "2024-07-24T15:22:39Z", "updatedAt": "2024-07-24T15:33:08Z", "denomination": { "asset": "BTC", "amount": "0.00121023", "rate": "1.00", "target": "origin" } } } ``` ### Execution modes Crypto deposits are processed using different execution modes depending on the environment and configuration: * **On-chain execution**: The transaction is processed directly on the blockchain network. The `origin.node.execution.mode` will be `onchain` and include a `transactionHash` as shown in the example above. * **Off-chain execution**: When both the sender and recipient are Uphold users, the transaction is processed internally within Uphold's infrastructure. This eliminates network fees and is faster than on-chain processing. The `origin.node.execution.mode` will be `offchain`. The `execution` object for these transactions includes additional properties: `accountOwnerId` (the sender user ID) and `accountId` (the sender account ID). * **Simulated execution (Test Helpers)**: Used in development environments for testing purposes ([Simulate crypto deposit](/rest-apis/core-api/accounts/test-helpers/simulate-crypto-deposit)). The transaction appears as processed but does not affect actual blockchain state (user balances will be affected though). The `origin.node.execution.mode` will be `simulated`. ## Handle on-hold transactions If the crypto deposit is placed `on-hold` with reason `pending-requests-for-information`, resolve the pending RFIs before the deposit can complete. ### List pending RFIs Call [List requests for information](/rest-apis/core-api/transactions/rfis/list-requests-for-information) to retrieve the pending RFIs for the transaction. ```http theme={null} GET /core/transactions/{transactionId}/requests-for-information ``` ```json theme={null} { "requestsForInformation": [ { "id": "3f6d0c1e-a1bf-4b25-9802-2a3ee492d3c8", "type": "travel-rule", "status": "pending", "data": {}, "createdAt": "2024-07-24T15:22:39Z", "updatedAt": "2024-07-24T15:22:39Z" } ] } ``` ### Travel Rule Travel Rule is a regulatory requirement that mandates the collection and transmission of information about the originator and beneficiary of certain crypto transactions. Use the [Travel Rule widget](/widgets/travel-rule) to collect the required information. #### Create the widget session Call [Create session](/rest-apis/widgets-api/travel-rule/create-session) with `"flow": "deposit-form"` and the RFI id. ```http theme={null} POST /widgets/travel-rule/sessions { "flow": "deposit-form", "data": { "requestForInformationId": "3f6d0c1e-a1bf-4b25-9802-2a3ee492d3c8" } } ``` ```json theme={null} { "session": { "flow": "deposit-form", "url": "https://widgets.uphold.com/travel-rule/sessions/xyz789", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "data": { "provider": "notabene", "parameters": {} } } } ``` #### Open the widget The example below is for web applications. For native apps using a WebView, see [Native app integration](/widgets/travel-rule/installation-and-setup#native-app-integration). ```javascript theme={null} import { TravelRuleWidget } from '@uphold/enterprise-travel-rule-widget-web-sdk'; const widget = new TravelRuleWidget<'deposit-form'>(session, { debug: true }); widget.on('complete', (event) => { const { value: travelRule } = event.detail; // Send travelRule data to your backend to update the RFI widget.unmount(); }); widget.on('cancel', error => ...); widget.on('error', error => ...); widget.mountIframe(document.getElementById('tr-deposit')); ``` #### Update the RFI Once the widget emits `complete`, call [Update request for information](/rest-apis/core-api/transactions/rfis/update-request-for-information) with the Travel Rule data. ```http theme={null} PUT /core/transactions/{transactionId}/requests-for-information/{requestForInformationId} { "data": { // Travel Rule data from complete event } } ``` ```json theme={null} { "requestForInformation": { "id": "3f6d0c1e-a1bf-4b25-9802-2a3ee492d3c8", "type": "travel-rule", "status": "ok", "data": {}, "createdAt": "2024-07-24T15:22:39Z", "updatedAt": "2024-07-24T15:25:12Z" } } ``` Once the RFI is resolved, the transaction resumes processing automatically. ## Notify the user Display an in-app confirmation when the transaction is `completed`, and send an email if applicable. You now support crypto deposits with the Enterprise API Suite. # Crypto transfer integration overview (50+ blockchains) Source: https://developer.uphold.com/developer-guides/crypto-transfers/overview Build native crypto on/off-ramps across 50+ blockchain networks, with on-chain, off-chain, and simulated execution modes for deposits and withdrawals. Power native crypto on/off-ramp experiences across 50+ blockchain networks. ## Key concepts * **Deposits** are detected automatically when incoming funds arrive at the generated deposit address. No quote is required. * **Withdrawals** are quote-based — the user must confirm a quote before the transaction is created and broadcast. * **Execution modes** — transactions can be processed on-chain, off-chain (between Uphold users), or in simulated mode (sandbox only). * **Transaction rules** — some quotes may include requirements (e.g., Travel Rule) that must be resolved before creating the transaction. On-hold transactions may require pending RFIs to be resolved. ## Start building Generate a deposit address and monitor for incoming on-chain transfers. Let users select a crypto network and receive deposit instructions with a low-code, embeddable widget. Create and broadcast a crypto withdrawal to an external address. Let users select a network and enter their address with a low-code, embeddable widget. # Crypto withdrawal via the Payment Widget Source: https://developer.uphold.com/developer-guides/crypto-transfers/withdrawal/via-payment-widget Send crypto withdrawals with the Uphold Payment Widget for asset, network and address collection, then create the quote and transaction via the REST API. The Payment Widget handles crypto asset, network and address collection for withdrawals. Your backend creates the session, then continues with the REST API to create a quote and transaction once the user has provided the destination details. The Payment Widget does not create any transaction. Transaction creation must be handled by your backend via the REST API. ## Prerequisites * The user has [completed onboarding](/developer-guides/user-onboarding/overview) and has the required capabilities enabled. ## Walkthrough ```mermaid theme={null} sequenceDiagram autonumber participant Usr as User participant U as Your App participant B as Your Backend participant P as Payment Widget participant A as Uphold participant N as Blockchain Network Usr->>U: Start crypto withdrawal U->>B: List accounts B->>A: GET /core/accounts A-->>B: { accounts } B-->>U: { accounts } Usr->>U: Choose source account U->>B: Create widget session B->>A: Create widget session (select-for-withdrawal) A-->>B: { session } B-->>U: { session } U->>P: Initialize widget Usr->>P: Select asset, network and enter address P-->>U: complete { via: "crypto-network", selection } U->>B: Request quote B->>A: Create quote A-->>B: { quote } B-->>U: { quote } Usr->>U: Confirm quote U->>B: Create transaction B->>A: Create transaction A-->>B: { transaction } A-->>B: webhook: transaction.created (processing) A->>N: Broadcast withdrawal N-->>A: Confirmations reached A-->>B: webhook: transaction.status-changed (completed/failed) B-->>Usr: Notify the user ``` *** ## Select source account Crypto withdrawals can be sourced from any account. If the selected account is not in the withdrawal asset, the balance will be converted at the time of the transaction using Uphold's prevailing rate. Make sure the origin asset has the necessary [features enabled](/rest-apis/core-api/assets/introduction#features-and-deposits-/-withdrawals). Call [List accounts](/rest-apis/core-api/accounts/list-accounts) to retrieve the user's accounts and let them choose one with sufficient balance for the withdrawal. ```http theme={null} GET /core/accounts?currency=BTC ``` ```json theme={null} { "accounts": [ { "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "label": "My BTC account", "asset": "BTC", "balance": { "total": "0.05", "available": "0.05" } } ] } ``` *** ## Set a crypto destination The widget lets the user select a crypto asset, choose a network and enter the destination address. ### Create a widget session Call the [Create widget session](/rest-apis/widgets-api/payment/create-session) endpoint to create a session for the `select-for-withdrawal` flow. ```http theme={null} POST /widgets/payment/sessions { "flow": "select-for-withdrawal" } ``` A successful response returns a `session` object with a `token` and `url` to initialize the widget. ```json theme={null} { "session": { "flow": "select-for-withdrawal", "url": "https://payment.enterprise.uphold.com/", "token": "GEbRxBN...edjnXbL" } } ``` ### Set up the widget Initialize the widget for the `select-for-withdrawal` flow using the session data returned from the API. ```javascript theme={null} import { PaymentWidget } from '@uphold/enterprise-payment-widget-web-sdk'; const initializeWithdrawalWidget = async (session) => { const widget = new PaymentWidget<'select-for-withdrawal'>(session, { debug: true }); widget.on('ready', () => { console.log('Ready'); }); widget.on('complete', (event) => { console.log('Complete', JSON.stringify(event.detail.value)); }); widget.on('cancel', () => { console.log('Cancelled'); widget.unmount(); }); widget.on('error', (event) => { console.error('Error', event.detail.error); widget.unmount(); }); widget.mountIframe(document.getElementById('payment-container')); }; ``` The example above is for web applications. For native apps using a WebView, you'll need a bridge for events as outlined in [Installation & setup](/widgets/payment/installation-and-setup#native-application-integration). ### Handle the complete event The widget lets the user select a crypto asset, choose a network and enter a destination address. If the network requires a destination tag or memo, the widget prompts for it and warns the user if it's missing. When the user completes the selection, the `complete` event fires with `via: "crypto-network"`. ```javascript theme={null} widget.on('complete', (event) => { const { via, selection } = event.detail.value; if (via === 'crypto-network') { handleCryptoWithdrawal(selection); } widget.unmount(); }); ``` The event payload: * `via` — set to `crypto-network` when the user provides a crypto withdrawal address. * `selection.asset` — the selected crypto asset code (e.g. `BTC`, `XRP`). * `selection.network` — the selected blockchain network (e.g. `bitcoin`, `xrp-ledger`). * `selection.address` — the destination wallet address. * `selection.reference` — the destination tag or memo, if required by the network. ```json theme={null} { "via": "crypto-network", "selection": { "asset": "XRP", "network": "xrp-ledger", "address": "rPjTZfLP3Qxwwd2xvXSALJzEFmmf7bEYgh", "reference": "12345678" } } ``` ### Handle cancellations The `cancel` event fires when the user closes the widget without completing the selection. ```javascript theme={null} widget.on('cancel', () => { widget.unmount(); // Redirect back or show a cancellation message }); ``` ### Handle errors The `error` event fires when an error occurs during the flow. ```javascript theme={null} widget.on('error', (event) => { console.error('Widget error:', event.detail.error); widget.unmount(); // Show a user-friendly error message }); ``` ## Create a quote Use the `selection` data from the widget to create a quote via the [Create quote](/rest-apis/core-api/transactions/create-quote) endpoint. ```http theme={null} POST /core/transactions/quote { "origin": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8" }, "destination": { "type": "crypto-address", "asset": "XRP", "network": "xrp-ledger", "address": "rPjTZfLP3Qxwwd2xvXSALJzEFmmf7bEYgh", "reference": "12345678" }, "denomination": { "asset": "XRP", "amount": "10.00", "target": "origin" } } ``` Quotes typically **expire** quickly. Prompt for user confirmation within the expiry window and requote if needed. ## Handle quote requirements When the quote is returned, check the `requirements` array. If non-empty, resolve each requirement before creating the transaction. ```json theme={null} { "quote": { "id": "623000c8-9bdf-4a2b-aa3d-6a6b44a7f6a0", "requirements": [ "travel-rule" ], "expiresAt": "2024-07-24T15:22:39Z" } } ``` ### Travel Rule Travel Rule is a regulatory requirement that mandates the collection and transmission of information about the originator and beneficiary of certain crypto transactions. Use the [Travel Rule widget](/widgets/travel-rule) to collect the required information. #### Create a widget session Call [Create session](/rest-apis/widgets-api/travel-rule/create-session) with `"flow": "withdrawal-form"` and the quote id. ```http theme={null} POST /widgets/travel-rule/sessions { "flow": "withdrawal-form", "data": { "quoteId": "623000c8-9bdf-4a2b-aa3d-6a6b44a7f6a0" } } ``` A successful response returns a `session` object with a `token` and `url` to initialize the widget. ```json theme={null} { "session": { "flow": "withdrawal-form", "url": "https://widgets.uphold.com/travel-rule/sessions/abc123", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "data": { "provider": "notabene", "parameters": {} } } } ``` #### Set up the widget Initialize the widget for the `withdrawal-form` flow using the session data returned from the API. ```javascript theme={null} import { TravelRuleWidget } from '@uphold/enterprise-travel-rule-widget-web-sdk'; const initializeTravelRuleWidget = async (session) => { const widget = new TravelRuleWidget<'withdrawal-form'>(session, { debug: true }); widget.on('ready', () => { console.log('Ready'); }); widget.on('complete', (event) => { console.log('Complete', JSON.stringify(event.detail.value)); }); widget.on('cancel', () => { console.log('Cancelled'); widget.unmount(); }); widget.on('error', (event) => { console.error('Error', event.detail.error); widget.unmount(); }); widget.mountIframe(document.getElementById('travel-rule-container')); }; ``` The example above is for web applications. For native apps using a WebView, you'll need a bridge for events as outlined in [Installation & setup](/widgets/travel-rule/installation-and-setup#native-app-integration). #### Handle the complete event The `complete` event fires when the user successfully submits the Travel Rule form. Send the `travelRule` data to your backend to include when creating the transaction. ```javascript theme={null} widget.on('complete', (event) => { const { value: travelRule } = event.detail; sendToBackend({ travelRule }); widget.unmount(); }); ``` #### Handle cancellations The `cancel` event fires when the user closes the widget without completing the form. ```javascript theme={null} widget.on('cancel', () => { widget.unmount(); // Redirect back or show a cancellation message }); ``` #### Handle errors The `error` event fires when an unrecoverable error occurs. ```javascript theme={null} widget.on('error', (event) => { console.error('Widget error:', event.detail.error); widget.unmount(); // Show a user-friendly error message }); ``` ## Create a transaction Once the user confirms the quote, create the transaction using the [Create transaction](/rest-apis/core-api/transactions/create-transaction) endpoint. If the Travel Rule widget emitted a `complete` event, include the `travelRule` data in `params`. If Travel Rule was required, the original quote may have expired while the user was completing the form. If so, create a new quote before proceeding — the Travel Rule data remains valid and will automatically apply to the new quote. ```http theme={null} POST /core/transactions { "quoteId": "623000c8-9bdf-4a2b-aa3d-6a6b44a7f6a0", "params": { "travelRule": { // Travel Rule data from widget complete event — omit if not required } } } ``` In a successful crypto withdrawal, the origin is the source account and the destination is a `crypto-address` node reflecting the recipient's on-chain address. ```json [expandable] theme={null} { "transaction": { "id": "223c24c5-76c6-4553-91bc-5af519441f03", "origin": { "amount": "0.00121023", "asset": "BTC", "rate": "0.00002629253259492961", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "destination": { "amount": "0.00121023", "asset": "BTC", "rate": "1", "node": { "type": "crypto-address", "address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", "network": "bitcoin", "execution": { "mode": "onchain" } } }, "fees": [], "status": "processing", "quotedAt": "2024-07-24T15:02:39Z", "createdAt": "2024-07-24T15:22:39Z", "updatedAt": "2024-07-24T15:22:39Z", "denomination": { "amount": "100.00", "asset": "GBP", "target": "origin", "rate": "0.00001210225938333485" } } } ``` ### Execution modes Crypto withdrawals support different execution modes depending on the destination and environment: * **On-chain execution**: The transaction is processed directly on the blockchain network. The `destination.node.execution.mode` will be `onchain` and include blockchain-specific details such as transaction hashes upon completion. * **Off-chain execution**: When both the sender and recipient are Uphold users, the transaction is processed internally within Uphold's infrastructure. This eliminates network fees and is faster than onchain processing. The `destination.node.execution.mode` will be `offchain`. For offchain transactions, the `execution` object includes additional properties: `accountOwnerId` (the recipient user ID) and `accountId` (the recipient account ID). * **Simulated execution**: Used in development environments for testing purposes. The transaction appears processed but does not affect actual blockchain state (user balances will be affected though). The `destination.node.execution.mode` will be `simulated`. ## Monitor for settlement Prefer **webhooks** for real-time updates, or fall back to **polling** if webhooks are not feasible. * Webhook events (recommended): * [core.transaction.created](/rest-apis/core-api/transactions/webhooks/transaction-created) * `status: processing` → initiated but not yet broadcast * [core.transaction.status-changed](/rest-apis/core-api/transactions/webhooks/transaction-status-changed) * `status: completed` → broadcast and confirmed * `status: on-hold` → transaction checks paused (e.g., pending RFIs) * `status: failed` → transaction failed, check `statusDetails` for more info * Polling (fallback): [Get transaction](/rest-apis/core-api/transactions/get-transaction) ## Notify the user Display an in-app confirmation when the transaction is `completed`, and send an email if applicable. You now support crypto withdrawals via the Payment Widget. # Crypto withdrawal via the REST API Source: https://developer.uphold.com/developer-guides/crypto-transfers/withdrawal/via-rest-api Send crypto withdrawals with the Uphold REST API: collect destination details, create a quote, handle requirements, and monitor for settlement. This guide walks you through supporting crypto withdrawals using the REST API. ## Prerequisites * The user has [completed onboarding](/developer-guides/user-onboarding/overview) and has the required capabilities enabled. If the destination network requires an **additional reference** (e.g., destination tag, memo), strongly encourage your users to provide it. Missing references often lead to loss of funds or lengthy recovery. ## Walkthrough ```mermaid theme={null} sequenceDiagram autonumber participant Usr as User participant U as Your App participant B as Your Backend participant A as Uphold participant N as Blockchain Network Usr->>U: Start crypto withdrawal U->>B: List accounts B->>A: GET /core/accounts A-->>B: { accounts } B-->>U: { accounts } Usr->>U: Choose source account and amount U->>B: Request quote B->>A: Create quote A-->>B: { quote } B-->>U: { quote } Usr->>U: Confirm quote U->>B: Create transaction B->>A: Create transaction A-->>B: { transaction } A-->>B: webhook: transaction.created (processing) A->>N: Broadcast withdrawal N-->>A: Confirmations reached A-->>B: webhook: transaction.status-changed (completed/failed) B-->>Usr: Notify the user ``` ## Check available rails Call [List rails](/rest-apis/core-api/assets/list-rails) to confirm the asset and network the user wants to withdraw to, and verify the `withdraw` feature is enabled. ```http theme={null} GET /core/rails?type=crypto&asset=BTC ``` A successful response lists all rails for the asset. Each rail includes a `features` array — only proceed with networks that include `"withdraw"`. For assets supported by multiple networks, require an explicit network selection from the user. Use the network's `reference` field to determine whether to show a destination tag or memo input in your UI. ```json theme={null} { "rails": [ { "type": "crypto", "network": "bitcoin", "method": "crypto-transaction", "asset": "BTC", "decimals": 8, "features": [ "deposit", "withdraw" ] }, { "type": "crypto", "network": "lightning", "method": "crypto-transaction", "asset": "BTC", "decimals": 8, "features": [] } ] } ``` ## Select source account Crypto withdrawals can be sourced from any account. If the selected account is not in the withdrawal asset, the balance will be converted at the time of the transaction using Uphold's prevailing rate. Make sure the origin asset has the necessary [features enabled](/rest-apis/core-api/assets/introduction#features-and-deposits-/-withdrawals). Call [List accounts](/rest-apis/core-api/accounts/list-accounts) to retrieve the user's accounts and let them choose one with sufficient balance for the withdrawal. ```http theme={null} GET /core/accounts?currency=BTC ``` ```json theme={null} { "accounts": [ { "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a", "label": "My BTC account", "asset": "BTC", "balance": { "total": "0.05", "available": "0.05" } } ] } ``` ## Create a quote Create a quote for the withdrawal using the [Create quote](/rest-apis/core-api/transactions/create-quote) endpoint. ```http theme={null} POST /core/transactions/quote { "origin": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8" }, "destination": { "type": "crypto-address", "asset": "BTC", "network": "bitcoin", "address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa" }, "denomination": { "asset": "GBP", "amount": "100.00", "target": "origin" } } ``` A successful response returns a `quote` object with details about the withdrawal, including fees and expiration. ```json [expandable] theme={null} { "quote": { "id": "623000c8-9bdf-4a2b-aa3d-6a6b44a7f6a0", "origin": { "amount": "0.00121023", "asset": "BTC", "rate": "0.00002629253259492961", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "destination": { "amount": "0.00121023", "asset": "BTC", "rate": "1", "node": { "type": "crypto-address", "address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", "network": "bitcoin", "execution": { "mode": "onchain" } } }, "denomination": { "amount": "100.00", "asset": "GBP", "target": "origin", "rate": "0.00001210225938333485" }, "fees": [], "expiresAt": "2024-07-24T15:22:39Z" } } ``` Quotes typically **expire** quickly. Prompt for user confirmation within the expiry window and regenerate if needed. ## Handle quote requirements When the quote is returned, check the `requirements` array. If non-empty, resolve each requirement before creating the transaction. ```json theme={null} { "quote": { "id": "623000c8-9bdf-4a2b-aa3d-6a6b44a7f6a0", "requirements": [ "travel-rule" ], "expiresAt": "2024-07-24T15:22:39Z" } } ``` ### Travel Rule Travel Rule is a regulatory requirement that mandates the collection and transmission of information about the originator and beneficiary of certain crypto transactions. Use the [Travel Rule widget](/widgets/travel-rule) to collect the required information. #### Create a widget session Call [Create session](/rest-apis/widgets-api/travel-rule/create-session) with `"flow": "withdrawal-form"` and the quote id. ```http theme={null} POST /widgets/travel-rule/sessions { "flow": "withdrawal-form", "data": { "quoteId": "623000c8-9bdf-4a2b-aa3d-6a6b44a7f6a0" } } ``` A successful response returns a `session` object with a `token` and `url` to initialize the widget. ```json theme={null} { "session": { "flow": "withdrawal-form", "url": "https://widgets.uphold.com/travel-rule/sessions/abc123", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "data": { "provider": "notabene", "parameters": {} } } } ``` #### Set up the widget Initialize the widget for the `withdrawal-form` flow using the session data returned from the API. ```javascript theme={null} import { TravelRuleWidget } from '@uphold/enterprise-travel-rule-widget-web-sdk'; const initializeTravelRuleWidget = async (session) => { const widget = new TravelRuleWidget<'withdrawal-form'>(session, { debug: true }); widget.on('ready', () => { console.log('Ready'); }); widget.on('complete', (event) => { console.log('Complete', JSON.stringify(event.detail.value)); }); widget.on('cancel', () => { console.log('Cancelled'); widget.unmount(); }); widget.on('error', (event) => { console.error('Error', event.detail.error); widget.unmount(); }); widget.mountIframe(document.getElementById('travel-rule-container')); }; ``` The example above is for web applications. For native apps using a WebView, you'll need a bridge for events as outlined in [Installation & setup](/widgets/travel-rule/installation-and-setup#native-app-integration). #### Handle the complete event The `complete` event fires when the user successfully submits the Travel Rule form. Send the `travelRule` data to your backend to include when creating the transaction. ```javascript theme={null} widget.on('complete', (event) => { const { value: travelRule } = event.detail; sendToBackend({ travelRule }); widget.unmount(); }); ``` #### Handle cancellations The `cancel` event fires when the user closes the widget without completing the form. ```javascript theme={null} widget.on('cancel', () => { widget.unmount(); // Redirect back or show a cancellation message }); ``` #### Handle errors The `error` event fires when an unrecoverable error occurs. ```javascript theme={null} widget.on('error', (event) => { console.error('Widget error:', event.detail.error); widget.unmount(); // Show a user-friendly error message }); ``` ## Create a transaction Once the user confirms the quote, create the transaction using the [Create transaction](/rest-apis/core-api/transactions/create-transaction) endpoint. If the Travel Rule widget emitted a `complete` event, include the `travelRule` data in `params`. If Travel Rule was required, the original quote may have expired while the user was completing the form. If so, create a new quote before proceeding — the Travel Rule data remains valid and will automatically apply to the new quote. ```http theme={null} POST /core/transactions { "quoteId": "623000c8-9bdf-4a2b-aa3d-6a6b44a7f6a0", "params": { "travelRule": { // Travel Rule data from widget complete event — omit if not required } } } ``` In a successful crypto withdrawal, the origin is the source account and the destination is a `crypto-address` node reflecting the recipient's on-chain address. ```json [expandable] theme={null} { "transaction": { "id": "223c24c5-76c6-4553-91bc-5af519441f03", "origin": { "amount": "0.00121023", "asset": "BTC", "rate": "0.00002629253259492961", "node": { "type": "account", "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8", "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a" } }, "destination": { "amount": "0.00121023", "asset": "BTC", "rate": "1", "node": { "type": "crypto-address", "address": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", "network": "bitcoin", "execution": { "mode": "onchain" } } }, "fees": [], "status": "processing", "quotedAt": "2024-07-24T15:02:39Z", "createdAt": "2024-07-24T15:22:39Z", "updatedAt": "2024-07-24T15:22:39Z", "denomination": { "amount": "100.00", "asset": "GBP", "target": "origin", "rate": "0.00001210225938333485" } } } ``` ### Execution modes Crypto withdrawals support different execution modes depending on the destination and environment: * **On-chain execution**: The transaction is processed directly on the blockchain network. The `destination.node.execution.mode` will be `onchain` and include blockchain-specific details such as transaction hashes upon completion. * **Off-chain execution**: When both the sender and recipient are Uphold users, the transaction is processed internally within Uphold's infrastructure. This eliminates network fees and is faster than onchain processing. The `destination.node.execution.mode` will be `offchain`. For offchain transactions, the `execution` object includes additional properties: `accountOwnerId` (the recipient user ID) and `accountId` (the recipient account ID). * **Simulated execution**: Used in development environments for testing purposes. The transaction appears processed but does not affect actual blockchain state (user balances will be affected though). The `destination.node.execution.mode` will be `simulated`. ## Monitor for settlement Prefer **webhooks** for real-time updates, or fall back to **polling** if webhooks are not feasible. * Webhook events (recommended): * [core.transaction.created](/rest-apis/core-api/transactions/webhooks/transaction-created) * `status: processing` → initiated but not yet broadcast * [core.transaction.status-changed](/rest-apis/core-api/transactions/webhooks/transaction-status-changed) * `status: completed` → broadcast and confirmed * `status: on-hold` → transaction checks paused (e.g., pending RFIs) * `status: failed` → transaction failed, check `statusDetails` for more info * Polling (fallback): [Get transaction](/rest-apis/core-api/transactions/get-transaction) ## Notify the user Display an in-app confirmation when the transaction is `completed`, and send an email if applicable. You now support crypto withdrawals with the Enterprise API Suite. # Developer guides for onboarding and money movement Source: https://developer.uphold.com/developer-guides/overview Step-by-step guides for the most common Uphold integrations: user onboarding, plus bank, card, and crypto deposits and withdrawals via API or Widget. The guides in this section walk through the full lifecycle of the most common integration scenarios — from bringing new users onto the platform to moving funds across crypto networks, banks, and cards. ## User onboarding Bring retail users onto the platform — verified, compliant, and ready to transact. Businesses Coming soon } icon="briefcase" href="/developer-guides/user-onboarding/business/overview"> Onboard business clients with the KYB and compliance workflows they need. ## Money movement Connect to local banking rails across the UK, EU, and US. Let users fund and cash out instantly with the card already in their wallet. Power native crypto on/off-ramp experiences across 50+ blockchain networks. Retrieve monthly portfolio snapshots and transaction history, and generate compliance reports for your users. * [Fetching the data](/developer-guides/statements/fetching-data) * [Preparing data](/developer-guides/statements/preparing-data) * [Generating the report](/developer-guides/statements/generating-report) # Introduction to dynamic forms Source: https://developer.uphold.com/developer-guides/resources/dynamic-forms/introduction Dynamic forms are server-driven JSON Schema and UI Schema definitions that adapt data collection (KYC, compliance) without you shipping app code changes. Dynamic forms are server-driven forms that enable flexible and adaptive data collection. They allow the Enterprise API Suite to evolve form requirements — such as adding new fields for regulatory compliance — without requiring you to update your application code. ## What are dynamic forms? Dynamic forms are defined by API responses rather than hardcoded in your application. They consist of two parts: * **JSON Schema**: Defines the data structure and validation rules * **UI Schema**: Defines the layout, controls, and conditional behavior This approach is powered by [JSON Forms](https://jsonforms.io/), an open standard for describing forms in a platform-agnostic way. ## Why dynamic forms? Forms automatically adapt to changing compliance requirements without app updates. Questions are revealed based on previous answers, creating a guided experience. Standardized form definitions ensure consistent behavior across platforms. Your integration remains compliant as form requirements are updated. ## How dynamic forms work When an API response includes a dynamic form, you receive a `schema` and a `uiSchema`. Your application uses these to render the form dynamically. ### Progressive disclosure Forms support **progressive disclosure** — the ability to reveal additional fields based on previous answers. When you submit partial data, the API may return an updated `schema` and `uiSchema` with new questions influenced by your submission. This creates a conversational flow where users only see relevant questions, improving the experience and data quality. ### Prefilled forms Some forms can be rendered with previously submitted data, allowing users to review and update their information. In these cases, you receive the `schema` and `uiSchema` alongside the existing data, which you can use to prefill the form. ## Where you can find dynamic forms Several KYC processes use dynamic forms to collect user information. The specific fields required may vary based on the user's country of residence. | Process | Description | | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Profile](/rest-apis/core-api/kyc/update-profile) | Basic information such as name, birthdate, and citizenship. Requirements vary by country — for example, users in EU must also provide birthplace details. | | [Customer due diligence](/rest-apis/core-api/kyc/update-customer-due-diligence) | Financial information and risk assessment | | [Tax details](/rest-apis/core-api/kyc/update-tax-details) | Tax residency and identification numbers | | [Crypto risk assessment](/rest-apis/core-api/kyc/update-crypto-risk-assessment) | Knowledge assessment for crypto risks (GB residents only) | | [Self-categorization statement](/rest-apis/core-api/kyc/update-self-categorization-statement) | Investor profile classification (GB residents only) | For more details on KYC processes, see the [KYC Introduction](/rest-apis/core-api/kyc/introduction). ## Implementation approaches You can implement dynamic form rendering using an official JSON Forms library or by building your own custom renderer. See [Rendering](./rendering) for detailed guidance on both approaches. ## Next steps Learn about JSON Schema types and validation. Explore layouts, controls, and rules. # Render dynamic forms in your application Source: https://developer.uphold.com/developer-guides/resources/dynamic-forms/rendering Render Uphold dynamic forms in your app using the JSON Schema, UI Schema, and submission handlers, with examples for React, JSON Forms, and validation. This page provides implementation guidance for rendering dynamic forms in your application. ## Getting started To render a dynamic form, you need: 1. A **schema** — the JSON Schema defining data types and validation 2. A **uiSchema** — the UI Schema defining layout and controls 3. A **data** — (optional) existing data to prefill the form 4. A **renderer** — either a JSON Forms library or your own implementation ## Using JSON Forms libraries The fastest way to get started is using an official JSON Forms renderer: * [React](https://jsonforms.io/docs/integrations/react/) * [Angular](https://jsonforms.io/docs/integrations/angular) * [Vue](https://jsonforms.io/docs/integrations/vue) These libraries handle schema interpretation, validation, and rendering out of the box. ### Schema-level hints The JSON Schema includes a `format` keyword that indicates how a field should be rendered. Use it to select the appropriate input component: | Format | Implementation | | ---------------- | -------------------------- | | `format: "date"` | Render a date picker input | ### Custom renderers for Enterprise API Suite The UI Schema extends JSON Forms with custom `options` that require additional rendering logic: | Option | Implementation | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `data.source` | Populate options from any data source (API, database, etc.) based on the source identifier. The Enterprise API Suite provides endpoints for available sources. | | `data.exclude` | Filter the data source based on the specified restriction | | `format: "postal-code"` | Render a postal code input with country-aware formatting | | `rules` | Apply client-side validation rules (e.g., age thresholds) | | `dependsOn` | Re-fetch or update the control when a dependency value changes | ## Building a custom renderer If you need full control over the user experience, you can build your own form renderer. Your implementation must: 1. **Parse the schema** — Extract property definitions, types, and validation rules 2. **Parse the uiSchema** — Build the layout tree from elements 3. **Render controls** — Map schema types to appropriate input components 4. **Apply rules** — Evaluate conditions and show/hide/enable/disable elements 5. **Validate input** — Enforce schema constraints before submission 6. **Handle progressive disclosure** — Re-render when the API returns updated schemas ## Handling progressive disclosure When implementing progressive disclosure: 1. Render the form from `schema`, `uiSchema`, and any existing `data` 2. Collect user input for the current step (Category) 3. Submit the answers to the API 4. Compare the new response with the previous one: * If there's a new root property in the schema, or a new Category in the uiSchema, re-render the form to show the new questions * If the schema and uiSchema are unchanged, the form is complete ## Examples: React custom renderers The examples below show how to implement custom renderers using [JSON Forms for React](https://jsonforms.io/docs/integrations/react/) and Material UI. Each renderer follows the same pattern: a **tester** that matches specific UI Schema options, and a **control component** that renders the appropriate input. ### Country renderer (`data.source: "countries"`) Fetches the country list and applies `data.exclude` filters. In this example, the data is fetched from [List countries](/rest-apis/core-api/countries/list-countries) endpoint. ```tsx expandable theme={null} import { withJsonFormsControlProps } from '@jsonforms/react'; import { rankWith } from '@jsonforms/core'; import { Autocomplete, TextField } from '@mui/material'; import { useEffect, useState } from 'react'; // Custom renderer that fetches data for the data.source option const DataSourceControl = (props) => { const { data, path, handleChange, uischema, label } = props; const [options, setOptions] = useState([]); const { data: dataOptions } = uischema.options || {}; useEffect(() => { if (dataOptions?.source === 'countries') { // Fetch countries and apply exclude filters fetch('https://api.enterprise.uphold.com/core/countries', { headers: { 'Authorization': `Bearer ${accessToken}` // Your OAuth2 access token } }) .then((res) => res.json()) .then((countries) => { // Apply exclude filter if specified const filtered = dataOptions.exclude?.restrictions ? countries.filter((country) => !dataOptions.exclude.restrictions.some((r) => country.restrictions?.some((cr) => cr.scope === r.scope) ) ) : countries; setOptions(filtered.map((c) => ({ value: c.code, label: c.name }))); }); } }, [dataOptions]); return ( o.value === data) || null} onChange={(_, selected) => handleChange(path, selected?.value)} getOptionLabel={(option) => option.label} renderInput={(params) => } /> ); }; // Tester: use this renderer when data.source is present const dataSourceTester = rankWith(10, (uischema) => uischema.options?.data?.source ? true : false ); // Export for use with JsonForms export const dataSourceRenderer = { tester: dataSourceTester, renderer: withJsonFormsControlProps(DataSourceControl), }; ``` This example demonstrates: * Detecting the `data.source` option with a custom tester * Fetching options from the API and applying `data.exclude` filters * Using Material UI's Autocomplete for consistent styling * Passing existing `data` to prefill the form ### Subdivision renderer (`data.source: "subdivisions"` + `dependsOn`) Fetches subdivisions based on the selected country. Uses `dependsOn` to read the current country value and re-fetch when it changes. In this example, the data is fetched from [Get country](/rest-apis/core-api/countries/get-country) endpoint. ```tsx expandable theme={null} import { withJsonFormsControlProps } from '@jsonforms/react'; import { rankWith, Resolve, toDataPath } from '@jsonforms/core'; import { useJsonForms } from '@jsonforms/react'; import { Autocomplete, TextField } from '@mui/material'; import { useEffect, useRef, useState } from 'react'; // Custom renderer that fetches subdivisions based on the selected country const SubdivisionControl = (props) => { const { data, path, handleChange, uischema, label } = props; const { core } = useJsonForms(); const [options, setOptions] = useState([]); const isFirstRender = useRef(true); const { data: dataOptions } = uischema.options || {}; // Read the country value from the dependency declared in dependsOn const dependsOn = uischema.options?.dependsOn; const countryScope = dependsOn?.find(({ name }) => name === 'country')?.scope; const country = countryScope ? Resolve.data(core?.data, toDataPath(countryScope)) : undefined; useEffect(() => { if (dataOptions?.source === 'subdivisions' && country) { // Clear the subdivision value when the country changes (except on initial render) if (!isFirstRender.current) { handleChange(path, undefined); } isFirstRender.current = false; // Fetch subdivisions from GET /countries/{country} fetch(`https://api.enterprise.uphold.com/core/countries/${country}`, { headers: { 'Authorization': `Bearer ${accessToken}` // Your OAuth2 access token } }) .then((res) => res.json()) .then((data) => { const subdivisions = data.country?.subdivisions ?? []; setOptions(subdivisions.map((s) => ({ value: s.code, label: s.name }))); }); } else { setOptions([]); } }, [dataOptions, country]); return ( o.value === data) || null} onChange={(_, selected) => handleChange(path, selected?.value)} getOptionLabel={(option) => option.label} disabled={!country} renderInput={(params) => } /> ); }; // Tester: use this renderer when data.source is "subdivisions" const subdivisionTester = rankWith(10, (uischema) => uischema.options?.data?.source === 'subdivisions' ); // Export for use with JsonForms export const subdivisionRenderer = { tester: subdivisionTester, renderer: withJsonFormsControlProps(SubdivisionControl), }; ``` This example demonstrates: * Reading dependency values from `dependsOn` using `Resolve.data` and `toDataPath` * Re-fetching subdivisions from [Get country](/rest-apis/core-api/countries/get-country) when the country changes * Clearing the selected value when the dependency changes (except on initial render) * Disabling the control until a country is selected ### Date renderer (`format: "date"` + `rules`) Renders a date picker and converts `rules` (e.g., `difference-greater-than-or-equal-to-threshold`, `difference-less-than-or-equal-to-threshold`) into `min`/`max` date constraints. The `format: "date"` is defined in the JSON Schema; this renderer adds support for the `rules` option from the UI Schema. ```tsx expandable theme={null} import { withJsonFormsControlProps } from '@jsonforms/react'; import { rankWith } from '@jsonforms/core'; import { TextField } from '@mui/material'; // Custom renderer that handles the format: "date" schema with validation rules const DateControl = (props) => { const { data, path, handleChange, uischema, label } = props; const rules = uischema.options?.rules ?? []; // Your own function that converts rules into date picker constraints. // This must be implemented by the partner (e.g., evaluating threshold rules against the current date). const { min, max } = RulesEvaluator.datepicker.evaluate(rules); return ( handleChange(path, e.target.value || undefined)} /> ); }; // Tester: use this renderer when schema.format is "date" const dateTester = rankWith(10, (schema) => schema?.format === 'date' ); // Export for use with JsonForms export const dateRenderer = { tester: dateTester, renderer: withJsonFormsControlProps(DateControl), }; ``` This example demonstrates: * Detecting `format: "date"` in the JSON Schema with a custom tester * Delegating rule evaluation to a partner-implemented function * Applying `min`/`max` constraints to the native date input ### Postal code renderer (`format: "postal-code"` + `dependsOn`) Renders a text input for postal codes. Uses `dependsOn` to read the current country and subdivision values, then applies country-specific validation patterns. ```tsx expandable theme={null} import { withJsonFormsControlProps } from '@jsonforms/react'; import { rankWith, Resolve, toDataPath } from '@jsonforms/core'; import { useJsonForms } from '@jsonforms/react'; import { Autocomplete, TextField } from '@mui/material'; import { useEffect, useState } from 'react'; // Custom renderer that validates postal codes based on the selected country const PostalCodeControl = (props) => { const { data, path, handleChange, uischema, label } = props; const { core } = useJsonForms(); const [pattern, setPattern] = useState(null); // Read dependency values declared in dependsOn const dependsOn = uischema.options?.dependsOn; const countryScope = dependsOn?.find(({ name }) => name === 'country')?.scope; const subdivisionScope = dependsOn?.find(({ name }) => name === 'subdivision')?.scope; const country = countryScope ? Resolve.data(core?.data, toDataPath(countryScope)) : undefined; const subdivision = subdivisionScope ? Resolve.data(core?.data, toDataPath(subdivisionScope)) : undefined; useEffect(() => { if (uischema.options?.format === 'postal-code' && country) { // Your own service/function that returns a regex pattern for the given country. // This must be implemented by the partner (e.g., from a local mapping or an API). PostalCodeService.getPattern(country, subdivision).then(setPattern); } else { setPattern(null); } }, [uischema.options?.format, country, subdivision]); const validationError = data && pattern && !new RegExp(pattern).test(data) ? 'Invalid postal code format' : undefined; return ( handleChange(path, e.target.value)} /> ); }; // Tester: use this renderer when options.format is "postal-code" const postalCodeTester = rankWith(10, (uischema) => uischema.options?.format === 'postal-code' ); // Export for use with JsonForms export const postalCodeRenderer = { tester: postalCodeTester, renderer: withJsonFormsControlProps(PostalCodeControl), }; ``` This example demonstrates: * Reading multiple dependency values (`country`, `subdivision`) from `dependsOn` * Delegating pattern resolution to a partner-implemented service * Applying regex validation against the resolved pattern * Disabling the control until a country is selected ### Registering all custom renderers Register all custom renderers with the `JsonForms` component: ```tsx expandable theme={null} import { JsonForms } from '@jsonforms/react'; import { materialRenderers, materialCells } from '@jsonforms/material-renderers'; import { dataSourceRenderer } from './DataSourceControl'; import { subdivisionRenderer } from './SubdivisionControl'; import { dateRenderer } from './DateControl'; import { postalCodeRenderer } from './PostalCodeControl'; const customRenderers = [ dataSourceRenderer, subdivisionRenderer, dateRenderer, postalCodeRenderer, ...materialRenderers, ]; const DynamicForm = ({ schema, uiSchema, data }) => ( ); ``` ## See it in action To see dynamic forms in practice, explore the KYC processes that use them: Learn how dynamic forms power KYC data collection. # Dynamic forms JSON Schema reference Source: https://developer.uphold.com/developer-guides/resources/dynamic-forms/schema Reference for the JSON Schema that defines dynamic form data structure, types, and validation rules. Covers properties, required fields, and constraints. The JSON Schema defines the data structure, types, and validation rules for dynamic forms. It follows the [JSON Schema](https://json-schema.org/) specification. ## Structure A schema defines an `object` with `properties`. Each property specifies its type and validation constraints: ```json theme={null} { "type": "object", "properties": { "firstName": { "type": "string", "minLength": 1, "maxLength": 100 }, "age": { "type": "number" } }, "required": ["firstName"] } ``` The `required` array lists properties that must be provided when submitting the form. ## Steps Root-level properties in the schema represent **steps** — logical groupings of related questions. Each step is an object containing the fields the user needs to complete. A form with one step: ```json theme={null} { "type": "object", "properties": { "personalInfo": { "type": "object", "properties": { "firstName": { "type": "string" }, "lastName": { "type": "string" } } } } } ``` A form with two steps: ```json theme={null} { "type": "object", "properties": { "personalInfo": { "type": "object", "properties": { "firstName": { "type": "string" }, "lastName": { "type": "string" } } }, "contactInfo": { "type": "object", "properties": { "email": { "type": "string" }, "phone": { "type": "string" } } } } } ``` ### Progressive disclosure Steps enable **progressive disclosure** — as the user completes one step and submits their answers, the API may return an updated schema with new steps revealed. This means the schema grows dynamically based on previous answers, allowing you to guide users through a multi-step flow without showing all questions upfront. ## Types ### String Text values, typically rendered as text inputs. ```json theme={null} { "type": "string" } ``` ### Number Numeric values, typically rendered as number inputs. ```json theme={null} { "type": "number" } ``` ### Boolean True/false values, typically rendered as checkboxes or toggles. ```json theme={null} { "type": "boolean" } ``` ### Array Lists of items, typically rendered as multi-selects or repeatable sections. ```json theme={null} { "type": "array", "items": { "type": "object", "properties": { "country": { "type": "string" }, "taxId": { "type": "string" } } }, "minItems": 1, "maxItems": 5 } ``` ### Object Nested structures with their own properties. ```json theme={null} { "type": "object", "properties": { "street": { "type": "string" }, "city": { "type": "string" }, "postalCode": { "type": "string" } } } ``` ## Formats The `format` keyword provides semantic meaning to string values. ### Date Date values, typically rendered as date pickers. ```json theme={null} { "type": "string", "format": "date" } ``` Custom display formats (e.g., `postal-code`) are defined in the UI Schema via [`options.format`](/developer-guides/resources/dynamic-forms/ui-schema#format), not in the JSON Schema. ## Validation keywords ### String validation | Keyword | Description | | ----------- | --------------------------------------- | | `minLength` | Minimum number of characters | | `maxLength` | Maximum number of characters | | `pattern` | Regular expression the value must match | ```json theme={null} { "type": "string", "pattern": "^[A-Z]{2}[0-9]{9}$", "minLength": 11, "maxLength": 11 } ``` ### Array validation | Keyword | Description | | ------------- | ------------------------------------------------- | | `minItems` | Minimum number of items | | `maxItems` | Maximum number of items | | `uniqueItems` | When `true`, all items must be unique | | `contains` | At least one item must match the specified schema | ```json theme={null} { "type": "array", "minItems": 1, "maxItems": 10, "uniqueItems": true } ``` ### Enumeration and constants #### enum Restricts a property to a predefined list of allowed values. ```json theme={null} { "type": "string", "enum": ["employed", "self-employed", "retired", "student", "unemployed"] } ``` #### const Restricts a property to a single fixed value. ```json theme={null} { "type": "string", "const": "agreed" } ``` #### oneOf Allows a value to match one of several schemas. Often used for conditional validation based on a previous answer. ```json theme={null} { "oneOf": [ { "properties": { "employmentStatus": { "const": "employed" }, "employerName": { "type": "string" } }, "required": ["employerName"] }, { "properties": { "employmentStatus": { "const": "self-employed" }, "businessName": { "type": "string" } }, "required": ["businessName"] }, { "properties": { "employmentStatus": { "enum": ["retired", "student", "unemployed"] } } } ] } ``` In this example, users who select "employed" must provide an employer name, those who select "self-employed" must provide a business name, and other statuses require no additional information. ## Next steps Learn how to render forms with layouts, controls, and rules. Implementation guidance and best practices. # Dynamic forms UI Schema reference Source: https://developer.uphold.com/developer-guides/resources/dynamic-forms/ui-schema Reference for the UI Schema that defines dynamic form layouts, controls, conditional visibility, and custom options based on the JSON Forms specification. The UI Schema defines how to render a dynamic form — the layout structure, controls, conditional visibility rules, and custom options. It follows the [JSON Forms UI Schema](https://jsonforms.io/docs/uischema) specification. ## Structure A UI Schema is a tree of **elements**. Each element has a `type` that determines how it behaves: * **Layouts** — Container elements that organize other elements * **Controls** — Elements that render input fields bound to schema properties ```json theme={null} { "type": "VerticalLayout", "elements": [ { "type": "Control", "scope": "#/properties/firstName", "label": "First Name" }, { "type": "Control", "scope": "#/properties/lastName", "label": "Last Name" } ] } ``` ## Layouts Layouts organize controls and other layouts into a visual structure. ### VerticalLayout Arranges elements vertically, one below the other. ```json theme={null} { "type": "VerticalLayout", "elements": [ { "type": "Control", "scope": "#/properties/firstName" }, { "type": "Control", "scope": "#/properties/lastName" } ] } ``` ### HorizontalLayout Arranges elements horizontally, side by side. ```json theme={null} { "type": "HorizontalLayout", "elements": [ { "type": "Control", "scope": "#/properties/firstName" }, { "type": "Control", "scope": "#/properties/lastName" } ] } ``` ### Group Groups related controls together with an optional label. ```json theme={null} { "type": "Group", "label": "Personal Information", "elements": [ { "type": "Control", "scope": "#/properties/firstName" }, { "type": "Control", "scope": "#/properties/lastName" } ] } ``` ### Categorization Organizes content into tabs or wizard-style steps. A `Categorization` contains multiple `Category` elements, each representing a step in the form. ```json theme={null} { "type": "Categorization", "elements": [ { "type": "Category", "label": "Personal Info", "elements": [ { "type": "Control", "scope": "#/properties/personalInfo/properties/firstName" }, { "type": "Control", "scope": "#/properties/personalInfo/properties/lastName" } ] }, { "type": "Category", "label": "Contact Info", "elements": [ { "type": "Control", "scope": "#/properties/contactInfo/properties/email" }, { "type": "Control", "scope": "#/properties/contactInfo/properties/phone" } ] } ] } ``` Categories correspond to **steps** in the schema. Each `Category` typically maps to a root-level schema property, enabling progressive disclosure as users complete each step. ### ListWithDetail Renders an array as a master-detail view — a list of items on one side and a detail form for the selected item. ```json theme={null} { "type": "ListWithDetail", "scope": "#/properties/addresses", "options": { "detail": { "type": "VerticalLayout", "elements": [ { "type": "Control", "scope": "#/properties/street" }, { "type": "Control", "scope": "#/properties/city" } ] } } } ``` ## Controls Controls render input fields and bind them to schema properties. ### Control The `Control` element renders an input based on the schema type at the specified `scope`. ```json theme={null} { "type": "Control", "scope": "#/properties/email", "label": "Email Address" } ``` | Property | Description | | --------- | ------------------------------------------------------------------------- | | `scope` | JSON Pointer to the schema property (e.g., `#/properties/email`) | | `label` | Display label for the control (optional — derived from schema if omitted) | | `options` | Custom options to modify control behavior | The renderer automatically selects the appropriate input type based on the schema: | Schema Type | Rendered As | | ------------------------------------------ | -------------------- | | `string` | Text input | | `string` + `format: "date"` (schema) | Date picker | | `string` + `options.format: "postal-code"` | Postal code input | | `string` + `enum` | Dropdown/select | | `number` | Number input | | `boolean` | Checkbox or toggle | | `array` | List with add/remove | ## Rules Rules control the visibility and enabled state of elements based on data conditions. ```json theme={null} { "type": "Control", "scope": "#/properties/employerName", "rule": { "effect": "SHOW", "condition": { "scope": "#/properties/employmentStatus", "schema": { "const": "employed" } } } } ``` ### Effects | Effect | Description | | --------- | ------------------------------------------------------------ | | `SHOW` | Show the element when condition is true, hide otherwise | | `HIDE` | Hide the element when condition is true, show otherwise | | `ENABLE` | Enable the element when condition is true, disable otherwise | | `DISABLE` | Disable the element when condition is true, enable otherwise | ### Conditions A condition specifies a `scope` (the property to evaluate) and a `schema` that the value must match. ```json theme={null} { "condition": { "scope": "#/properties/country", "schema": { "enum": ["US", "CA"] } } } ``` Common condition patterns: | Pattern | Schema | | ------------------ | ----------------------------- | | Equals a value | `{ "const": "value" }` | | One of many values | `{ "enum": ["a", "b", "c"] }` | | Not empty | `{ "minLength": 1 }` | ## Options The `options` property on controls allows you to customize behavior. These are standard [JSON Forms options](https://jsonforms.io/docs/uischema/controls#options). ### Array options Options for array controls: | Option | Description | | ------------------ | ---------------------------------------------------- | | `showSortButtons` | Shows buttons to reorder items | | `elementLabelProp` | Property to use as the label for each item in a list | | `disableAdd` | Prevents adding new items (renderer-specific) | | `disableRemove` | Prevents removing items (renderer-specific) | ```json theme={null} { "type": "Control", "scope": "#/properties/documents", "options": { "elementLabelProp": "name", "showSortButtons": true } } ``` ### readonly Renders the control as read-only. ```json theme={null} { "type": "Control", "scope": "#/properties/accountId", "options": { "readonly": true } } ``` ### detail For `ListWithDetail` layouts, specifies the UI Schema for the detail view. ```json theme={null} { "type": "ListWithDetail", "scope": "#/properties/items", "options": { "detail": { "type": "VerticalLayout", "elements": [...] } } } ``` ## Custom options The Enterprise API Suite extends JSON Forms with custom options for specific use cases. ### data Specifies a data source for populating select options dynamically. The `data` object defines where to fetch options from and how to filter them. ```json theme={null} { "type": "Control", "scope": "#/properties/country", "options": { "data": { "source": "countries", "exclude": { "restrictions": [ { "scope": "citizenship" } ] } } } } ``` | Property | Description | | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `data.source` | The data source to fetch options from.
Available sources: `"countries"` ([List countries](/rest-apis/core-api/countries/list-countries)) and `"subdivisions"` ([Get country](/rest-apis/core-api/countries/get-country), returns subdivisions for a given country). | | `data.exclude` | Optional filter to exclude values from the data source | | `data.exclude.restrictions` | Array of restrictions to filter values from the data source | | `data.exclude.restrictions[].scope` | The restriction scope (e.g., `citizenship`, `residence`, `geolocation`, `phone`) | ### format Specifies custom display formats for controls that are not covered by the standard JSON Schema `format` keyword. Standard formats like `date` are defined in the [JSON Schema](/developer-guides/resources/dynamic-forms/schema#formats) and handled natively by renderers. ```json theme={null} { "type": "Control", "scope": "#/properties/address/properties/postalCode", "options": { "format": "postal-code" } } ``` | Format | Description | | ------------- | ---------------------------------------------------------------------------------------------------------- | | `postal-code` | Renders an input optimized for postal codes, with formatting and validation rules that may vary by country | ### rules (validation) Defines client-side validation rules on a control. These rules allow you to enforce constraints that go beyond standard JSON Schema validation, such as age thresholds relative to the current date. These validation rules are defined in `options.rules` on individual controls and are distinct from the [visibility rules](#rules) (`rule` with `effect` and `condition`) that control element visibility. ```json theme={null} { "type": "Control", "scope": "#/properties/birthdate", "options": { "rules": [ { "rule": "difference-greater-than-or-equal-to-threshold", "threshold": { "limit": 18, "unit": "years" } }, { "rule": "difference-less-than-or-equal-to-threshold", "threshold": { "limit": 100, "unit": "years" } } ] } } ``` In this example, the date must be at least 18 years ago and at most 100 years ago, effectively restricting the input to valid adult birthday dates. | Property | Description | | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `rules[].rule` | The validation rule type. Available rules: `difference-greater-than-or-equal-to-threshold`, `difference-less-than-or-equal-to-threshold`. | | `rules[].threshold.limit` | The numeric limit for the threshold | | `rules[].threshold.unit` | The unit for the threshold (e.g., `"years"`) | ### dependsOn Declares that a control depends on the value of one or more other controls. When a dependency value changes, the dependent control should update its available options or validation accordingly. ```json theme={null} { "type": "Control", "scope": "#/properties/address/properties/subdivision", "options": { "data": { "source": "subdivisions" }, "dependsOn": [ { "name": "country", "scope": "#/properties/address/properties/country" } ] } } ``` In this example, the subdivision field depends on the selected country — when the country changes, the list of available subdivisions is updated. | Property | Description | | ------------------- | ------------------------------------------------------ | | `dependsOn` | Array of field dependencies | | `dependsOn[].name` | The logical name of the dependency (e.g., `"country"`) | | `dependsOn[].scope` | JSON Pointer to the dependency's schema property | Dependencies can also be chained. For example, a postal code field can depend on both country and subdivision: ```json theme={null} { "type": "Control", "scope": "#/properties/address/properties/postalCode", "options": { "format": "postal-code", "dependsOn": [ { "name": "country", "scope": "#/properties/address/properties/country" }, { "name": "subdivision", "scope": "#/properties/address/properties/subdivision" } ] } } ``` ## Next steps Implementation guidance and best practices. # Fetching data Source: https://developer.uphold.com/developer-guides/statements/fetching-data This guide covers how to set a period and denomination, fetch portfolio holdings and transactions, and pull it all together into a multi-month report. ## Prerequisites * The user is onboarded and verified (KYC is complete). ## Set the period All statements are scoped to a specific period, defined by three required query parameters: * `period` — the period type. Currently only `one-month` is supported. * `year` — the calendar year (e.g. `2025`). * `month` — the calendar month as a number from `1` to `12`. The `period` object in the response reflects the exact UTC start and end timestamps of the requested period: ```json theme={null} { "period": { "from": "2025-03-01T00:00:00.000Z", "to": "2025-03-31T23:59:59.999Z" } } ``` Valid periods range from the month the user's account was created to the last fully completed calendar month. Requests outside this range, or with an unsupported denomination asset, return a `409` error. A valid period with no user activity still returns a successful response — holdings and transactions will have no entries. ## Set the denomination The `denomination` query parameter accepts a single asset code (e.g. `denomination=GBP`). If not provided, `USD` is used by default. Exchange rates use USD as the base pair. When the denomination is not USD, the response also includes a `USD-{denomination}` rate to complete the conversion. Supported assets include some of the major fiat currencies and BTC. If you need a particular asset added to this list, please reach out to your Account Manager. For a more detailed explanation of the denomination concept, check the [Core Concepts](/rest-apis/core-api/concepts#denomination) page. ## Fetch portfolio statements Call [Get portfolio statement](/rest-apis/core-api/statements/get-portfolio-statement) to retrieve the user's holdings at the end of the requested period, along with exchange rates in the requested denomination. Rates are a period-end snapshot. ```http theme={null} GET /core/statements/portfolio?period=one-month&year=2025&month=3&denomination=GBP ``` Holdings are returned per asset. Assets already held in the denomination currency (GBP in this case) have no rate entry — their value is their amount directly. ```json theme={null} { "statement": { "holdings": [ { "asset": "BTC", "total": "0.02" }, { "asset": "ETH", "total": "0.5" }, { "asset": "GBP", "total": "250.00" } ], "period": { "from": "2025-03-01T00:00:00.000Z", "to": "2025-03-31T23:59:59.999Z" }, "rates": { "BTC-USD": "68106.26", "ETH-USD": "2094.45", "USD-GBP": "0.75629" } } } ``` ## Fetch transaction statements Call [Get transactions statement](/rest-apis/core-api/statements/get-transactions-statement) to retrieve a paginated list of completed transactions during the period, each with exchange rates captured at the time they completed. ```http theme={null} GET /core/statements/transactions?period=one-month&year=2025&month=3&denomination=GBP&page=1&perPage=50 ``` Each entry contains the transaction details and a `rates` object with exchange rates captured at the time that transaction completed. ```json theme={null} { "statement": { "period": { "from": "2025-03-01T00:00:00.000Z", "to": "2025-03-31T23:59:59.999Z" }, "transactions": [ { "rates": { "BTC-USD": "68106.26", "USD-GBP": "0.75629" }, "transaction": { "id": "8daa6dbd-21a0-4305-93c3-bd1d04bc575c", "status": "completed", "completedAt": "2025-03-23T12:06:55.116Z", "denomination": { "amount": "100", "asset": "GBP", "target": "origin" }, "origin": { "amount": "100", "asset": "GBP" }, "destination": { "amount": "0.00186036", "asset": "BTC", "rate": "0.00001869711794070723" }, "fees": [ { "amount": "0.5", "asset": "GBP", "type": "deposit" } ] } } ] }, "pagination": { "first": "https://api.enterprise.uphold.com/core/statements/transactions?period=one-month&year=2025&month=3&denomination=GBP&page=1&perPage=50", "next": "https://api.enterprise.uphold.com/core/statements/transactions?period=one-month&year=2025&month=3&denomination=GBP&page=2&perPage=50" } } ``` ### Handling pagination The response includes a `pagination` object with `first` and `next` URL links. `next` points to the next page and is absent on the last page. For a full overview of how pagination works across the API, see [Pagination in responses](/rest-apis/pagination#pagination-in-responses). To retrieve all transactions, request each page in sequence and follow `next` until it's absent. The following helper collects all pages upfront: ```javascript theme={null} async function fetchAllTransactions({ token, userId, year, month, denomination }) { const transactions = []; let page = 1; const perPage = 50; while (true) { const params = new URLSearchParams({ period: 'one-month', year, month, denomination, page, perPage }); const response = await fetch( `https://api.enterprise.uphold.com/core/statements/transactions?${params}`, { headers: { Authorization: `Bearer ${token}`, 'X-On-Behalf-Of': userId } } ); if (!response.ok) { throw new Error(`Failed to fetch transactions: ${response.status}`); } const { statement, pagination } = await response.json(); transactions.push(...statement.transactions); if (!pagination.next) break; page++; } return transactions; } ``` ## Build a quarterly statement To generate a quarterly report, use the helpers above: fetch the portfolio snapshot at the end of the last month of the quarter and aggregate transactions across all three months. ```javascript theme={null} const QUARTER_MONTHS = { Q1: [1, 2, 3], Q2: [4, 5, 6], Q3: [7, 8, 9], Q4: [10, 11, 12] }; async function fetchQuarterlyData({ token, userId, year, quarter, denomination }) { const months = QUARTER_MONTHS[quarter]; const { statement: portfolio } = await fetchPortfolioStatement({ token, userId, year, month: months[2], denomination }); const allTransactions = []; for (const month of months) { const monthTransactions = await fetchAllTransactions({ token, userId, year, month, denomination }); allTransactions.push(...monthTransactions); } return { portfolio, transactions: allTransactions }; } ``` ## Next steps Compute denominated values for holdings and transactions and build the report payload. Build a PDF from the processed data and run the generation pipeline in a background worker. # Generating report Source: https://developer.uphold.com/developer-guides/statements/generating-report This guide covers how to build a document template from processed data and run the generation pipeline in a background worker to ensure end users aren't blocked waiting for the result. ## Choose a generation strategy Choose the method that best fits your infrastructure and layout requirements: * **Render via headless browser** — build an HTML/CSS template and use a browser engine to render it to PDF. Best when you want full control over layout and styling via HTML/CSS. * **Construct programmatically** — use a PDF library like [PDFKit](https://pdfkit.org) to build the document step-by-step in code. Highly performant and ideal for simple, repetitive layouts without the overhead of a browser. * **Use an external service** — send your structured data to a third-party API that handles rendering and hosting, offloading document assembly entirely. The example below uses headless browser rendering with Puppeteer: an HTML template is built from the processed data, and Puppeteer renders it to a PDF. By the end of this step, you will have a PDF buffer ready to store and deliver. ### Render with a headless browser Install Puppeteer: ```bash theme={null} npm install puppeteer ``` Puppeteer downloads a compatible version of Chromium automatically. In environments where you cannot install Chromium (e.g. some serverless platforms), use `puppeteer-core` with a pre-installed browser instead. The template receives the processed data object and returns an HTML string. All values are pre-computed — the template is only responsible for layout: ```javascript theme={null} import puppeteer from 'puppeteer'; function buildHtml(data) { const { period, denomination, holdings, totalValue, transactions, totalFees } = data; const holdingsRows = holdings .map(({ asset, amount, value }) => `${asset}${amount}${value} ${denomination}` ) .join(''); const txRows = transactions .map(({ date, id, status, origin, destination, denominatedAmount, denominatedAsset, value, txFees }) => ` ${date} ${id} ${status} ${origin.amount} ${origin.asset} ${destination.amount} ${destination.asset} ${denominatedAmount} ${denominatedAsset} ${value} ${denomination} ${txFees} ${denomination} ` ) .join(''); return `

Compliance Report

Period: ${period.from.slice(0, 10)} to ${period.to.slice(0, 10)}

Generated: ${new Date().toISOString().slice(0, 10)}

Denomination: ${denomination}

Holdings at end of period

${holdingsRows}
Asset Amount Value (${denomination})
Total${totalValue} ${denomination}

Transactions (${transactions.length})

${txRows}
Date Transaction ID Status Origin Destination Amount Value (${denomination}) Fees (${denomination})
Total fees${totalFees} ${denomination}
`; } export async function generateStatementPdf(data) { const html = buildHtml(data); const browser = await puppeteer.launch({ args: ['--no-sandbox'] }); try { const page = await browser.newPage(); await page.setContent(html, { waitUntil: 'networkidle0' }); return await page.pdf({ format: 'A4', margin: { top: '2cm', right: '2cm', bottom: '2cm', left: '2cm' }, printBackground: true }); } finally { await browser.close(); } } ``` `generateStatementPdf` returns a `Buffer` containing the PDF, ready to be stored or delivered. ## Execute as a background task Offload report generation to an asynchronous worker to keep long-running generation from blocking your API. By returning immediately, your application remains responsive while the worker handles data retrieval, formatting, and file storage in the background. End users don't wait for a report — they request one and get notified when it's ready. Process outline: 1. The end user requests a report. Your backend responds immediately with a reference to the background job. 2. A worker runs the full pipeline: fetch → process → generate. 3. When the report is ready, your backend notifies the end user — via a retrieval link, email, or any other delivery method. ```javascript theme={null} // Route — accepts the request and enqueues the job // Adapt to your preferred job queue (BullMQ, Agenda, etc.) app.post('/reports/monthly', async (req, res, next) => { try { const { userId, year, month, denomination = 'GBP' } = req.body; const token = req.headers.authorization?.split(' ')[1]; const jobId = await reportQueue.add('generate-statement', { token, userId, year: Number(year), month: Number(month), denomination }); res.status(202).json({ jobId }); } catch (error) { next(error); } }); // Worker — runs the full pipeline in the background // Adapt to your queue library (BullMQ v2: new Worker('generate-statement', async job => {...}), Agenda, etc.) async function runReportJob(job) { const { token, userId, year, month, denomination } = job.data; const [{ statement: portfolio }, transactions] = await Promise.all([ fetchPortfolioStatement({ token, userId, year, month, denomination }), fetchAllTransactions({ token, userId, year, month, denomination }) ]); const data = processStatementData({ portfolio, transactions, denomination }); const pdf = await generateStatementPdf(data); const url = await storeReport(pdf, { userId, year, month }); // e.g. upload to S3 or equivalent storage await notifyUser(userId, { reportUrl: url }); // e.g. send email, webhook, or push notification } ``` If any step throws, the error propagates to the queue, which handles job failure and retry according to its configuration. For implementation details on `fetchPortfolioStatement` and `fetchAllTransactions` check [Fetching data](/developer-guides/statements/fetching-data). For details on `processStatementData` check [Preparing data](/developer-guides/statements/preparing-data). # Overview Source: https://developer.uphold.com/developer-guides/statements/overview The guides in this section cover how to retrieve monthly financial records for your users, work with exchange rates and denomination, and generate compliance-ready reports from that data. Statements provide two complementary views of a user's financial activity for a given period: a portfolio snapshot at the end of the period and a paginated list of all completed transactions. Together, they supply the data needed for tax reporting and regulatory compliance. ## Generating reports Generating a compliance report from statement data is a three-step process: Retrieve the portfolio snapshot and full transaction list for the period, paginating through all results. [How to fetch the data](/developer-guides/statements/fetching-data) Interpret the exchange rates, compute denominated values for each holding and transaction, and shape the data for the renderer. [How to prepare data](/developer-guides/statements/preparing-data) Build an HTML template from the processed data and render it to a PDF using a headless browser. [How to generate the report](/developer-guides/statements/generating-report) # Preparing data Source: https://developer.uphold.com/developer-guides/statements/preparing-data This guide covers how to compute currency conversions for holdings and transactions and structure the resulting data into a format ready for asynchronous document generation. ## Interpret the rates The `rates` object uses `{asset}-USD` keys — each representing the price of 1 unit of the asset in USD. When the denomination is not USD, a `USD-{denomination}` key is also included: ```json theme={null} { "BTC-USD": "68106.26", "ETH-USD": "2094.45", "USD-GBP": "0.75629" } ``` `"BTC-USD": "68106.26"` means 1 BTC = 68106.26 USD. Multiply by `USD-GBP` to get the GBP value. Portfolio rates reflect the end of the requested period. Transaction rates are captured at the time the transaction settled. Assets already held in the denomination currency (e.g. GBP when denomination is GBP) have no rate entry — their value is their amount directly. For a detailed explanation of how denomination works in this API, see [Denomination](/rest-apis/core-api/concepts#denomination) in the Core API concepts. ## Calculate portfolio values For each holding, convert the asset's USD rate by the `USD-{denomination}` rate: ``` 0.02 BTC × 68106.26 USD/BTC × 0.75629 GBP/USD = 1030.16 GBP 0.5 ETH × 2094.45 USD/ETH × 0.75629 GBP/USD = 792.01 GBP 250.00 GBP = 250.00 GBP (no rate needed) ``` In code: ```javascript theme={null} function convertToDenomination(amount, asset, rates, denomination) { if (asset === denomination) return parseFloat(amount); if (asset === 'USD') return parseFloat(amount) * parseFloat(rates[`USD-${denomination}`]); const usdRate = rates[`${asset}-USD`]; if (denomination === 'USD') return parseFloat(amount) * parseFloat(usdRate); return parseFloat(amount) * parseFloat(usdRate) * parseFloat(rates[`USD-${denomination}`]); } function computeHoldingValue(holding, rates, denomination) { return convertToDenomination(holding.total, holding.asset, rates, denomination).toFixed(2); } ``` ## Calculate transaction values Each transaction entry has a `denomination` field — the amount and currency the transaction was quoted in, used as the authoritative reference value for compliance purposes — and a `rates` object scoped to that transaction. To get the GBP value of a transaction, convert the denomination amount using the per-transaction rate: ```javascript theme={null} function computeTransactionValue(entry, denomination) { const { transaction, rates } = entry; const { amount, asset } = transaction.denomination; return convertToDenomination(amount, asset, rates, denomination).toFixed(2); } ``` For the GBP→BTC transaction from the previous step, `denomination.asset` is already `GBP`, so the GBP value is `100.00` directly — no rate lookup required. ## Build the payload Use the helpers above to transform the raw API response into a flat object ready for the report renderer: ```javascript theme={null} function processStatementData({ portfolio, transactions, denomination }) { // Holdings const holdings = portfolio.holdings.map(holding => ({ asset: holding.asset, amount: holding.total, value: computeHoldingValue(holding, portfolio.rates, denomination) })); const totalValue = holdings .reduce((sum, h) => sum + parseFloat(h.value), 0) .toFixed(2); // Transactions const processedTransactions = transactions.map(entry => { const { transaction, rates } = entry; const txFees = (transaction.fees ?? []) .reduce((sum, fee) => sum + convertToDenomination(fee.amount, fee.asset, rates, denomination), 0) .toFixed(2); return { date: transaction.completedAt.slice(0, 10), id: transaction.id, status: transaction.status, origin: { amount: transaction.origin.amount, asset: transaction.origin.asset }, destination: { amount: transaction.destination.amount, asset: transaction.destination.asset }, denominatedAmount: transaction.denomination.amount, denominatedAsset: transaction.denomination.asset, value: computeTransactionValue(entry, denomination), fees: transaction.fees ?? [], txFees }; }); const totalFees = processedTransactions .reduce((sum, t) => sum + parseFloat(t.txFees), 0) .toFixed(2); return { period: portfolio.period, denomination, holdings, totalValue, transactions: processedTransactions, totalFees }; } ``` Applied to the GBP example data from the previous step, this produces: ```json theme={null} { "period": { "from": "2025-03-01T00:00:00.000Z", "to": "2025-03-31T23:59:59.999Z" }, "denomination": "GBP", "holdings": [ { "asset": "BTC", "amount": "0.02", "value": "1030.16" }, { "asset": "ETH", "amount": "0.5", "value": "792.01" }, { "asset": "GBP", "amount": "250.00", "value": "250.00" } ], "totalValue": "2072.17", "transactions": [ { "date": "2025-03-23", "id": "8daa6dbd-21a0-4305-93c3-bd1d04bc575c", "status": "completed", "origin": { "amount": "100", "asset": "GBP" }, "destination": { "amount": "0.00186036", "asset": "BTC" }, "denominatedAmount": "100", "denominatedAsset": "GBP", "value": "100.00", "fees": [{ "amount": "0.5", "asset": "GBP", "type": "deposit" }], "txFees": "0.50" } ], "totalFees": "0.50" } ``` ## Next steps Build a PDF from the processed data and run the generation pipeline in a background worker. # Business user onboarding overview (KYB verification) Source: https://developer.uphold.com/developer-guides/user-onboarding/business/overview Onboard business clients with KYB and compliance workflows on the Uphold platform. Coverage, capabilities, and integration paths for business users. Coming soon This section will cover how to onboard business users onto the platform. The onboarding flow includes creating the user, completing the required KYB processes, and confirming the user's capabilities are unlocked before they transact. If you are interested in onboarding business users, please [reach out to us](https://uphold.com/enterprise#contact). In the meantime, you can onboard individual users using the guides in the [Individual](/developer-guides/user-onboarding/individual/overview) section. # Individual user onboarding overview (KYC verification) Source: https://developer.uphold.com/developer-guides/user-onboarding/individual/overview Verify retail user identity and unlock capabilities through Uphold's individual KYC processes, with Uphold-verified or partner-verified models. Individual onboarding verifies user identity and ensures compliance readiness for regulated financial services. It involves creating a user account and completing the KYC processes required for the user's country of residency. ## Verification responsibility Each KYC process can be verified by Uphold or by your organization (the partner). The table below shows which verification model each process supports. | Process | Uphold-verified | Partner-verified | | ---------------------------------------- | ------------------------------------ | ------------------------------------ | | Email | Not supported | Supported | | Phone | Not supported | Supported | | Profile | — | — | | Address | — | — | | Identity | Supported | Supported | | Proof of address | Supported | Supported | | Customer due diligence | Supported | Supported | | Enhanced due diligence | Not supported | Supported | | Crypto risk assessment (UK users) | Supported | Supported | | Self-categorization statement (UK users) | Supported | Supported | | Tax Details | Supported | Not supported | ## Regional requirements KYC requirements vary from region to region, and from partner to partner based on their risk appetite and regulatory interpretation. Below is a high-level overview of Uphold requirements. Refer to your Account Manager for specific requirements applicable to your integration. | Process / Region | UK | US | Notes | | ---------------------------------- | ------------------------------------------ | ------------------------------------------- | -------------------------------------------------------------- | | **Email, Phone, Profile, Address** | Required | Required | | | **Identity** | Required | Required | | | **Proof of address** | Required | Conditional¹ | ¹Only required when identity document does not have an address | | **Customer due diligence** | Required | Conditional¹ | ¹Only required when doing crypto deposits or withdrawals | | **Enhanced due diligence** | Conditional | Conditional | Only required for high-risk users | | **Crypto risk assessment** | Required | Exempt | UK only - FCA requirement | | **Self-Categorization** | Required | Exempt | UK only - FCA requirement | | **Tax Details** | Required | Required | | ## High-level onboarding flow ### 1. Create the user Fetch the applicable Terms of Service for the user's country of residency, present them for acceptance, and create the user. ### 2. Complete KYC processes Establish the user's basic identity and contact information for regulatory compliance. User provides their email and proves ownership, usually via a confirmation link sent to their email. User provides their phone number and proves ownership, usually via an SMS OTP. User provides full name, date of birth, citizenship. User provides residential street address, city, postal code, country. Confirm the user is who they claim to be through government-issued identification or electronic verification (e-IDV). Confirm the user resides where they claim to through document verification or electronic verification. In certain regions (e.g., US), this verification will be automatically attempted if the provided document for Identity Verification contains address data (e.g. driver's license). Assess user risk profile and understand intended use. Risk assessment questionnaire to evaluate customer profile and intended use. Determines if Enhanced due diligence is needed. Additional documentation on source of funds and wealth. Only triggered if Customer due diligence indicates a high-risk profile. **UK users only** - Financial Conduct Authority regulatory requirements Knowledge assessment to evaluate user's understanding of cryptocurrency risks before trading. User self-identifies their investor experience level to determine appropriate protections. Comply with tax reporting obligations. ### 3. Monitor status & verify capabilities View completion status of all required processes and identify any remaining blockers. See what actions the user can perform: deposits, trades, withdrawals, and transaction limits. Subscribe to KYC webhooks for real-time status updates. ## Best practices Don't request all information upfront. Start with basic processes (email, phone, profile, address) and collect sensitive documents only when needed for specific capabilities. Provide clear requirements for document quality, accepted types, and file formats. Reduces verification failures significantly. ## Periodic review Some processes require periodic review, in which their status may change to `pending`, signaling that the user must provide updated information. * `profile`: The user must confirm their profile information is still accurate. * `address`: The user must confirm their address is still accurate. * `identity`: The user must provide up-to-date identity when their underlying document is about to expire. * `customerDueDiligence`: The user must redo the form after a certain period of time. * `selfCategorizationStatement`: The user must redo the form after a certain period of time. **When:** The periodic review might be triggered at any time, but usually it happens between 1-3 years after the last submission or when important documents are about to expire, such as the identity document (typically 30-90 days before the document's expiration date). **How to handle:** 1. Monitor KYC webhooks to catch `core.kyc.*.status-changed` events in real time. 2. Collect and submit updated information. # Onboard individual users via the REST API Source: https://developer.uphold.com/developer-guides/user-onboarding/individual/via-api Step-by-step guide to onboarding individual users via the Uphold REST API: create the user, complete KYC processes, and unlock capabilities. This guide walks through the complete individual user onboarding flow using the REST API — from creating the user to confirming their capabilities are unlocked. ## Prerequisites * API client credentials with permission to create users and manage KYC. ## Walkthrough ```mermaid theme={null} sequenceDiagram autonumber participant U as Client App participant B as Your Backend participant A as Core API participant W as Webhooks U->>B: User provides country of residence B->>A: GET /core/terms-of-service?type=general&country={country} A-->>B: { termsOfService[] } B-->>U: Display Terms of Service U->>B: User accepts Terms of Service B->>A: POST /core/users (with ToS) A-->>B: { user } A-->>W: core.user.created B->>A: GET /core/kyc A-->>B: { processes with statuses } B->>A: PATCH /core/kyc/processes/email B->>A: PATCH /core/kyc/processes/phone B->>A: PATCH /core/kyc/processes/address B->>A: GET /core/kyc?detailed=profile + PATCH /core/kyc/processes/profile B->>A: POST /core/files + PATCH /core/kyc/processes/identity A-->>W: core.kyc.*.status-changed (for each process) B->>A: GET /core/capabilities A-->>B: { capabilities[] } B-->>U: User is ready to transact ``` ## Retrieve terms of service Before creating a user, retrieve the general Terms of Service applicable to their country of residence by calling the [List terms of service](/rest-apis/core-api/terms-of-service/list-terms-of-service) endpoint with `type=general` and the user's country code. ```http theme={null} GET /core/terms-of-service?type=general&country={country} ``` Display the Terms of Service content to the user and record their acceptance. ## Create the user Once the user has accepted the Terms of Service, call [Create user](/rest-apis/core-api/users/create-user) to register them on the platform. The `X-Uphold-User-Ip` [user context](/rest-apis/headers#user-context) header is **mandatory** when creating a user, as it records the user's IP address at the time of Terms of Service acceptance. ```http theme={null} POST /core/users { "type": "individual", "email": "john.doe@example.com", "citizenshipCountry": "GB", "country": "GB", "subdivision": "GB-MAN", "termsOfService": "general-gb-fca" } ``` A successful response returns the created user's information. ```json Response theme={null} { "user": { "id": "cd21b26d-35d2-408a-9201-b8fdbef7a604", "type": "individual", "email": "john.doe@uphold.com", "citizenshipCountry": "GB", "address": { "country": "GB", "subdivision": "GB-MAN" }, "createdAt": "2024-03-13T20:20:39Z", "updatedAt": "2024-03-13T20:20:39Z" } } ``` You can optionally include custom [entity metadata](/rest-apis/entity-metadata) in the `metadata` field to store your own business data (e.g. external IDs or tracking parameters). Subscribe to the `core.user.created` webhook to be notified asynchronously. ## Check required processes The KYC processes required for a user are determined by the Terms of Service they accepted at registration — different ToS codes map to different regulatory regimes with different requirements. After creating the user, call [Get KYC overview](/rest-apis/core-api/kyc/get-overview) to see the full list of applicable processes and their current statuses. ```http theme={null} GET /core/kyc ``` A successful response returns the list of processes with their statuses. Initially, all processes will typically be in `pending` status, indicating they are waiting for the user to provide information. ```json Response [expandable] theme={null} { "email": { "code": "email", "status": "pending", "verification": { "model": "partner-verified", "method": "manual", "dependencies": [] } }, "phone": { "code": "phone", "status": "pending", "verification": { "model": "partner-verified", "method": "manual", "dependencies": [] } }, "profile": { "code": "profile", "status": "pending", "verification": { "model": "uphold-verified", "method": "manual", "dependencies": [] } }, "address": { "code": "address", "status": "pending", "verification": { "model": "uphold-verified", "method": "manual", "dependencies": [] } }, "identity": { "code": "identity", "status": "pending", "verification": { "model": "partner-verified", "method": "manual", "dependencies": [ "phone", "profile", "address" ], "submittable": true } }, "proofOfAddress": { "code": "proof-of-address", "status": "pending", "verification": { "model": "partner-verified", "method": "manual", "dependencies": [ "phone", "profile", "address" ], "submittable": true } }, "customerDueDiligence": { "code": "customer-due-diligence", "status": "pending", "verification": { "model": "uphold-verified", "method": "manual", "dependencies": [] } }, "enhancedDueDiligence": { "code": "enhanced-due-diligence", "status": "exempt", "verification": { "model": "partner-verified", "method": "manual", "dependencies": [ "customer-due-diligence" ] } }, "cryptoRiskAssessment": { "code": "crypto-risk-assessment", "status": "exempt", "verification": { "model": "uphold-verified", "method": "manual", "dependencies": [] } }, "selfCategorizationStatement": { "code": "self-categorization-statement", "status": "exempt", "verification": { "model": "uphold-verified", "method": "manual", "dependencies": [] } }, "taxDetails": { "code": "tax-details", "status": "pending", "verification": { "model": "uphold-verified", "method": "manual", "dependencies": [ "profile", "address" ] } }, "screening": { "code": "screening", "status": "pending", "verification": { "model": "uphold-verified", "method": "automatic", "triggers": [ "profile", "identity" ], "dependencies": [] } }, "risk": { "code": "risk", "status": "pending", "verification": { "model": "uphold-verified", "method": "automatic", "dependencies": [] } } } ``` Processes with `status: "exempt"` are not required for this user's region. See the [regional requirements](/developer-guides/user-onboarding/individual/overview#regional-requirements) for the full requirements matrix per region. ## Complete KYC processes | Process | Category | | ------------------------------------------------------------- | ---------------------- | | [email](#email-and-phone), [phone](#email-and-phone) | Direct submission | | [profile](#profile) | Form-based | | [address](#address) | Direct submission | | [identity](#identity) | File-based | | [proofOfAddress](#proof-of-address) | File-based | | [customerDueDiligence](#customer-due-diligence) | Form-based | | [enhancedDueDiligence](#enhanced-due-diligence) | File-based | | [cryptoRiskAssessment](#crypto-risk-assessment) | Form-based | | [selfCategorizationStatement](#self-categorization-statement) | Form-based | | [taxDetails](#tax-details) | Form-based | | [screening](#screening-and-risk), [risk](#screening-and-risk) | Background (automatic) | Some processes have dependencies (`verification.dependencies`) that must be satisfied before submission: * `identity` and `proofOfAddress` depend on `phone`, `profile`, and `address` being completed — the data collected in these processes is used for data matching against the submitted documents. * `enhancedDueDiligence` is only triggered after `customerDueDiligence` returns a high-risk score — submit it only when prompted. * `taxDetails` depends on `profile` and `address` being completed — the user details are combined with tax information to correctly set up the user's tax configuration. * All other processes can be submitted in any order or in parallel. *** ### Email and phone Verify the user's email and phone number. Call [Update email](/rest-apis/core-api/kyc/update-email) and [Update phone](/rest-apis/core-api/kyc/update-phone) with `input` containing the email address and phone number, respectively, and `output` containing the verification datetime. ```http theme={null} PATCH /core/kyc/processes/email { "input": { "email": "john.doe@example.com" }, "output": { "verifiedAt": "2025-01-29T10:32:00Z" } } ``` A successful response returns the submitted information with an `ok` status. ```json Response theme={null} { "email": { "code": "email", "status": "ok", "verification": { "model": "partner-verified", "method": "manual", "dependencies": [] }, "input": { "email": "john.doe@example.com" }, "output": { "verifiedAt": "2025-01-29T10:32:00Z" } } } ``` This mode is under construction. Contact your Account Manager for the latest availability. *** ### Profile Declare the user's personal information, such as their name, date of birth, and citizenship. Call [Update profile](/rest-apis/core-api/kyc/update-profile) with `input.details` containing the relevant fields. Profile is a form-based process — the required fields may vary based on the user's region and other factors. Always check the returned `hint` object for the specific schema to display. See [Dynamic forms](/developer-guides/resources/dynamic-forms/introduction) for guidance on rendering the form and submitting the data. ```http theme={null} PATCH /core/kyc/processes/profile { "input": { "details": { "fullName": "John Doe", "primaryCitizenship": "GB", "birthdate": "1987-01-01" } } } ``` A successful response always includes a `hint` object with the form schema and UI schema — even when `status` is `ok` — so the user can update their information later. ```json Response theme={null} { "profile": { "code": "profile", "status": "ok", "verification": { "model": "uphold-verified", "method": "manual", "dependencies": [] }, "input": { "details": { "fullName": "John Doe", "primaryCitizenship": "GB", "birthdate": "1987-01-01" } }, "hint": { "type": "form", "schema": { "type": "object", "properties": { /* ... */ } }, "uiSchema": { "type": "Categorization", "elements": [ /* ... */ ] } } } } ``` *** ### Address Declare the user's residential address. Call [Update address](/rest-apis/core-api/kyc/update-address) with `input` containing the address fields. ```http theme={null} PATCH /core/kyc/processes/address { "input": { "country": "GB", "city": "Manchester", "line1": "1 High Street", "postalCode": "M4 1AA" } } ``` A successful response returns the submitted information with an `ok` status. ```json Response theme={null} { "address": { "code": "address", "status": "ok", "verification": { "model": "uphold-verified", "method": "manual", "dependencies": [] }, "input": { "address": { "country": "GB", "subdivision": "GB-MAN", "city": "Manchester", "line1": "1 High Street", "line2": "Northern Quarter", "postalCode": "M4 1AA" } } } } ``` *** ### Identity Verify that the user is the person they claim to be. Your organization performs identity verification through an already-approved method and submits the result to Uphold. Two sub-types are supported: Call [Create file](/rest-apis/core-api/files/create-file) for each document to obtain an upload URL. ```http theme={null} POST /core/files { "category": "image", "contentType": "image/png" } ``` A successful response returns an `upload` object with a presigned URL and form data for uploading the file to the provided storage service. ```json Response theme={null} { "file": { "id": "38cce774-b225-41ed-a9fd-f9c9777a13de", "status": "pending", "category": "image", "contentType": "image/png", "upload": { "url": "https://example.com/upload", "formData": { "X-Amz-Algorithm": "AWS4-HMAC-SHA256", "X-Amz-Credential": "ASIE4FQORBNQHNQD5CG6/20240801/us-east-1/s3/aws4_request", "X-Amz-Date": "20240801T102500Z", "X-Amz-Security-Token": "IQoJb3JpZ2luX2VjEIv//////////...JBr6h2HkzH/aFSArQcs=", "X-Amz-Signature": "b4da8cf1a59327988a8108c965a4789fc8ba2d20f5bffda076106b2b254def29", "Key": "4c13b6f5-987e-43df-bc12-042b58307a80", "Policy": "eyJjb25kaXRpb25zIjpbeyJidWN...TA6MjU6MDAuMjAyWiJ9" }, "expiresAt": "2024-03-13T20:20:39Z" } } } ``` Upload the file to the provided `upload.url` using the returned `upload.formData` fields as multipart form parameters. Call [Update identity](/rest-apis/core-api/kyc/update-identity), referencing the file IDs in `input.media` and setting `output` to the verification results from your provider. ```http theme={null} PATCH /core/kyc/processes/identity { "type": "document-submission", "input": { "media": [ { "context": "photo-document-front", "fileId": "470b2192-893f-4ce6-9daa-816d4c319a84" }, { "context": "photo-selfie", "fileId": "5d3a1c72-bd4f-4e2a-a123-9f8765432100" } ] }, "output": { "provider": "veriff", "document": { "type": "passport", "number": "7700225VH", "country": "GB", "expiresAt": "2026-03-13" }, "person": { "givenName": "John", "familyName": "Doe", "birthdate": "1987-01-01", "gender": "male" }, "verifiedAt": "2025-01-29T10:36:00Z" } } ``` A successful response returns the submitted information with an `ok` status. ```json Response theme={null} { "identity": { "code": "identity", "status": "ok", "type": "document-submission", "verification": { "model": "partner-verified", "method": "manual", "dependencies": ["phone", "profile", "address"], "submittable": false }, "input": { "media": [ { "context": "photo-document-front", "fileId": "470b2192-893f-4ce6-9daa-816d4c319a84" }, { "context": "photo-selfie", "fileId": "5d3a1c72-bd4f-4e2a-a123-9f8765432100" } ] }, "output": { "provider": "veriff", "document": { "type": "passport", "number": "7700225VH", "country": "GB", "expiresAt": "2026-03-13" }, "person": { "givenName": "John", "familyName": "Doe", "birthdate": "1987-01-01", "gender": "male" }, "verifiedAt": "2025-01-29T10:36:00Z" } } } ``` Your organization performs electronic identity verification through an already-approved provider that checks the user's declared information against public and third-party records, without requiring physical documents, and submits the result to Uphold. Call [Update identity](/rest-apis/core-api/kyc/update-identity), populating `input.data` with the user's declared information and `output` with the provider and verification datetime. ```http theme={null} PATCH /core/kyc/processes/identity { "type": "electronic-verification", "input": { "data": { "givenName": "John", "familyName": "Doe", "birthdate": "1987-01-01", "citizenshipCountry": "GB", "address": { "country": "GB", "city": "Manchester", "line1": "1 High Street", "postalCode": "M4 1AA" } } }, "output": { "provider": "onfido", "verifiedAt": "2025-01-29T10:36:00Z" } } ``` A successful response returns the submitted information with an `ok` status. ```json Response theme={null} { "identity": { "code": "identity", "status": "ok", "type": "electronic-verification", "verification": { "model": "partner-verified", "method": "manual", "dependencies": ["phone", "profile", "address"], "submittable": false }, "input": { "data": { "givenName": "John", "familyName": "Doe", "birthdate": "1987-01-01", "citizenshipCountry": "GB", "address": { "country": "GB", "city": "Manchester", "line1": "1 High Street", "postalCode": "M4 1AA" } } }, "output": { "provider": "onfido", "verifiedAt": "2025-01-29T10:36:00Z" } } } ``` This type does not unblock the same capabilities as document submission. See the [comparison table](/rest-apis/core-api/kyc/update-identity#when-to-use-document-submission-verification-vs-electronic-verification) for details. Uphold performs identity verification on the user's behalf. Two methods are supported: Launch the Uphold-hosted KYC Widget to collect identity documents and complete verification through a pre-built UI. Ingest an existing identity verification from your KYC provider (Sumsub, Veriff) without building a custom mapping layer. *** ### Proof of address Verify the user's residential address. The process may already be `ok` in certain regions (e.g., US) if the `identity` process was completed with an identity document containing an address. Your organization performs proof-of-address verification through a contracted provider and submits the result to Uphold. Two sub-types are supported: Call [Create file](/rest-apis/core-api/files/create-file) to obtain an upload URL for the document. ```http theme={null} POST /core/files { "category": "document", "contentType": "image/png" } ``` A successful response returns an `upload` object with a presigned URL and form data for uploading the file to the storage provider. ```json Response theme={null} { "file": { "id": "38cce774-b225-41ed-a9fd-f9c9777a13de", "status": "pending", "category": "document", "contentType": "image/png", "upload": { "url": "https://example.com/upload", "formData": { "X-Amz-Algorithm": "AWS4-HMAC-SHA256", "X-Amz-Credential": "ASIE4FQORBNQHNQD5CG6/20240801/us-east-1/s3/aws4_request", "X-Amz-Date": "20240801T102500Z", "X-Amz-Security-Token": "IQoJb3JpZ2luX2VjEIv//////////...JBr6h2HkzH/aFSArQcs=", "X-Amz-Signature": "b4da8cf1a59327988a8108c965a4789fc8ba2d20f5bffda076106b2b254def29", "Key": "4c13b6f5-987e-43df-bc12-042b58307a80", "Policy": "eyJjb25kaXRpb25zIjpbeyJidWN...TA6MjU6MDAuMjAyWiJ9" }, "expiresAt": "2024-03-13T20:20:39Z" } } } ``` Upload the file to the provided `upload.url` using the returned `upload.formData` fields as multipart form parameters. Call [Update proof-of-address](/rest-apis/core-api/kyc/update-proof-of-address), referencing the file ID in `input.media` and setting `output` to the verification results from your provider. ```http theme={null} PATCH /core/kyc/processes/proof-of-address { "type": "document-submission", "input": { "media": [ { "context": "address-proof", "fileId": "459c0447-8916-4bad-bb67-f2354fcfb10b" } ] }, "output": { "provider": "sumsub", "person": { "givenName": "John", "familyName": "Doe", "address": { "country": "GB", "subdivision": "GB-MAN", "city": "Manchester", "line1": "1 High Street", "line2": "Northern Quarter", "postalCode": "M4 1AA" } }, "verifiedAt": "2025-01-29T10:37:00Z" } } ``` A successful response returns the submitted information with an `ok` status. ```json Response theme={null} { "proofOfAddress": { "code": "proof-of-address", "status": "ok", "type": "document-submission", "verification": { "model": "partner-verified", "method": "manual", "dependencies": ["phone", "profile", "address"] }, "input": { "media": [ { "context": "address-proof", "fileId": "459c0447-8916-4bad-bb67-f2354fcfb10b" } ] }, "output": { "provider": "sumsub", "person": { "givenName": "John", "familyName": "Doe", "address": { "country": "GB", "subdivision": "GB-MAN", "city": "Manchester", "line1": "1 High Street", "line2": "Northern Quarter", "postalCode": "M4 1AA" } }, "verifiedAt": "2025-01-29T10:37:00Z" } } } ``` Some KYC providers can verify the user's address electronically, without requiring a physical document, by checking the declared address against public and third-party records. Call [Update proof-of-address](/rest-apis/core-api/kyc/update-proof-of-address), populating `input.data` with the user's declared information and `output` with the provider and verification datetime. ```http theme={null} PATCH /core/kyc/processes/proof-of-address { "type": "electronic-verification", "input": { "data": { "givenName": "John", "familyName": "Doe", "address": { "country": "GB", "subdivision": "GB-MAN", "city": "Manchester", "line1": "1 High Street", "line2": "Northern Quarter", "postalCode": "M4 1AA" } } }, "output": { "provider": "sumsub", "verifiedAt": "2025-01-29T10:37:00Z" } } ``` A successful response returns the submitted information with an `ok` status. ```json Response theme={null} { "proofOfAddress": { "code": "proof-of-address", "status": "ok", "type": "electronic-verification", "verification": { "model": "partner-verified", "method": "manual", "dependencies": ["phone", "profile", "address"] }, "input": { "data": { "givenName": "John", "familyName": "Doe", "address": { "country": "GB", "subdivision": "GB-MAN", "city": "Manchester", "line1": "1 High Street", "line2": "Northern Quarter", "postalCode": "M4 1AA" } } }, "output": { "provider": "sumsub", "verifiedAt": "2025-01-29T10:37:00Z" } } } ``` Uphold performs proof-of-address verification on the user's behalf. Two methods are supported: Launch the Uphold-hosted KYC Widget to collect proof-of-address documents and complete verification through a pre-built UI. Ingest an existing proof-of-address verification from your KYC provider (Sumsub, Veriff) without building a custom mapping layer. *** ### Customer due diligence Assess the user's financial profile and risk level based on their expected activities, source of funds, and other relevant information. Uphold performs customer due diligence checks based on the user's profile and declared intent. Call [Update customer due diligence](/rest-apis/core-api/kyc/update-customer-due-diligence) with `input` containing the answers for each form step, repeating until `status` returns `ok`. Customer due diligence is a form-based process with multiple steps. The required fields may vary based on the user's region, expected activities, and other factors. Always check the returned `hint` object for the specific schema to display. See [Dynamic forms](/developer-guides/resources/dynamic-forms/introduction) for guidance on rendering the form and submitting the data. ```http theme={null} PATCH /core/kyc/processes/customer-due-diligence { "input": { "formId": "ae80271a-a3f0-453b-8dc6-36b777817217", "answers": { "intent": { "expected-activities": [ "cryptocurrency_investing", "deposit_withdraw_crypto" ] } } } } ``` A successful response returns the submitted information with an `ok` status, along with the calculated risk score and verification datetime. ```json Response theme={null} { "customerDueDiligence": { "code": "customer-due-diligence", "status": "ok", "verification": { "model": "uphold-verified", "method": "manual", "dependencies": [] }, "input": { "formId": "ae80271a-a3f0-453b-8dc6-36b777817217", "answers": { "intent": { "expected-activities": [ "cryptocurrency_investing", "deposit_withdraw_crypto" ] }, "financial-information": { "source-of-funds": "salary", "annual-income-range": "40000-60000-GBP", "expected-annual-deposits-range": "0-5000-GBP" }, "employment": { "status": "employed" }, "employment-details": { "industry": "engineering" } } }, "output": { "score": "low", "expiresAt": "2025-01-01T00:00:00Z", "verifiedAt": "2022-01-01T00:00:00Z" } } } ``` **Understanding score** The `output.score` reflects the user's assessed risk level. The higher the score, the shorter the recollection period before the user must resubmit. **Understanding expiration** The `output.expiresAt` is the deadline by which the user must resubmit customer due diligence — it opens for recollection 60 days before that date, at which point the process status reverts to `pending`. The user remains approved during this window and can continue to operate without restrictions until the deadline is reached. Your organization collects the user's financial profile information, calculates the user's risk score, then submits the result to Uphold. Call [Update customer due diligence](/rest-apis/core-api/kyc/update-customer-due-diligence) with `input` containing the user's financial profile information and `output` containing the calculated risk score and verification datetime. ```http theme={null} PATCH /core/kyc/processes/customer-due-diligence { "input": { "answers": { "financial-profile": { "expected-activities": [ "cryptocurrency_investing" ], "source-of-funds": "salary", "annual-income-range": "40000-60000-GBP", "savings-and-investments-range": "0-5000-GBP", "employment-status": "employed", "employment-industry": "Engineering", "employment-occupation": "Galactic Vibe Curator" } } }, "output": { "score": "low", "expiresAt": "2026-01-01T00:00:00Z", "verifiedAt": "2023-01-01T00:00:00Z" } } ``` A successful response returns the submitted information with an `ok` status. ```json Response theme={null} { "customerDueDiligence": { "code": "customer-due-diligence", "status": "ok", "verification": { "model": "partner-verified", "method": "manual", "dependencies": [] }, "input": { "formId": "ae80271a-a3f0-453b-8dc6-36b777817217", "answers": { "financial-profile": { "expected-activities": [ "cryptocurrency_investing" ], "source-of-funds": "salary", "annual-income-range": "40000-60000-GBP", "savings-and-investments-range": "0-5000-GBP", "employment-status": "employed", "employment-industry": "Engineering", "employment-occupation": "Galactic Vibe Curator" } } }, "output": { "score": "low", "expiresAt": "2026-01-01T00:00:00Z", "verifiedAt": "2023-01-01T00:00:00Z" } } } ``` **Understanding expiration** Even though `output.expiresAt` is set by your organization, the process will automatically re-open for recollection 60 days before that date, reverting to `pending` until the user resubmits. The user remains approved during this window and can continue to operate without restrictions until the deadline is reached. *** ### Enhanced due diligence Enhanced due diligence is an additional layer of verification required for users with a high customer due diligence score. Your organization performs enhanced due diligence through an already-approved method, such as manual review of source-of-funds document (e.g. pay stub, bank statement, portfolio statement), and submits the result to Uphold. Call [Create file](/rest-apis/core-api/files/create-file) to obtain an upload URL for the source-of-funds document. ```http theme={null} POST /core/files { "category": "document", "contentType": "image/png" } ``` A successful response returns an `upload` object with a presigned URL and form data for uploading the file to the storage provider. ```json Response theme={null} { "file": { "id": "57a3ecf2-5404-4654-9ece-feb504a69f86", "status": "pending", "category": "document", "contentType": "image/png", "upload": { "url": "https://example.com/upload", "formData": { "X-Amz-Algorithm": "AWS4-HMAC-SHA256", "X-Amz-Credential": "ASIE4FQORBNQHNQD5CG6/20240801/us-east-1/s3/aws4_request", "X-Amz-Date": "20240801T102500Z", "X-Amz-Security-Token": "IQoJb3JpZ2luX2VjEIv//////////...JBr6h2HkzH/aFSArQcs=", "X-Amz-Signature": "b4da8cf1a59327988a8108c965a4789fc8ba2d20f5bffda076106b2b254def29", "Key": "4c13b6f5-987e-43df-bc12-042b58307a80", "Policy": "eyJjb25kaXRpb25zIjpbeyJidWN...TA6MjU6MDAuMjAyWiJ9" }, "expiresAt": "2024-03-13T20:20:39Z" } } } ``` Upload the file to the provided `upload.url` using the returned `upload.formData` fields as multipart form parameters. Call [Update enhanced due diligence](/rest-apis/core-api/kyc/update-enhanced-due-diligence), referencing the file ID in `input.media` and setting `output` containing the verification datetime. ```http theme={null} PATCH /core/kyc/processes/enhanced-due-diligence { "input": { "media": [ { "context": "source-of-funds-proof", "fileId": "57a3ecf2-5404-4654-9ece-feb504a69f86" } ] }, "output": { "verifiedAt": "2025-01-29T10:38:00Z" } } ``` A successful response returns the submitted information with an `ok` status. ```json Response theme={null} { "enhancedDueDiligence": { "code": "enhanced-due-diligence", "status": "ok", "verification": { "model": "partner-verified", "method": "manual", "dependencies": ["customer-due-diligence"] }, "input": { "media": [ { "context": "source-of-funds-proof", "fileId": "57a3ecf2-5404-4654-9ece-feb504a69f86" } ] }, "output": { "verifiedAt": "2025-01-29T10:38:00Z" } } } ``` This mode is under construction. Contact your Account Manager for the latest availability. *** ### Crypto risk assessment Assess the user's knowledge of cryptocurrency risks and their suitability for engaging in crypto-related activities. Uphold administers a crypto risk assessment quiz to the user, with a different set of questions per attempt, and evaluates the answers to determine whether the user is approved for crypto-related activities. Call [Update crypto risk assessment](/rest-apis/core-api/kyc/update-crypto-risk-assessment) with `input` containing the answers for each form step, repeating until `output` is updated. Crypto risk assessment is a form-based process with multiple steps. The required fields may vary based on the user's region, current attempt, and other factors. Always check the returned `hint` object for the specific schema to display. See [Dynamic forms](/developer-guides/resources/dynamic-forms/introduction) for guidance on rendering the form and submitting the data. ```http theme={null} PATCH /core/kyc/processes/crypto-risk-assessment { "input": { "formId": "7a0f4229-e3de-4dfd-8f91-9b1308b2dc33", "answers": { "decision-making": { "who-makes-investment-decisions": "i_decide" } } } } ``` A successful response returns the submitted information with an `ok` status, along with approval result, attempts used and remaining, and verification datetime. ```json Response theme={null} { "cryptoRiskAssessment": { "code": "crypto-risk-assessment", "status": "ok", "verification": { "model": "uphold-verified", "method": "manual", "dependencies": [] }, "input": { "formId": "7a0f4229-e3de-4dfd-8f91-9b1308b2dc33", "answers": { "decision-making": { "who-makes-investment-decisions": "i_decide" }, "loss-tolerance": { "acceptable-loss": "full_loss_possible" }, "volatility-understanding": { "expected-price-behaviour": "highly_volatile" }, "liquidity-awareness": { "selling-timeline": "may_be_delayed" }, "regulatory-protection": { "compensation-coverage": "no_coverage" }, "portfolio-allocation": { "share-of-net-assets": "small_share" }, "complexity-acknowledgement": { "product-mechanics": "complex_and_risky" } } }, "output": { "result": "approved", "attempts": { "used": 1, "maximum": 5 }, "verifiedAt": "2022-01-01T00:00:00Z" } } } ``` **Understanding result** When the `output.result` is `approved`, the user is cleared for crypto-related activities. When `rejected`, the user can either retry the quiz (when attempts remain) or will be offboarded (when the maximum attempts have been reached). **Understanding attempts** Use the `output.attempts` object to track how many attempts the user has made and the maximum allowed. When a cooldown is active, `retryAt` is present and indicates the earliest datetime the user can submit a new attempt. ```json theme={null} { "attempts": { "used": 1, "maximum": 5, "retryAt": "2025-01-29T12:00:00Z" } } ``` **Understanding offboarding** If a failed attempt occurs, the `output.offboard` object is present. `endsAt` indicates either the deadline by which the user must submit a new attempt (when attempts remain), or the expected offboarding datetime (when the maximum has been reached). `completedAt` is set to the effective datetime when the user was offboarded. ```json theme={null} { "offboard": { "endsAt": "2025-06-29T00:00:00Z", "completedAt": "2025-01-29T12:00:00Z" } } ``` Your organization administers a crypto risk assessment quiz to the user and submits the results to Uphold. Call [Update crypto risk assessment](/rest-apis/core-api/kyc/update-crypto-risk-assessment) with `input` containing the form answers and `output` containing the approval result, attempts used and remaining, and verification datetime. ```http theme={null} PATCH /core/kyc/processes/crypto-risk-assessment { "input": { "answers": { "crypto-quiz": { "decision-making": "i_decide", "loss-tolerance": "full_loss_possible", "volatility-understanding": "highly_volatile", "liquidity-awareness": "may_be_delayed", "regulatory-protection": "no_coverage", "portfolio-allocation": "small_share", "complexity-acknowledgement": "complex_and_risky" } } }, "output": { "result": "approved", "attempts": { "used": 1, "maximum": 5 }, "verifiedAt": "2023-01-01T00:00:00Z" } } ``` A successful response returns the submitted information with an `ok` status. ```json Response theme={null} { "cryptoRiskAssessment": { "code": "crypto-risk-assessment", "status": "ok", "verification": { "model": "partner-verified", "method": "manual", "dependencies": [] }, "input": { "formId": "7a0f4229-e3de-4dfd-8f91-9b1308b2dc33", "answers": { "crypto-quiz": { "decision-making": "i_decide", "loss-tolerance": "full_loss_possible", "volatility-understanding": "highly_volatile", "liquidity-awareness": "may_be_delayed", "regulatory-protection": "no_coverage", "portfolio-allocation": "small_share", "complexity-acknowledgement": "complex_and_risky" } } }, "output": { "result": "approved", "attempts": { "used": 1, "maximum": 5 }, "verifiedAt": "2023-01-01T00:00:00Z" } } } ``` *** ### Self-categorization statement Collect the user's self-declared financial profile and categorize them accordingly. Uphold administers a self-categorization statement form to the user, and evaluates the answers to determine the user's category. Call [Update self-categorization statement](/rest-apis/core-api/kyc/update-self-categorization-statement) with `input` containing the answers for each form step, repeating until `output` is updated. Self-categorization statement is a form-based process with multiple steps. The required fields may vary based on the user's region, category, and other factors. Always check the returned `hint` object for the specific schema to display. See [Dynamic forms](/developer-guides/resources/dynamic-forms/introduction) for guidance on rendering the form and submitting the data. ```http theme={null} PATCH /core/kyc/processes/self-categorization-statement { "input": { "formId": "ac33651f-f2d3-47c4-8e8d-06fb87361f5c", "answers": { "investor": { "type": "restricted_investor" } } } } ``` A successful response returns the submitted information with an `ok` status, along with the categorization result and verification datetime. ```json Response theme={null} { "selfCategorizationStatement": { "code": "self-categorization-statement", "status": "ok", "verification": { "model": "uphold-verified", "method": "manual", "dependencies": [] }, "input": { "formId": "ac33651f-f2d3-47c4-8e8d-06fb87361f5c", "answers": { "investor": { "type": "restricted_investor" }, "investor-profile": { "invested-less-than-10-percent": "yes", "invested-percentage": "5", "invest-less-than-10-percent": "yes", "invest-percentage": "5", "investor-type-confirmation": "yes" } } }, "output": { "result": "approved", "attempts": { "used": 1, "maximum": 30 }, "expiresAt": "2025-01-01T00:00:00Z", "verifiedAt": "2022-01-01T00:00:00Z" } } } ``` **Understanding result** When the `output.result` is `approved`, the user is cleared for the activities allowed for their category. When `rejected`, the user can either retry the statement (when attempts remain) or will be offboarded (when the maximum attempts have been reached). **Understanding attempts** Use the `output.attempts` object to track how many attempts the user has made and the maximum allowed. ```json theme={null} { "attempts": { "used": 1, "maximum": 30 } } ``` **Understanding expiration** The `output.expiresAt` is the deadline by which the user must resubmit the self-categorization statement — it opens for recollection 60 days before that date, at which point the process status reverts to `pending`. The user remains approved during this window and can continue to operate without restrictions until the deadline is reached. Your organization administers a self-categorization statement form to the user and submits the results to Uphold. Call [Update self-categorization statement](/rest-apis/core-api/kyc/update-self-categorization-statement) with `input` containing the form answers and `output` containing the categorization result, attempts used and remaining, expiration, and verification datetime. ```http theme={null} PATCH /core/kyc/processes/self-categorization-statement { "input": { "answers": { "investor-category": { "type": "high_net_worth_investor", "annual-income-above-100000": "yes", "annual-income": 120000, "net-assets-above-250000": "yes", "net-assets": 255000 } } }, "output": { "result": "approved", "attempts": { "used": 1, "maximum": 30 }, "expiresAt": "2026-01-01T00:00:00Z", "verifiedAt": "2023-01-01T00:00:00Z" } } ``` A successful response returns the submitted information with an `ok` status. ```json Response theme={null} { "selfCategorizationStatement": { "code": "self-categorization-statement", "status": "ok", "verification": { "model": "partner-verified", "method": "manual", "dependencies": [] }, "input": { "answers": { "investor-category": { "type": "high_net_worth_investor", "annual-income-above-100000": "yes", "annual-income": 120000, "net-assets-above-250000": "yes", "net-assets": 255000 } } }, "output": { "result": "approved", "attempts": { "used": 1, "maximum": 30 }, "expiresAt": "2026-01-01T00:00:00Z", "verifiedAt": "2023-01-01T00:00:00Z" } } } ``` **Understanding expiration** Even though `output.expiresAt` is set by your organization, the process will automatically re-open for recollection 60 days before that date, reverting to `pending` until the user resubmits. The user remains approved during this window and can continue to operate without restrictions until the deadline is reached. *** ### Tax details Collect the user's tax residency and tax identification information for tax reporting purposes. Uphold collects tax details through a two-step form. The first step asks for the user's countries of tax residency. Based on the response, the second step requests the required tax identification document numbers for each specified country. See [Dynamic forms](/developer-guides/resources/dynamic-forms/introduction) for guidance on rendering the form and submitting the data. Call [Update tax details](/rest-apis/core-api/kyc/update-tax-details) with the countries where the user is tax resident. The required information varies based on the user's region. Always check the form schema returned in the `hint` dynamically to determine which fields to collect. ```http theme={null} PATCH /core/kyc/processes/tax-details { "input": { "taxResidency": { "countries": ["GB"] } } } ``` The response returns `pending` with the next form step in `hint`, pre-filled with the required tax identification documents for each declared country. ```json Response theme={null} { "taxDetails": { "code": "tax-details", "status": "pending", "verification": { "model": "uphold-verified", "method": "manual", "dependencies": ["profile", "address"] }, "input": { "taxResidency": { "countries": ["GB"] }, "taxIdentification": { "documents": [ { "type": "tin", "country": "GB" } ] } }, "hint": { "type": "form", "schema": { "type": "object", "properties": { /* ... */ } }, "uiSchema": { "type": "Categorization", "elements": [ /* ... */ ] } } } } ``` Call [Update tax details](/rest-apis/core-api/kyc/update-tax-details) again with the tax identification document numbers and certification of the information's accuracy. The required information varies based on the user's region. Always check the form schema returned in the `hint` dynamically to determine which fields to collect. ```http theme={null} PATCH /core/kyc/processes/tax-details { "input": { "taxResidency": { "countries": ["GB"] }, "taxIdentification": { "documents": [ { "type": "tin", "country": "GB", "value": "1234567890" } ], "certify": true } } } ``` A successful response returns the submitted information with an `ok` status. The form is always returned — even when the process is `ok` — so the user can update their tax details when needed. ```json Response theme={null} { "taxDetails": { "code": "tax-details", "status": "ok", "verification": { "model": "uphold-verified", "method": "manual", "dependencies": ["profile", "address"] }, "input": { "taxResidency": { "countries": [ "GB" ] }, "taxIdentification": { "documents": [ { "type": "tin", "country": "GB", "number": "6405444702" } ], "certify": true } }, "output": { "result": "approved", "verifiedAt": "2022-01-01T00:00:00Z" }, "hint": { "type": "form", "schema": { "type": "object", "properties": { /* ... */ } }, "uiSchema": { "type": "Categorization", "elements": [ /* ... */ ] } } } } ``` This mode is under construction. Contact your Account Manager for the latest availability. *** ### Screening and risk Uphold automatically screens users against sanctions lists and assesses their risk level based on various factors, such as their location, transaction patterns, and other relevant data. ```json theme={null} { "screening": { "code": "screening", "status": "ok", "verification": { "model": "uphold-verified", "method": "automatic", "triggers": ["profile", "identity"], "dependencies": [] }, "output": { "result": "approved" } }, "risk": { "code": "risk", "status": "ok", "verification": { "model": "uphold-verified", "method": "automatic", "dependencies": [] }, "output": { "result": "approved" } } } ``` **Understanding result** When the `output.result` is `approved`, the user has passed screening or risk assessment. When `rejected`, the user has been flagged for potential issues and may require further review or offboarding. ## Monitor user onboarding Prefer **webhooks** for real-time updates, or fall back to **polling** if webhooks are not feasible. * Webhook events (recommended): * [core.user.created](/rest-apis/core-api/users/webhooks/user-created) * [core.kyc.\*.status-changed](/rest-apis/core-api/kyc/webhooks) * `status: pending` → waiting for the user to provide information * `status: running` → Uphold is processing the provided information * `status: ok` → the process has been successfully verified * `status: failed` → verification failed — the user may need to resubmit * `status: exempt` → the process is not required for this user * Polling (fallback): [Get KYC overview](/rest-apis/core-api/kyc/get-overview) ## Check capabilities Always check if the required capabilities for the user's intended activities are unlocked before allowing them to transact. Call [List capabilities](/rest-apis/core-api/capabilities/list-user-capabilities) to check restrictions and requirements for each capability. ```http theme={null} GET /core/capabilities ``` The response includes the list of capabilities with any outstanding `requirements` or `restrictions`. ```json Response theme={null} { "capabilities": [ { "key": "crypto-deposits", "enabled": true, "requirements": [], "restrictions": [] }, { "key": "crypto-withdrawals", "enabled": true, "requirements": [ "user-must-submit-identity" ], "restrictions": [] } ] } ``` The user is now onboarded and ready to transact. # Onboard individual users via the KYC Connector Source: https://developer.uphold.com/developer-guides/user-onboarding/individual/via-kyc-connector Use the Uphold KYC Connector to ingest verifications from Sumsub or Veriff, mapping provider payloads to KYC processes without bespoke integration work. The [KYC Connector](/rest-apis/kyc-connector-api/introduction) is an ingestion layer between third-party KYC providers and Uphold's platform. Instead of manually mapping provider-specific payloads to Uphold's KYC model, you create an **ingestion** and the KYC Connector handles extraction and submission on your behalf. The KYC Connector covers four processes: `profile`, `address`, `identity`, and `proofOfAddress`. Any remaining required processes (e.g. `customerDueDiligence`, `taxDetails`) must still be completed via the [REST API](/developer-guides/user-onboarding/individual/via-api). ## Prerequisites * API client credentials with permission to create users and create ingestions. * Your Sumsub account [must be configured](/rest-apis/kyc-connector-api/sumsub/overview#setup-requirements) for KYC data sharing. ## Walkthrough ```mermaid theme={null} sequenceDiagram autonumber participant U as Client App participant B as Your Backend participant A as Core API participant K as KYC Connector API participant S as KYC Provider participant W as Webhooks U->>B: User provides country of residence B->>A: GET /core/terms-of-service?type=general&country={country} A-->>B: { termsOfService[] } B-->>U: Display Terms of Service U->>B: User accepts Terms of Service B->>A: POST /core/users (with ToS + X-Uphold-User-Ip) A-->>B: { user } B-->>U: User completes verification (existing provider flow) U->>S: User completes KYC (identity, proof-of-address) S-->>B: Applicant approved (webhook or callback) B->>S: Generate share token for applicant S-->>B: { shareToken } B->>K: POST /kyc-connector/sumsub/ingestions (shareToken + processes) K-->>B: { ingestion } A-->>W: kyc-connector.sumsub.ingestion.status-changed (running → finished) B->>A: GET /core/kyc (verify process statuses) B->>A: PATCH /core/kyc/* (complete remaining processes via API) B->>A: GET /core/capabilities A-->>B: { capabilities[] } B-->>U: User is ready to transact ``` ## Create the user The user creation step is identical to the [REST API approach](/developer-guides/user-onboarding/individual/via-api#create-the-user): fetch the applicable Terms of Service, display them to the user, and call [Create User](/rest-apis/core-api/users/create-user): ```http theme={null} GET /core/terms-of-service?type=general&country={country} POST /core/users ``` The `X-Uphold-User-Ip` [user context](/rest-apis/headers#user-context) header is **mandatory** when creating a user. ## Collect KYC data Your existing KYC provider flow requires no changes. Once the user completes verification and the applicant is approved, generate a **share token** from your backend. This token authorizes Uphold to copy the applicant's KYC data from your provider account. ## Create an ingestion Call [Create ingestion](/rest-apis/kyc-connector-api/sumsub/create-ingestion) with the share token and the list of KYC processes you want to ingest. ```json theme={null} POST /kyc-connector/sumsub/ingestions { "shareToken": "_act-sbx-jwt-eyJhbGciOiJub25lIn0.eyJqdGkiOiJ0ZXN0IiwidXJsIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSJ9.", "processes": [ "identity", "proof-of-address" ] } ``` A successful response returns an `ingestion` object with details about the ingestion and its initial status: ```json Response theme={null} { "ingestion": { "id": "019b2184-1ca9-7dd6-b7a5-ec242def68b0", "userId": "123e4567-e89b-12d3-a456-426614174000", "status": "queued", "processes": [ "identity", "proof-of-address" ], "provider": "sumsub", "parameters": { "shareToken": "_act-sbx-jwt-eyJhbGciOiJub25lIn0.eyJqdGkiOiJ0ZXN0IiwidXJsIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSJ9." }, "createdAt": "2026-01-15T14:23:01.819Z", "updatedAt": "2026-01-15T14:23:01.819Z" } } ``` ## Monitor the ingestion Prefer **webhooks** for real-time updates, or fall back to **polling** if webhooks are not feasible. * Webhook events (recommended): * [kyc-connector.sumsub.ingestion.created](/rest-apis/kyc-connector-api/sumsub/webhooks/ingestion-created) * [kyc-connector.sumsub.ingestion.status-changed](/rest-apis/kyc-connector-api/sumsub/webhooks/ingestion-status-changed) * `status: queued` → ingestion created, waiting to be picked up * `status: running` → fetching and submitting provider data to Uphold * `status: finished` → processing complete * Polling (fallback): [Get ingestion](/rest-apis/kyc-connector-api/sumsub/get-ingestion) When the ingestion reaches `finished`, inspect the `result` field for each process. A `completed` result means the data was successfully extracted and submitted to Uphold — it does **not** mean the KYC process itself has been verified. Always confirm KYC process statuses using [Get KYC Overview](/rest-apis/core-api/kyc/get-overview). ## Complete onboarding process Once the ingestion finishes, the KYC Connector has submitted only the processes it supports (`profile`, `address`, `identity`, `proof-of-address`). Use [Get KYC Overview](/rest-apis/core-api/kyc/get-overview) to confirm which processes are now `ok` and identify what still needs to be collected. Any remaining required processes (e.g. `customerDueDiligence`, `taxDetails`) must still be completed. Check required processes and prompt the user to submit any outstanding ones. Call the relevant endpoints to submit the data and update process statuses until all are `ok`. Embed the KYC Widget to let the user complete the remaining processes through a pre-built UI. ## Check capabilities Always check if the required capabilities for the user's intended activities are unlocked before allowing them to transact. Call [List capabilities](/rest-apis/core-api/capabilities/list-user-capabilities) to check restrictions and requirements for each capability. ```http theme={null} GET /core/capabilities ``` The response includes the list of capabilities with any outstanding `requirements` or `restrictions`. ```json Response theme={null} { "capabilities": [ { "key": "crypto-deposits", "enabled": true, "requirements": [], "restrictions": [] }, { "key": "crypto-withdrawals", "enabled": true, "requirements": [ "user-must-submit-identity" ], "restrictions": [] } ] } ``` The user is now onboarded and ready to transact. ## Prerequisites * API client credentials with permission to create users, manage Veriff config, and create ingestions. * One or more Veriff integrations configured with the appropriate verification flows (IDV, PoA). ## Walkthrough ```mermaid theme={null} sequenceDiagram autonumber participant U as Client App participant B as Your Backend participant A as Core API participant K as KYC Connector API participant V as Veriff participant W as Webhooks B->>K: PUT /kyc-connector/veriff/config (one-time setup) K-->>B: { config } U->>B: User provides country of residence B->>A: GET /core/terms-of-service?type=general&country={country} A-->>B: { termsOfService[] } B-->>U: Display Terms of Service U->>B: User accepts Terms of Service B->>A: POST /core/users (with ToS + X-Uphold-User-Ip) A-->>B: { user } B-->>U: User completes verification (existing provider flow) U->>V: User completes KYC (identity, proof-of-address) V-->>B: Session approved (webhook or callback) B->>K: POST /kyc-connector/veriff/ingestions (sessions + processes) K-->>B: { ingestion } A-->>W: kyc-connector.veriff.ingestion.status-changed (running → finished) B->>A: GET /core/kyc (verify process statuses) B->>A: PATCH /core/kyc/* (complete remaining processes via API) B->>A: GET /core/capabilities A-->>B: { capabilities[] } B-->>U: User is ready to transact ``` ## Configure the provider Before creating any ingestions, you must register your Veriff integration credentials with Uphold. This is a **one-time setup** (or whenever you need to update credentials or integrations). Call [Set Veriff config](/rest-apis/kyc-connector-api/veriff/set-config) with your integration details: ```json theme={null} PUT /kyc-connector/veriff/config { "integrations": [ { "name": "my-idv-integration", "apiKey": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "sharedSecretKey": "f0e1d2c3-b4a5-6789-0fed-cba987654321", "processes": [ "profile", "identity" ] }, { "name": "my-poa-integration", "apiKey": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "sharedSecretKey": "e1d2c3b4-a596-7890-fedc-ba9876543210", "processes": [ "address", "proof-of-address" ] } ] } ``` You can configure multiple integrations if you use separate Veriff integrations for different processes (e.g. one for IDV, another for PoA). ## Create the user The user creation step is identical to the [REST API approach](/developer-guides/user-onboarding/individual/via-api#create-the-user): fetch the applicable Terms of Service, display them to the user, and call [Create User](/rest-apis/core-api/users/create-user): ```http theme={null} GET /core/terms-of-service?type=general&country={country} POST /core/users ``` The `X-Uphold-User-Ip` [user context](/rest-apis/headers#user-context) header is **mandatory** when creating a user. ## Collect KYC data Your existing Veriff verification flow requires no changes. Once the user completes verification and the session is approved, you will have the **session ID** ready to use. ## Create an ingestion Call [Create ingestion](/rest-apis/kyc-connector-api/veriff/create-ingestion) with the session details and the list of KYC processes you want to ingest. Each session references an **integration name** from your provider configuration and the **session ID** from Veriff. ```json theme={null} POST /kyc-connector/veriff/ingestions { "processes": [ "profile", "address", "identity", "proof-of-address" ], "sessions": [ { "sessionId": "550e8400-e29b-41d4-a716-446655440000", "integrationName": "my-idv-integration", "processes": [ "profile", "identity" ] }, { "sessionId": "661f9511-f30c-52e5-b827-557766551111", "integrationName": "my-poa-integration", "processes": [ "address", "proof-of-address" ] } ] } ``` A successful response returns an `ingestion` object with details about the ingestion and its initial status: ```json Response theme={null} { "ingestion": { "id": "019b2184-1ca9-7dd6-b7a5-ec242def68b0", "userId": "123e4567-e89b-12d3-a456-426614174000", "status": "queued", "processes": [ "profile", "address", "identity", "proof-of-address" ], "provider": "veriff", "parameters": { "sessions": [ { "sessionId": "550e8400-e29b-41d4-a716-446655440000", "integrationName": "my-idv-integration", "processes": [ "profile", "identity" ] }, { "sessionId": "661f9511-f30c-52e5-b827-557766551111", "integrationName": "my-poa-integration", "processes": [ "address", "proof-of-address" ] } ] }, "createdAt": "2026-01-15T14:23:01.819Z", "updatedAt": "2026-01-15T14:23:01.819Z" } } ``` ## Monitor the ingestion Prefer **webhooks** for real-time updates, or fall back to **polling** if webhooks are not feasible. * Webhook events (recommended): * [kyc-connector.veriff.ingestion.created](/rest-apis/kyc-connector-api/veriff/webhooks/ingestion-created) * [kyc-connector.veriff.ingestion.status-changed](/rest-apis/kyc-connector-api/veriff/webhooks/ingestion-status-changed) * `status: queued` → ingestion created, waiting to be picked up * `status: running` → fetching and submitting provider data to Uphold * `status: finished` → processing complete * Polling (fallback): [Get ingestion](/rest-apis/kyc-connector-api/veriff/get-ingestion) When the ingestion reaches `finished`, inspect the `result` field for each process. A `completed` result means the data was successfully extracted and submitted to Uphold — it does **not** mean the KYC process itself has been verified. Always confirm KYC process statuses using [Get KYC Overview](/rest-apis/core-api/kyc/get-overview). ## Complete onboarding process Once the ingestion finishes, the KYC Connector has submitted only the processes it supports (`profile`, `address`, `identity`, `proof-of-address`). Use [Get KYC Overview](/rest-apis/core-api/kyc/get-overview) to confirm which processes are now `ok` and identify what still needs to be collected. Any remaining required processes (e.g. `customerDueDiligence`, `taxDetails`) must still be completed. Check required processes and prompt the user to submit any outstanding ones. Call the relevant endpoints to submit the data and update process statuses until all are `ok`. Embed the KYC Widget to let the user complete the remaining processes through a pre-built UI. ## Check capabilities Always check if the required capabilities for the user's intended activities are unlocked before allowing them to transact. Call [List capabilities](/rest-apis/core-api/capabilities/list-user-capabilities) to check restrictions and requirements for each capability. ```http theme={null} GET /core/capabilities ``` The response includes the list of capabilities with any outstanding `requirements` or `restrictions`. ```json Response theme={null} { "capabilities": [ { "key": "crypto-deposits", "enabled": true, "requirements": [], "restrictions": [] }, { "key": "crypto-withdrawals", "enabled": true, "requirements": [ "user-must-submit-identity" ], "restrictions": [] } ] } ``` The user is now onboarded and ready to transact. # Onboard individual users via the KYC Widget Source: https://developer.uphold.com/developer-guides/user-onboarding/individual/via-kyc-widget Embed the Uphold KYC Widget to collect identity documents and KYC data from individuals without building a custom verification interface (in development). The KYC Widget is an embeddable UI component that handles the full KYC collection flow for individual users — from presenting forms to uploading identity documents — without requiring you to build and maintain a custom KYC interface. This widget is currently under development. If you are interested in using the KYC Widget to simplify the collection of KYC information from your end-users, please [reach out to us](https://uphold.com/enterprise#contact). In the meantime, you can onboard individual users using the [REST API](/developer-guides/user-onboarding/individual/via-api) or the [KYC Connector](/developer-guides/user-onboarding/individual/via-kyc-connector). # User onboarding overview — KYC, KYB, and capabilities Source: https://developer.uphold.com/developer-guides/user-onboarding/overview Establish user eligibility and compliance readiness with Uphold's onboarding model. Compare KYC and KYB methods, verification responsibility, and capabilities. Onboarding establishes users' **eligibility** and **compliance readiness** so they can access regulated financial actions on the Uphold Platform. ## Implementation decisions Before you start building, you must make two key decisions that define your system architecture, compliance responsibilities, and operational complexity: 1. **Which onboarding method you'll use** 2. **Who is responsible for verifying user information** ## 1. Choose your implementation method * You want to maintain control over the UI * You have specific branding requirements * You need a deep system integration * You want to maintain control over the UI * You already use Sumsub or Veriff * You don't want to modify your existing onboarding flows * You're comfortable with a managed UI * You need the fastest time-to-market * You have limited development resources ### Compare implementation methods | Characteristics | API | Connector | Widget | | -------------------- | --------------------------------------------------- | --------------------------------------------------- | --------------------------- | | **What you build** | Complete UI + backend orchestration using Core APIs | UI + provider integration | Embed widget component | | **Development time** | High (weeks-months) | Medium (1-2 weeks) | Low (days-weeks) | | **UX control** | Full control | Full control | Limited (Uphold-managed UI) | | **Maintenance** | You maintain all components | You maintain the UI, Uphold maintains the connector | Uphold maintains the widget | | **Customization** | Full UI customization | Full UI customization | Widget configuration only | ## 2. Choose verification responsibility When onboarding users, you must also decide who will be responsible for verifying the information collected. ### Verification models | Comparison | **Uphold-verified** | **Partner-verified** | | ----------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | Description | You collect the user data and Uphold verifies it. | You collect and verify user data, then submit the results to Uphold. | | Best if | You don't want to build or maintain verification systems. | You have existing onboarding infrastructure, or want full control over the user experience. | | Compliance | Uphold is responsible for compliance as the verifier. | Your organization is responsible for compliance as the verifier. | | Approvals | No approvals needed. | The Uphold compliance team must approve your verification processes. | ## Supported regions The Enterprise API Suite currently supports users in the following regions: * **United Kingdom** * **United States of America** * **European Union** Coming soon * **Rest of the world** Coming soon Contact your Account Manager to have more details about EU and rest of the world. ## Next Steps Complete step-by-step guide for Individual user verification. Businesses Coming soon } icon="briefcase" href="/developer-guides/user-onboarding/business/overview"> Complete step-by-step guide for Business user verification. # Introduction to the FIX APIs Source: https://developer.uphold.com/fix-apis/introduction If you are interested in using this API for pricing and trading, please [reach out to us](https://uphold.com/enterprise#contact). # Make your first API call Source: https://developer.uphold.com/get-started/make-your-first-api-call Make your first Uphold Enterprise API call using the public Postman workspace, with pre-configured Sandbox and Production environments and example requests. The fastest way to make your first API call is through our public [Postman workspace](https://www.postman.com/uphold/workspace/enterprise-api). It includes pre-configured requests for every endpoint, automated scripts that chain variables between calls, and ready-to-use Sandbox and Production environments — no boilerplate required. Open the [Enterprise API Postman workspace](https://www.postman.com/uphold/workspace/enterprise-api) and fork the collection to your own Postman account. Forking gives you a personal copy you can edit freely, and lets you pull upstream updates as the collection evolves. Enterprise API Postman workspace In the top-right corner of Postman, open the environment selector and choose the environment you want to target: * **Sandbox** — isolated testing environment with test funds and data. Recommended for development. * **Production** — live environment. Targeting Production may result in unintended changes to live data. Always validate against Sandbox first. Click the **Variables icon** next to the environment selector to open the variables drawer and set the following: | Variable | Value | | -------------------- | --------------------------------------------- | | `auth.client_id` | Your Client ID from the Enterprise Portal | | `auth.client_secret` | Your Client Secret from the Enterprise Portal | Postman variables drawer with client credentials If you are using the public workspace without forking, please be aware that credentials must be set through the variables drawer as shown above. Editing them via the Environments tab is not supported since the workspace is read-only. Don't have credentials yet? Follow the [Set up your account](/get-started/set-up-your-account) guide to create your API client. Lift off! Start with the [Authentication token](https://www.postman.com/uphold/workspace/enterprise-api/request/34958229-01b72490-f8c2-47c4-9449-83cb06726948?action=share\&source=copy-link\&creator=42626858\&active-environment=27bc7058-5bbb-457e-9234-c1abcba54ea8) request. It exchanges your client credentials for an access token. Once you send it, the collection scripts automatically store the token in the `auth.access_token` variable — subsequent requests will use it without any manual steps. The same applies throughout the collection: resource IDs (e.g. `core.user.id`, `core.account.id`, `core.transaction.id`) are captured after create/fetch operations and reused in follow-up requests automatically. This means you can work through a full flow — authenticate, create a user, create a quote, execute a transaction — by running requests in sequence without manually copying values between them. # Get started with the Uphold Enterprise API Suite Source: https://developer.uphold.com/get-started/overview Build enterprise-grade crypto products with the Uphold Enterprise API Suite. Find quickstart links to set up your account and make your first API call. Build enterprise-grade crypto products with a comprehensive API suite designed for financial institutions. Everything you need to make your first API call. * [Set up your account](/get-started/set-up-your-account) * [Make your first API call](/get-started/make-your-first-api-call) End-to-end, step-by-step walkthroughs for key flows. * [User onboarding](/developer-guides/user-onboarding) * [Money movement](/developer-guides/overview#money-movement) Full reference for all available APIs and endpoints. * [Core API](/rest-apis/core-api) * [KYC Connector API](/rest-apis/kyc-connector-api) Need to move fast? Low-code? There's a solution for you. * [Payment Widget](/widgets/payment) * [KYC Widget](/widgets/kyc) Coming soon # Set up your Uphold Enterprise account Source: https://developer.uphold.com/get-started/set-up-your-account Register your organization in the Enterprise Portal to access the Sandbox and Production environments, manage credentials, and start integrating the APIs. Before you can interact with the Enterprise API Suite, you must onboard through the [**Enterprise Portal.**](https://portal.enterprise.uphold.com/) The Enterprise Portal is your central mission control to manage the **Sandbox** and **Production** environments. Navigate to the [Enterprise Portal](https://portal.enterprise.uphold.com/register). Fill in your company details and set your administrator credentials. Once logged in, you have immediate access to the **Sandbox environment**. This is a fully isolated testing ground that mirrors Production behavior but uses test funds and data, allowing you to validate your integration safely. In the portal sidebar, navigate to **Clients** and click **Create new client**. Give your client a name (e.g., "Development App") and select the **Scopes** (permissions) your application needs. Enterprise Portal client creation Click **Generate client secret** to create your `Client ID` and `Client Secret`. Your credentials are ready. Now make your first API call using the Postman collection or cURL. # Authentication Source: https://developer.uphold.com/rest-apis/authentication Authenticate Uphold REST API requests with OAuth2 access tokens. Includes token endpoints, scopes, and unauthorized error handling for 401 responses. All requests must be authenticated or they will be rejected with a `401 Unauthorized` status code. ## OAuth2 The REST APIs use the industry standard [OAuth2](https://oauth.net/2/) protocol for authentication. It's a well defined and widely used specification for token-based authentication and authorization. ### Grant types Because the REST APIs are meant to be consumed by businesses, the supported grant type is [client credentials](https://oauth.net/2/grant-types/client-credentials/). For this grant, you need to provide a valid client ID and client secret to create access tokens. To obtain an access token, you may call the [Request OAuth2 token](./core-api/authentication/request-oauth2-token) endpoint. You can then use access tokens to authenticate subsequent requests by adding the `Authorization: Bearer {accessToken}` HTTP header. You can manage your clients in Enterprise Portal. Please note that client secrets act like passwords, so be sure to keep them secure! ### Subjects Calls to the REST API endpoints are always contextualized with a subject. A subject represents the actor performing the action, which can be one of the following: * `client`: The OAuth2 client itself, used for operations that don't require user context * `user:individual`: An individual user within an organization * `user:business`: A business user within an organization Depending on your client configuration, the client's tokens may be able to target different subjects: * **Organization-wide clients** default to the `client` subject but can act on behalf of any user within an organization, provided they have the `core.users:act-on-behalf-of` scope. Add the `X-On-Behalf-Of: user {userId}` HTTP header to the request, where `{userId}` is the ID of the user you want to target. * **Single-user clients** are associated with a specific user within the organization and can only perform actions on behalf of that user. No additional header is required for these clients. However, you may optionally include the `X-On-Behalf-Of: user {userId}` HTTP header, but the `{userId}` must match the user associated with the client. ### Scopes Clients are associated with a set of scopes that define the permissions of tokens. This allows you to create as many clients as needed, each with a different set of permissions based on your requirements. If you attempt to call an endpoint that requires certain scopes, but the token you are using doesn't have them, you will receive a `403 Forbidden` status code. ## API keys API keys are another widely used way to authenticate requests, but they are not supported at this time. If you have a use case that requires API keys, please reach out to your Account Manager. ## User blocked If a user is internally blocked, every request to non `GET` endpoints will fail with `409` HTTP status code and the following body: ```json theme={null} { "code": "operation-not-allowed", "message": "The current status of the user does not allow calling this endpoint", "reasons": ["user-blocked"] } ``` # REST API changelog Source: https://developer.uphold.com/rest-apis/changelog Track Uphold REST API releases, breaking changes, enhancements, and new features. Includes RSS feed for KYC, transactions, and other endpoint updates. ### KYC verification model **Summary** Every KYC process is now explicit about who is responsible for verifying it — replacing the previous implicit authoritativeness model. **Details** * **Verification object:** Every KYC process now includes a `verification` object with `model` (`uphold-verified` or `partner-verified`), `method` (`manual` or `automatic`), `triggers`, and `dependencies`. * **`uphold-verified` for `identity` and `proofOfAddress`:** These processes now support the `uphold-verified` model, meaning verification is driven by a third-party provider session hosted by Uphold. No explicit update can be made — Uphold is notified by the provider and updates the process automatically. **Documentation** * [KYC verification](/rest-apis/core-api/kyc/introduction#verification) ### Crypto risk assessment updated for AAQ v3 **Summary** The Crypto Risk Assessment form for UK users has been refreshed to align with the latest revision of the FCA's appropriateness assessment requirements (AAQ v3). Because the form is delivered as a [dynamic form](/developer-guides/resources/dynamic-forms/introduction), integrations that render `hint.schema` and `hint.uiSchema` pick up the new questionnaire automatically. **Details** * New users complete the updated AAQ v3 questionnaire. * Users who already completed the previous version are not required to reassess. **Documentation** * [Update crypto risk assessment](/rest-apis/core-api/kyc/update-crypto-risk-assessment) * [Dynamic forms](/developer-guides/resources/dynamic-forms/introduction) ### Transaction and portfolio statements **Summary** Added two new endpoints to retrieve monthly statements for a user's portfolio holdings and transactions. **Details** * **Portfolio statement**: Returns holdings per asset at the end of a given period, including exchange rates. * **Transactions statement**: Returns a paginated list of transactions for a given period, with full transaction details and exchange rates. * Both endpoints accept `year`, `month`, and an optional `denomination` parameter (single asset code, defaults to `USD`) to control the currency used for exchange rates. **Documentation** * [Get portfolio statement](/rest-apis/core-api/statements/get-portfolio-statement) * [Get transactions statement](/rest-apis/core-api/statements/get-transactions-statement) * [Statements developer guide](/developer-guides/statements/overview) ### Bank address destination node on ACH withdrawals **Summary** ACH withdrawal transactions now support bank-address as a destination node, allowing withdrawals without a linked external account (e.g., microdeposit disbursements during bank account ownership verification). **Documentation** * [Transaction nodes](/rest-apis/core-api/transactions/introduction#transaction-nodes) **Action required** * 🔍 Update your integration to handle ACH withdrawal transactions where `transaction.destination.node.type` is `bank-address`. ### Dynamic forms: restructured options with field dependencies and validation rules **Summary** The dynamic forms used in KYC processes have been enhanced with new capabilities and a restructured options format. These changes affect the `profile` and `taxDetails` processes. **Details** * **Restructured `data` option:** The `dataSource` and `exclude` options have been restructured under a `data` object with `source` and `exclude` properties, with `subdivisions` added as a new supported source alongside `countries`. * **New `format` option:** Custom display formats are now defined in the UI Schema via `options.format` (e.g., `postal-code` for postal code inputs). Standard formats like `date` remain in the JSON Schema. * **New `rules` option:** Controls can now define client-side validation rules such as `difference-greater-than-or-equal-to-threshold` and `difference-less-than-or-equal-to-threshold`, enabling constraints like age validation. * **New `dependsOn` option:** Controls can declare dependencies on other fields, enabling dynamic updates (e.g., subdivision list updates when country changes). **Documentation** * [Dynamic Forms — UI Schema](/developer-guides/resources/dynamic-forms/ui-schema#custom-options) * [Dynamic Forms — Schema](/developer-guides/resources/dynamic-forms/schema) * [Dynamic Forms — Rendering](/developer-guides/resources/dynamic-forms/rendering) **Action required** * ⚠️ **Breaking Change:** The `dataSource` option has been replaced by `data.source`, and `exclude` is now nested under `data.exclude`. Update your custom renderers to use the new structure. * 🔍 Implement support for the new `rules`, `dependsOn`, and `format` options in your custom renderers. ### Limit details on transaction amount errors **Summary** Transaction amount validation errors now include a `limit` object in the error details, providing clearer information about which limit was hit and what the allowed bounds are. * **Maximum and minimum amount limits**: The `limit` object now contains `maximumAmount` or `minimumAmount`, indicating the allowed bound for the transaction. * **Periodic limits**: When a daily, weekly, or monthly spending limit is exceeded, the API now returns a `transaction_amount_invalid` error. The `rule` field encodes the period (e.g. `amount-exceeds-daily-limit`) and the `limit` object includes both the total allowed amount (`maximumAmount`) and the remaining allowance for the period (`remainingAmount`). **Documentation** * [Create quote](/rest-apis/core-api/transactions/create-quote) * [Create transaction](/rest-apis/core-api/transactions/create-transaction) * [Transaction amount errors](/rest-apis/core-api/transactions/introduction#transaction-amount-errors) **Action required** * ⚠️ **Deprecation:** Update your integrations to use `limit.maximumAmount` or `limit.minimumAmount` instead of `threshold.value` in `transaction_amount_invalid` error details. * The `threshold` field is still returned but will be removed in a future release. ### Simulate crypto deposit **Summary** A new test helper endpoint is now available to simulate a crypto deposit in Sandbox. This endpoint allows you to test how your application handles incoming crypto deposits without needing to perform an actual testnet transfer. **Documentation** * [Simulate crypto deposit](/rest-apis/core-api/accounts/test-helpers/simulate-crypto-deposit) * [Testnets and token faucets](/rest-apis/core-api/accounts/test-helpers/fund-sandbox-accounts) ### SEPA deposits and withdrawals **Summary** Introduced support for deposits and withdrawals via the SEPA network. **Details** * **Deposit method**: Added support for SEPA deposits. * **External accounts**: SEPA accounts are automatically linked after the first successful deposit. * **Withdrawals**: Quotes and transactions now support linked SEPA accounts as a destination. * **Simulate bank deposit**: Added support for simulating SEPA deposits in the Sandbox environment. **Documentation** * [Networks and rails](/rest-apis/core-api/assets/introduction) * [Set up account deposit method](/rest-apis/core-api/accounts/set-up-account-deposit-method) * [External account support](/rest-apis/core-api/external-accounts/introduction#types-of-external-accounts) * [Simulate bank deposit](/rest-apis/core-api/accounts/test-helpers/simulate-bank-deposit) ### ACH deposits and withdrawals **Summary** Introduced support for deposits and withdrawals via the Automated Clearing House (ACH), along with the FedNow and Wire US bank rails. **Details** * **Account deposit method**: USD accounts can now be set up with a bank deposit method using US bank rails (ACH, FedNow, Wire). * **External accounts**: US bank accounts can now be added as external accounts for withdrawals. * **Create quote**: The external account node now accepts an optional `network` field to select a specific US bank rail when the external account supports multiple networks. * **Transaction nodes**: Bank deposits produce a `bank-address` node that identifies the network used for the transfer. * **Rail constraints**: Bank rails now support an `allowed-deposit-accounts` constraint to restrict deposits to the user's default account. **Documentation** * [Networks and rails](/rest-apis/core-api/assets/introduction) * [Set up account deposit method](/rest-apis/core-api/accounts/set-up-account-deposit-method) * [Create external account](/rest-apis/core-api/external-accounts/create-external-account) * [Create quote](/rest-apis/core-api/transactions/create-quote) * [Transactions](/rest-apis/core-api/transactions/introduction) ### Proof-of-address support for Veriff in KYC Connector API **Summary** The Veriff KYC Connector now supports the `proof-of-address` process. Proof-of-address data (e.g. utility bills, bank statements) can now be ingested from Veriff PoA sessions, in addition to the previously supported `profile`, `address`, and `identity` processes. **Documentation** * [Veriff provider overview](/rest-apis/kyc-connector-api/veriff/overview) * [Onboarding via KYC Connector](/developer-guides/user-onboarding/individual/via-kyc-connector) ### Filter assets and networks by type **Summary** The [List Assets](/rest-apis/core-api/assets/list-assets) and [List Networks](/rest-apis/core-api/assets/list-networks) endpoints now support a `type` query parameter, allowing results to be filtered by type (e.g. `crypto`, `fiat`). **Documentation** * [List assets](/rest-apis/core-api/assets/list-assets) * [List networks](/rest-apis/core-api/assets/list-networks) ### Custom TTL when creating quotes **Summary** Partners can now specify a custom `ttl` when creating a quote, changing the platform default. This is useful when processing deposits or withdrawals through your own rails (e.g. a custom card processor), where the user needs more time to complete the transaction before the quote expires. This feature must be enabled per organization — contact your Account Manager to request access. **Documentation** * [Create quote — TTL](/rest-apis/core-api/transactions/create-quote#ttl) ### Errors with `date_invalid` code now use hyphenated `details.rule` values **Summary** Error responses with the `date_invalid` code have been updated to use hyphenated values in the `details.rule` field for better consistency. This change affects all endpoints that return `date_invalid` errors. **Details** The following rule values have changed: | Before | After | | ----------------------------------------------- | ----------------------------------------------- | | `difference_greater_than_threshold` | `difference-greater-than-threshold` | | `difference_greater_than_or_equal_to_threshold` | `difference-greater-than-or-equal-to-threshold` | | `difference_less_than_threshold` | `difference-less-than-threshold` | | `difference_less_than_or_equal_to_threshold` | `difference-less-than-or-equal-to-threshold` | **Action required** * ⚠️ **Breaking Change:** Update any error handling logic that matches on `details.rule` values to use the new hyphenated format. ### List default accounts endpoint **Summary** Added an endpoint to list the default account for each asset owned by a user. Results can be filtered by asset code using the `asset` query parameter. Default accounts are where funds are credited when a push deposit does not include a `reference`, or when the network does not support targeting a specific account. **Documentation** * [List default accounts](/rest-apis/core-api/accounts/list-default-accounts) * [Default accounts](/rest-apis/core-api/accounts/introduction#default-accounts) ### Veriff provider support in KYC Connector API **Summary** Added Veriff as a new KYC provider in the KYC Connector API. Partners can now ingest and normalize KYC data from Veriff, in addition to the existing Sumsub support. **Details** * **Supported processes:** `profile`, `address`, and `identity`. * **Configuration management:** New endpoints to get and set Veriff integration configuration, including session-to-process mapping and Veriff API credentials. * **Webhooks:** Real-time notifications for ingestion lifecycle events (`ingestion-created` and `ingestion-status-changed`). **Documentation** * [Veriff provider overview](/rest-apis/kyc-connector-api/veriff/overview) * [Create Veriff ingestion](/rest-apis/kyc-connector-api/veriff/create-ingestion) * [Get Veriff configuration](/rest-apis/kyc-connector-api/veriff/get-config) ### List default accounts endpoint **Summary** Added an endpoint to list the default account for each asset owned by a user. Results can be filtered by asset code using the `asset` query parameter. Default accounts are where funds are credited when a push deposit does not include a `reference`, or when the network does not support targeting a specific account. **Documentation** * [List default accounts](/rest-apis/core-api/accounts/list-default-accounts) * [Default accounts](/rest-apis/core-api/accounts/introduction#default-accounts) ### Delete user endpoint requires review information **Summary** The [Delete User](/rest-apis/core-api/users/delete-user) endpoint now requires a request body with review information when deleting a user account. **Details** The request body must include a `review` object with the following properties: * `reason` (required): The reason code for deleting the user account. Must be one of: `other`, `closure-per-user-request`, `compliance-violation`, `fraud-violation`, `business-decision`. * `note` (optional): Additional notes about the account deletion. **Documentation** * [Delete User](/rest-apis/core-api/users/delete-user) **Action required** * ⚠️ **Breaking Change:** The endpoint now requires a request body with a `review` object. Update all calls to include `review.reason` (required) and optionally `review.note` in the request body. ### New asset information endpoint **Summary** Added asset information endpoint with descriptive details about each asset. The response supports locale selection via the `Accept-Language` header and format selection via the `format` query parameter (`text` by default, or `html`). The Market Pulse API endpoints have been reorganized on the documentation for better discoverability, but the paths and schemas remain unchanged. **Documentation** * [Get asset information](/rest-apis/market-pulse-api/assets/get-asset-information) ### News articles limit parameter **Summary** Changed default news articles to 10 and added a `limit` query parameter allowing partners to control the number of articles returned. The parameter accepts values between 1 and 10. **Documentation** * [List general news](/rest-apis/market-pulse-api/general/list-general-news) * [List asset news](/rest-apis/market-pulse-api/assets/list-asset-news) ### Update set metadata response code **Summary** We've updated the set metadata endpoint to return a 200 response code and the resulting metadata. ### Market Pulse API **Summary** Introduced the Market Pulse API, providing partners with access to real-time market insights and latest news. **Documentation** * [List asset news](/rest-apis/market-pulse-api/assets/list-asset-news) * [List general news](/rest-apis/market-pulse-api/general/list-general-news) * [Get asset statistics](/rest-apis/market-pulse-api/assets/get-asset-statistics) ### New KYC Connector API **Summary** We've launched the KYC Connector API, a new integration layer that connects third-party KYC providers with Uphold's platform. This API eliminates the need to build and maintain custom integrations by automatically ingesting and normalizing provider data into Uphold's KYC model for processes like profile, address, identity, and proof-of-address verification. **Details** * **Workflow-based ingestion:** Create ingestions that represent a KYC ingestion workflow for a given user and track its status via polling or webhooks. * **Multi-process support:** Ingest profile, address, identity, and proof-of-address KYC processes in a single workflow. * **Multi-provider support:** Ingest from multiple providers, starting with Sumsub support in this release and more providers coming soon. **Documentation** * [KYC Connector API Introduction](/rest-apis/kyc-connector-api/introduction) * [Sumsub provider](/rest-apis/kyc-connector-api/sumsub/overview) ### Deprecated `amount-limit-exceeded` transaction status reason code **Summary** The transaction status reason code `amount-limit-exceeded` has been deprecated and replaced with `provider-maximum-limit-exceeded` to provide clearer error information when transactions fail due to provider-imposed limits. **Documentation** * [Transactions](/rest-apis/core-api/transactions/introduction) **Action required** * ⚠️ **Breaking Change:** Update your integrations to check for `provider-maximum-limit-exceeded` instead of `amount-limit-exceeded` in the `statusDetails.reason` field of failed transactions. * ❌ The `amount-limit-exceeded` reason code will no longer be returned. ### Business user support in Widgets **Summary** Business user support is now available across all widgets, enabling partners to [create sessions](/rest-apis/widgets-api/payment/create-session) and embed widget experiences for business users as well as individuals. The [Capabilities endpoints](/rest-apis/core-api/capabilities/introduction) have also been enhanced to reflect business user permissions and features. Please note that for the [Payment Widget](/widgets/payment/introduction), card withdrawals and bank transfers for business users will be supported in a future release. **Documentation** * [Payment Widget](/widgets/payment/introduction) * [Travel Rule Widget](/widgets/travel-rule/introduction) * [Create session](/rest-apis/widgets-api/payment/create-session) * [Capabilities endpoints](/rest-apis/core-api/capabilities/introduction) ### Metadata support in the API **Summary** Introduced new endpoints to manage metadata for API resources. Partners can now programmatically create, update, retrieve, and delete custom metadata for supported entities, enabling more flexible integrations and resource annotations. This feature allows you to attach arbitrary key-value data to resources, such as accounts or users, to support custom workflows, tagging, or additional business logic. **Documentation** * [Entity metadata](/rest-apis/entity-metadata) * [Core API metadata endpoints](/rest-apis/core-api/metadata) ### Travel Rule compliance for crypto transactions **Summary** Added Travel Rule support in the Core API: endpoints and response hints to collect the required originator and beneficiary information for crypto deposits and withdrawals. **Details** * **Transaction RFIs:** New endpoints to [get](/rest-apis/core-api/transactions/rfis/get-request-for-information), [list](/rest-apis/core-api/transactions/rfis/list-requests-for-information) and [update](/rest-apis/core-api/transactions/rfis/update-request-for-information) requests for information (RFIs) for crypto deposits that require Travel Rule data. * **Quote Requirements:** Quote responses may include a `travel-rule` requirement when additional information is needed before executing a crypto withdrawal. **Documentation** * [Transactions](/rest-apis/core-api/transactions/introduction) — support for transactions subject to the Travel Rule * [Create session](/rest-apis/widgets-api/travel-rule/create-session) — create `deposit-form` and `withdrawal-form` sessions for the Travel Rule widget; see the [Travel Rule Widget](/widgets/travel-rule/installation-and-setup) for installation and setup instructions. **Actions required** * 🔔 Monitor [`core.transaction.status-changed`](/rest-apis/core-api/transactions/webhooks/transaction-status-changed) for transactions with status `on-hold`. * 🔍 Check for `travel-rule` requirements in quote responses when creating crypto withdrawal quotes. ### Validate network address endpoint **Summary** Introduced a new endpoint to programmatically validate the format of a network address before initiating a transaction. This helps ensure only valid crypto addresses are used, reducing the risk of user errors and failed transfers. Currently, validation is supported for addresses on crypto networks. **Documentation** * [Validate network address](/rest-apis/core-api/assets/validate-network-address) ### Support for multiple address formats in crypto deposit methods **Summary** The [Set up account deposit method](/rest-apis/core-api/accounts/set-up-account-deposit-method) endpoint now supports multiple crypto address formats for a single deposit method. Partners can now retrieve and display all supported address formats (e.g., `native-segwit`, `wrapped-segwit`, `pubkey-hash`) for a given network and asset, improving user experience and compatibility. **Documentation** * [Set up account deposit method](/rest-apis/core-api/accounts/set-up-account-deposit-method) ### New public Postman workspace **Summary** We've launched a new public Postman Workspace to reduce friction when accessing our API collections, enabling a smoother developer-experience. The previous invite-only workspace will remain accessible until 25th of January 2026, but it won't receive further updates. **Actions required** * Follow the updated [Make your first API call](/get-started/make-your-first-api-call) guide to set up the new Postman workspace. While the setup process is largely unchanged, environment variables have been renamed for clarity. **Documentation** * [Make your first API call](/get-started/make-your-first-api-call) ### Skip assets cooldowns test helper **Summary** Introduced a new [test helper](/rest-apis/test-helpers) endpoint to instantly bypass asset cooldowns in Sandbox environments. Asset cooldowns temporarily restrict features like `buy` and `deposit` for compliance reasons. This test helper allows you to skip the waiting period during development and testing, enabling immediate access to all asset features. For example, when testing with GB users, you can bypass the 24-hour cooldown and immediately test deposit or purchase flows. **Documentation** * [Skip assets cooldowns](/rest-apis/core-api/assets/test-helpers/skip-assets-cooldowns) * [Assets cooldowns](/rest-apis/core-api/assets/introduction#cooldowns) ### Business users **Summary** The API has been expanded to support business users, enabling interactions similar to those available for individual users. At this stage, business users must be created and KYB'ed outside the API. Reach out to your Account Manager if you have a use-case where business users are needed. Once verified, a business user can be managed through the API, including creating accounts, linking external accounts, and initiating transactions. **Documentation** * [Authentication - Subjects](/rest-apis/authentication#subjects) The documentation now includes badges to indicate the supported subject types for each endpoint. ### Terms of Service update for EEA users **Summary** Added `general-pt-bop` as a new Terms of Service code for EEA users, available starting **December 10, 2025** at 10:00 AM UTC. The existing `general-lt-fcs` code remains accepted through the end of 2025. **Documentation** * [Create user](/rest-apis/core-api/users/create-user) * [List Terms of Service](/rest-apis/core-api/terms-of-service/list-terms-of-service) **Actions required** * Use `general-pt-bop` for new EEA users starting December 10, 2025 at 10:00 AM UTC. * Display updated Terms of Service to existing EEA users (previous acceptances automatically migrated). ### EU and UK tax reporting regulatory requirements **Summary** Enhanced KYC processes to support EU and UK tax reporting regulatory requirements. The [`profile`](./core-api/kyc/update-profile) and [`taxDetails`](./core-api/kyc/update-tax-details) processes now dynamically adapt to the user's country, collecting additional required properties for compliance. **Enforcement dates** * **EU users:** Starting on December 10, 2025 at 10:00 AM UTC for all users. * **UK users:** Starting on December 30, 2025 at 10:00 AM UTC for newly registered users. **Details** * **Profile:** Collects place of birth and other citizenships for EU users * **Tax Details:** Supports tax address and multiple tax residence countries with flexible data collection when tax IDs cannot be provided * **Dynamic Forms:** Uses [JSON Forms](https://jsonforms.io/) with progressive disclosure based on user responses **Documentation** * [Get KYC overview](/rest-apis/core-api/kyc/get-overview) * [Update profile](/rest-apis/core-api/kyc/update-profile) * [Update tax details](/rest-apis/core-api/kyc/update-tax-details) **Actions required** * ⚠️ **Breaking Change:** `profile` object structure has changed in [Get KYC overview](/rest-apis/core-api/kyc/get-overview) and [Update profile](/rest-apis/core-api/kyc/update-profile) endpoints: * Properties moved to `input.details` (previously `input`). * The `citizenshipCountry` has been renamed to `primaryCitizenship`. * New `profile.hint` object with dynamic `schema` and `uiSchema`. * The legacy static structure is deprecated but still supported for backward compatibility. See [Update profile](/rest-apis/core-api/kyc/update-profile#body-input) for both versions. * Update response parsing and property names in your integration. * 🔍 Check `taxDetails.status` and use `taxDetails.hint.schema` to collect required properties. ### Crypto deposits and withdrawals **Summary** Expanded support for crypto funding flows, enabling seamless deposits and withdrawals in crypto. **Details** * **New Capabilities:** Introduced [`bank-deposits`](/rest-apis/core-api/capabilities/list-user-capabilities) and [`crypto-deposits`](/rest-apis/core-api/capabilities/list-user-capabilities). The existing `deposits` capability remains available for other deposit types. * **Networks:** Added `network.type` to [`List Networks`](/rest-apis/core-api/assets/list-networks) as a validation hint for frontend integrations. * **Quotes / Transactions:** The `crypto-address` node now includes an `execution` object supporting three modes — `onchain`, `offchain`, and `simulated`. Learn more about it [here](/developer-guides/crypto-transfers/withdrawal/via-rest-api#execution-modes). * **CDD for US Users:** All users (including US users) will require to [Update customer due diligence](/rest-apis/core-api/kyc/update-customer-due-diligence) before making a crypto deposit or crypto withdrawal. **Documentation** * [Crypto deposit flow](/developer-guides/crypto-transfers/deposit/via-rest-api) * [Crypto withdrawal flow](/developer-guides/crypto-transfers/withdrawal/via-rest-api) * [Create quote with node type `crypto-address` as destination](/rest-apis/core-api/transactions/create-quote#crypto-address) * [Transaction schema with node `execution` object](/rest-apis/core-api/transactions/create-transaction#response-transaction-origin-node-execution-transaction-hash) **Actions required** * ⚠️ Update your integrations to read `destination.node.execution.transactionHash` instead of `destination.node.transactionHash`. * ❌ The old property will no longer return a value. * 🔔 Ensure your app handles Customer Due Diligence for all users (including US users) before enabling crypto deposits or withdrawals. ### Assets cooldown ended webhook **Summary** Introduced a new webhook that notifies platforms when a user's asset cooldown period ends. This enables partners to update asset availability, unlock trading actions, and notify users in real-time, without relying on polling. **Documentation** * [Assets cooldown ended](/rest-apis/core-api/assets/webhooks/assets-cooldown-ended) **Actions required** * ✅ No changes needed if cooldown tracking is not required for your use case. * 🔔 Subscribe to this webhook to refresh asset states and proactively notify users when cooldowns end. ### Topper API **Summary** Introduced the Topper API to enable KYC sharing between partners and the [Topper Widget](/widgets/topper/introduction), allowing partners to complete the KYC of a Topper user directly via the Core API. **Documentation** * [Identify a user](/rest-apis/topper-api/kyc-sharing/identify-user) * [Create session](/rest-apis/topper-api/kyc-sharing/create-session) ### Portfolio endpoints **Summary** Introduced a group of endpoints to provide aggregated insights into a user's financial position across all accounts. **Documentation** * [Portfolio endpoints](/rest-apis/core-api/portfolio/introduction) ### FPS deposits and withdrawals **Summary** Introduced support for deposits and withdrawals via the Faster Payments System (FPS). **Documentation** * Added [`Bank`](/rest-apis/core-api/assets/list-networks#bank) to the [Network types](/rest-apis/core-api/assets/introduction#types-of-networks). * Added [`Bank (FPS)`](/rest-apis/core-api/accounts/set-up-account-deposit-method#bank-fps) to the [Account deposit methods](/rest-apis/core-api/accounts/set-up-account-deposit-method#bank-fps). * Added `Bank` to the [external account types](/rest-apis/core-api/external-accounts/introduction#types-of-external-accounts). * Added [`unique-account-number-viban`](/rest-apis/core-api/capabilities/list-user-capabilities) to the list of Capabilities. ### Asset ordering **Summary** Introduced support for sorting assets by various criteria, including market cap, price, and price variance. This enhancement enables partners to retrieve ordered asset lists from the API, eliminating the need for custom sorting logic. **Documentation** * [List assets sorting](/rest-apis/core-api/assets/list-assets#sorting) ### Widgets API **Summary** Introduced the Widgets API, enabling partners to create secure sessions for embedding Uphold widgets in their applications. **Documentation** * [Create session](/rest-apis/widgets-api/payment/create-session) for the [Payment Widget](/widgets/payment/introduction). ### Asset cooldowns **Summary** Enhanced the [Assets](/rest-apis/core-api/assets/introduction#asset-shape) schema by introducing a `cooldowns` property to provide details about any active cooldowns applied to the asset. This allows partners to understand which assets are temporarily restricted and when they become available again. **Documentation** * [Cooldowns](/rest-apis/core-api/assets/introduction#cooldowns) ### Core API **Summary** Introduced the Core API, providing the foundational infrastructure for building financial applications on Uphold. **Included features** This initial release introduced all foundational components of the Core API, including: * [Authentication endpoint](/rest-apis/core-api/authentication/request-oauth2-token) to request access tokens using the OAuth2 protocol. * [Countries endpoints](/rest-apis/core-api/countries/introduction) to retrieve information about the supported countries. * [Users endpoints](/rest-apis/core-api/users/create-user) to manage users, providing full CRUD capabilities and real-time webhooks to stay informed about user changes. * [KYC endpoints](/rest-apis/core-api/kyc/introduction) to manage KYC processes, ensuring that users are compliant with regulatory requirements. It also includes webhooks to notify about KYC status changes in real time. * [Capabilities endpoints](/rest-apis/core-api/capabilities/introduction) to retrieve capabilities, the actions and features that users can perform on the platform. * [Terms of Service endpoints](/rest-apis/core-api/terms-of-service/introduction) to retrieve applicable Terms of Service and record user acceptance, ensuring compliance with legal requirements. * [Files endpoints](/rest-apis/core-api/files/create-file) to generate upload and download links that support file-based KYC processes. * [Assets endpoints](/rest-apis/core-api/assets/introduction) to retrieve information about the assets available on the platform, as well as the networks and rails used to transfer them. * [Accounts endpoints](/rest-apis/core-api/accounts/introduction) to manage users' accounts, along with webhooks to asynchronously notify about account changes and balance updates. * [External accounts endpoints](/rest-apis/core-api/external-accounts/introduction) to link and manage financial accounts that users own outside the platform, such as debit or credit cards, enabling both pull deposits (moving funds into Uphold) and withdrawals (sending funds out of Uphold). * [Transactions endpoints](/rest-apis/core-api/transactions/introduction) to initiate and retrieve transactions through a unified RFQ (Request for Quote) model, supporting deposits, withdrawals, trades, and transfers across multiple asset types, along with webhooks to notify about transaction changes. **Documentation** * [Core API concepts](/rest-apis/core-api/concepts) # Archive account Source: https://developer.uphold.com/rest-apis/core-api/accounts/archive-account /_media/specs/core-openapi.mintlify.json delete /core/accounts/{accountId} Archive an existing account. # Create account Source: https://developer.uphold.com/rest-apis/core-api/accounts/create-account /_media/specs/core-openapi.mintlify.json post /core/accounts Create a new account. You can optionally include custom [entity metadata](../../entity-metadata) in the `metadata` field to store your own business data (e.g., account purpose, configuration, custom labels). If not provided during creation, you can add it later using [Set metadata](../metadata/set-metadata). # Get account Source: https://developer.uphold.com/rest-apis/core-api/accounts/get-account /_media/specs/core-openapi.mintlify.json get /core/accounts/{accountId} Retrieve an existing account by id. # Get account deposit method Source: https://developer.uphold.com/rest-apis/core-api/accounts/get-account-deposit-method /_media/specs/core-openapi.mintlify.json get /core/accounts/{accountId}/deposit-method Gets deposit method for depositing into an account externally. Use this endpoint to retrieve the deposit method for a given account, asset, and network. This is useful for determining available deposit options before initiating a deposit setup. You can check which assets support deposits by calling the [List Assets](../assets/list-assets) endpoint and filtering for assets that have `deposit` included in their features. Once you select an asset, you can determine the supported networks by calling the [List Rails](../assets/list-rails) endpoint for that asset and filtering for rails that also include `deposit` in their features. For more details, refer to the **[Assets section](../assets/introduction)**. Calling this endpoint will not trigger the [asynchronous setup process](./set-up-account-deposit-method#asynchronous-setup-process) of the deposit method which some networks require. You will need to call the [Set up Account Deposit Method](./set-up-account-deposit-method) endpoint to trigger it. # Accounts API introduction Source: https://developer.uphold.com/rest-apis/core-api/accounts/introduction Manage user accounts on the Uphold platform. Each account holds the balance of a single asset and acts as a container for the user's funds in Uphold. The accounts group of endpoints allows you to manage the user's accounts on the platform. A user may have many accounts, each one holding the balance of a specific asset. They act as a container for the user's funds stored in Uphold. ## Balance An account has two types of balances: * Available balance: The amount of funds that can be used for transactions. * Total balance: The total amount of funds in the account, including any pending transactions. ## Funding an account There are several ways to fund an account, that is, to have its balance increased. ### Pull deposits A pull deposit allows the platform to initiate the transfer of funds from the user's external account (e.g., a credit card or linked bank account), provided the user has given authorization. Pull deposits are processed through [external accounts](../external-accounts/introduction), which must be linked and authorized by the user before the platform can pull funds. To initiate a pull deposit, [create a quote](../transactions/create-quote) where the origin is the external account and the destination is the user's account. ### Push deposits A push deposit occurs when the user sends funds from an external source to their account on the platform. To facilitate a push deposit, use the [Set up account deposit method](./set-up-account-deposit-method) endpoint. This endpoint replies with the necessary deposit details for the user to complete the deposit from their side. ### From other accounts You can also fund accounts by moving funds between them, even if they hold different assets. To do this, [create a quote](../transactions/create-quote) specifying the origin and destination accounts. ## Default accounts When generating details for a push deposit using [Set up account deposit method](./set-up-account-deposit-method) endpoint, the response may include a `reference` field, which identifies the destination account to deposit into. If the user sends a push deposit without including the `reference`, the funds will be credited to the user's default account for that asset. Furthermore, there are cases in which rails do not support targeting a specific account for deposits. All deposits over those rails will be credited to the user's default account for the asset. To find out the user's default accounts for each asset, use the [List default accounts](./list-default-accounts) endpoint. ## Archiving accounts Accounts are not permanently deleted. If an external deposit is made to an archived account, the account will be automatically unarchived, and the user will successfully receive the funds. When this occurs, a [core.account.unarchived](./webhooks/account-unarchived) webhook will be triggered to notify you. # List accounts Source: https://developer.uphold.com/rest-apis/core-api/accounts/list-accounts /_media/specs/core-openapi.mintlify.json get /core/accounts List accounts owned by a user. # List default accounts Source: https://developer.uphold.com/rest-apis/core-api/accounts/list-default-accounts /_media/specs/core-openapi.mintlify.json get /core/accounts/defaults List the default account for each asset owned by a user. [Default accounts](./introduction#default-accounts) are used when receiving push deposits that have no `reference` or when a rail does not support targeting a specific account. # Set up account deposit method Source: https://developer.uphold.com/rest-apis/core-api/accounts/set-up-account-deposit-method /_media/specs/core-openapi.mintlify.json put /core/accounts/{accountId}/deposit-method Sets up a deposit method for depositing into an account externally. Use this endpoint to initiate the setup of a deposit method for an account. You can check which assets support deposits by calling the [List Assets](../assets/list-assets) endpoint and filtering for assets that have `deposit` included in their features. Once you select an asset, you can determine the supported networks by calling the [List Rails](../assets/list-rails) endpoint for that asset and filtering for rails that also include `deposit` in their features. For more details, refer to the **[Assets section](../assets/introduction)**. ## Asynchronous setup process Some deposit methods require an asynchronous setup process depending on the network. * Calling this endpoint for the first time will trigger the setup of the underlying deposit method. * If the deposit method is not immediately available, you can use the [Get Account Deposit Method](./get-account-deposit-method) endpoint with the same inputs to check the deposit method state. If the deposit method is already set up, calling this endpoint again with the same inputs will not trigger a new setup process and instead will return the existing deposit method. ## Bank deposits When setting up a bank deposit method, the bank details you receive depend on the subject requesting the setup: * **Individual users** receive virtual account details under their own name. Deposits without the provided `reference` will still be credited to the correct user, but funds will be deposited in a default account. * **Business users** may receive virtual account details under their own name or a master account in Uphold's name. In the latter case, the `reference` uniquely identifies both the business and target account and **must** be included in every transfer; otherwise, the funds won't be credited automatically and may be returned. # Fund Sandbox accounts Source: https://developer.uphold.com/rest-apis/core-api/accounts/test-helpers/fund-sandbox-accounts All the ways to fund a Sandbox account on the Uphold platform — bank deposit simulation, crypto deposit simulation, and other test helpers for testing flows. This page lists every way to fund a Sandbox account so you can perform a deposit and use the resulting balance for further testing. ## Simulated bank deposit Use the [Simulate bank deposit](./simulate-bank-deposit) test helper to credit a Sandbox account as if funds had arrived via bank transfer. Set up the target account first with [Setup account deposit method](../set-up-account-deposit-method) and use the returned details when calling the simulator. ## Simulated crypto deposit Use the [Simulate crypto deposit](./simulate-crypto-deposit) test helper to credit a Sandbox account as if a crypto deposit had been received. Set up the target account first with [Setup account deposit method](../set-up-account-deposit-method) and use the returned details when calling the simulator. Because the deposit is simulated, the transaction hash is not available on any blockchain. If you need an actual on-chain transaction, use the [testnet flow below](#crypto-deposit-via-testnets) instead. ## Crypto deposit via testnets When testing crypto deposit and withdrawal flows in Sandbox, Uphold integrates testnet blockchains so you can simulate real on-chain activity without using real funds. Use the details returned by the [Setup account deposit method](../set-up-account-deposit-method) endpoint to deposit testnet tokens into your Sandbox account. **Uphold does not supply test tokens.** You must source them yourself using the faucets listed below, or any other preferred alternative. ### Recommended token faucets | Asset | Testnet | Faucet | | ---------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | BTC | Testnet | [Bitcoin Testnet Faucet](https://bitcoinfaucet.uo1.net/), [Coinfaucet](https://coinfaucet.eu/en/btc-testnet/), [testnet-faucet.com](https://testnet-faucet.com/btc-testnet/) | | ETH | Sepolia | [Infura Sepolia Faucet](https://www.infura.io/faucet/sepolia), [GetBlock Faucet](https://getblock.io/faucet/eth-sepolia/) | | XRP | Ripple Testnet | [XRP Faucets](https://xrpl.org/resources/dev-tools/xrp-faucets) | | SOL | Devnet | [Solana Faucet](https://faucet.solana.com/), [SolFaucet](https://solfaucet.com/), [SolFaucet (Toga)](https://solfaucet.togatech.org/) | | USDC, EURC | Multiple | [Circle Faucet](https://faucet.circle.com/) | | Multiple | Multiple | [Chainlink Faucet](https://faucets.chain.link/) | | Multiple | Multiple | [Chainstack Faucet](https://faucet.chainstack.com/) | Please note that faucet availability and token limits are managed by third parties and may vary over time. Crypto withdrawals in Sandbox are simulated by default and do not reach the blockchain. Refer to your Account Manager if you require on-chain withdrawal testing. ## Card deposit using test cards Use the test cards below to simulate card deposit flows in the Sandbox environment without processing real transactions. These are cards you can use to create an [external account](../../external-accounts/create-external-account) and use it as the `origin` when [creating a quote](../../transactions/create-quote). ### Test cards The following cards are available for testing across different networks, countries, and card types. | Card Number | Type | Country | Features | Method | | ------------------ | ------ | ------- | ------------------- | ------- | | `5355223761921186` | Debit | GB | Deposit | Instant | | `5573606426146833` | Debit | GB | Deposit, Withdrawal | Instant | | `5518832400606463` | Debit | US | Deposit | N/A | | `5318773012490080` | Debit | US | Deposit, Withdrawal | Instant | | `5385308360135181` | Credit | US | Deposit | N/A | | `5502514549870410` | Debit | FR | Deposit | Instant | | `5436031030606378` | Credit | MU | Deposit | N/A | | Card Number | Type | Country | Features | Method | | ------------------ | ------ | ------- | ------------------- | ------- | | `4921817844445119` | Debit | GB | Deposit | Instant | | `4659105569051157` | Debit | GB | Deposit, Withdrawal | Instant | | `4242424242424242` | Credit | GB | Deposit | N/A | | `4024764449971519` | Debit | US | Deposit | Instant | | `4485040371536584` | Credit | US | Deposit | N/A | All test cards accept any three-digit CVV as valid, and any future expiry date in the MM/YY format. ### Simulating errors To trigger a specific error response, use the corresponding reserved amount when creating a transaction with any of the test cards above. All other amounts will result in a successful transaction. | Amount | Error Code | Description | | ------- | ----------------------- | ----------------------------------------------------- | | `12.12` | `card_unauthorized` | The card is not authorized for this transaction. | | `15.15` | `card_declined_by_bank` | The issuing bank declined the transaction. | | `20.20` | `card_expired` | The card has expired. | | `26.26` | `insufficient_funds` | The card has insufficient funds to cover the amount. | | `34.34` | `velocity` | The card has exceeded its transaction velocity limit. | | `60.60` | `card_unauthorized` | The card is not authorized for this transaction. | # Simulate bank deposit Source: https://developer.uphold.com/rest-apis/core-api/accounts/test-helpers/simulate-bank-deposit /_media/specs/core-openapi.mintlify.json post /core/accounts/test-helpers/bank-deposits Simulate an incoming bank deposit for testing purposes. This endpoint allows you to simulate a bank deposit based on the details returned by the [Setup Account Deposit Method](../set-up-account-deposit-method) endpoint. It is useful for testing how your application handles incoming deposits without needing to perform an actual bank transfer. # Simulate crypto deposit Source: https://developer.uphold.com/rest-apis/core-api/accounts/test-helpers/simulate-crypto-deposit /_media/specs/core-openapi.mintlify.json post /core/accounts/test-helpers/crypto-deposits Simulate an incoming crypto deposit for testing purposes. This endpoint allows you to simulate a crypto deposit based on the details returned by the [Setup account deposit method](../set-up-account-deposit-method) endpoint. It is useful for testing how your application handles incoming crypto deposits without needing to perform an actual testnet transfer. **Since this is a simulated deposit and does not use a real testnet, the transaction hash will not be available on the blockchain.** Check out [Testnets and token faucets](./fund-sandbox-accounts) for instructions on how to fund your Sandbox account using an actual testnet transaction. Depending on the country and/or the amount, the simulated crypto deposit may be placed `on-hold` pending Requests for Information (RFIs). See [Handle on-hold transactions](/developer-guides/money-movement/crypto-transfers/crypto-deposit#handle-on-hold-transactions) for more details. # Update account Source: https://developer.uphold.com/rest-apis/core-api/accounts/update-account /_media/specs/core-openapi.mintlify.json patch /core/accounts/{accountId} Update an existing account. # Account archived Source: https://developer.uphold.com/rest-apis/core-api/accounts/webhooks/account-archived /_media/specs/core-openapi.mintlify.json webhook core.account.archived An account has been archived. # Account Balance Changed Source: https://developer.uphold.com/rest-apis/core-api/accounts/webhooks/account-balance-changed /_media/specs/core-openapi.mintlify.json webhook core.account.balance-changed The balance of an account has changed. # Account Created Source: https://developer.uphold.com/rest-apis/core-api/accounts/webhooks/account-created /_media/specs/core-openapi.mintlify.json webhook core.account.created An account has been created. # Account unarchived Source: https://developer.uphold.com/rest-apis/core-api/accounts/webhooks/account-unarchived /_media/specs/core-openapi.mintlify.json webhook core.account.unarchived An account has been unarchived. # Get asset Source: https://developer.uphold.com/rest-apis/core-api/assets/get-asset /_media/specs/core-openapi.mintlify.json get /core/assets/{asset} Retrieve an asset by code. If the [subject](../../authentication#subjects) calling the endpoint is a user, the response will be contextualized accordingly. As an example, the `features` field will indicate the available features for the user. You can retrieve several assets in a single request by using the [Get Many Assets](./get-many-assets) endpoint. # Get asset rates Source: https://developer.uphold.com/rest-apis/core-api/assets/get-asset-rates /_media/specs/core-openapi.mintlify.json get /core/assets/{asset}/rates Retrieve asset rates against other assets. ## Denomination The `denomination` query parameter allows you to denominate rates against one or multiple assets. If no value is set for this parameter, the rates for all available assets will be returned. There is a specific set of assets allowed, including but not limited to: `USD`, `EUR`, `GBP`, `AUD`, `CAD`, `NZD`, `MXN`, and `BTC`. If you need a particular asset added to this list, please reach out to your Account Manager. For a more detailed explanation of the denomination concept, check the [Core Concepts](../concepts#denomination) page. #### Retrieving multiple asset rates at once You can use the denomination currency as a proxy for retrieving the specific asset rates you're interested in. This way, you won't have to: * Make one Get Asset Rates request per each asset rate desired * Discard all the undesired assets rates retrieved by default For example, if you want to get the rates for `BTC` and `ETH` in `USD`: 1. Define `asset` as `USD` 2. Set `denomination` as the list of desired assets: `BTC,ETH` 3. Calculate the inverse for each returned asset rate: $\frac{1}{\text{rate}}$ You can specify up to 100 asset codes in the `denomination` parameter per request. # Get asset historical rates Source: https://developer.uphold.com/rest-apis/core-api/assets/get-historical-rates /_media/specs/core-openapi.mintlify.json get /core/assets/{asset}/historical-rates Retrieve historical rates for a specific asset. ## Denomination The `denomination` query parameter allows you to denominate rates against another asset. If no value is set for this parameter, it defaults to `USD`. There is a specific set of assets allowed, including but not limited to: `USD`, `EUR`, `GBP`, `AUD`, `CAD`, `NZD`, `MXN`, and `BTC`. If you need a particular asset added to this list, please reach out to your Account Manager. For a more detailed explanation of the denomination concept, check the [Core Concepts](../concepts#denomination) page. ## Interval The `interval` query parameter determines the overall timespan of historical data and the frequency at which data points are added (rolled up). Each interval value defines how far back in time the data spans, and how granular each returned data point is within that range: | Interval | Description | Rollup Frequency | | :----------- | :--------------------------------- | :--------------- | | `one-hour` | Past hour of historical data | 30 seconds | | `one-day` | Past day of historical data | 10 minutes | | `one-week` | Past week of historical data | 1 hour | | `one-month` | Past month of historical data | 6 hours | | `one-year` | Past year of historical data | 2 days | | `five-years` | Past five years of historical data | 1 week | # Get network Source: https://developer.uphold.com/rest-apis/core-api/assets/get-network /_media/specs/core-openapi.mintlify.json get /core/networks/{network} Retrieve a network by code. # Assets API introduction Source: https://developer.uphold.com/rest-apis/core-api/assets/introduction Retrieve information about assets supported on Uphold, plus the networks and rails used to transfer them. Includes asset shape, types, and rate endpoints. The assets group of endpoints provides information about the assets available on the platform, as well as the networks and rails used to transfer them. ## Assets Assets represent the various financial instruments available on the platform, categorized by their type and features. ### Asset shape An asset has the following properties: ```json [expandable] theme={null} { "code": "BTC", "name": "Bitcoin", "type": "crypto", "symbol": "₿", "decimals": 8, "logo": "https://cdn.uphold.com/assets/BTC.svg", "features": [ "buy", "deposit", "sell", "transfer", "withdraw" ], "cooldowns": [] } ``` ### Types of assets There are different types of assets available on the platform: National currencies issued by governments and regulated by central banks. Some examples of fiat assets are `USD`, `EUR`, and `GBP`. Digital currencies that use cryptography for security and operate on decentralized networks. Some examples of crypto assets are `BTC`, `ETH`, and `USDT`. ### Features Each asset has a unique `code` and a set of features. These features determine the type of transactions that can be performed with the asset: * `buy`: The ability to purchase the asset, that is, when it's specified as the `destination` in a transaction (e.g., asset associated with the destination node) and the `origin` node has a different asset. * `transfer`: The ability to transfer the asset, that is, when the `origin` and `destination` node assets are the same. * `sell`: The ability to sell the asset, that is, when it's specified as the `origin` node in a transaction (e.g., asset associated with the account) and the destination node asset is different. * `deposit`: The ability to perform a deposit of the asset when specified as origin node of a transaction (e.g., through an [external account](../external-accounts/introduction)). * `withdraw`: The ability to perform a withdrawal of the asset when specified as origin node of a transaction (e.g., through an [external account](../external-accounts/introduction)). Please note that these features can be contextualized with a user, meaning that a user may have access to some features of an asset and not others. ### Cooldowns Cooldowns indicate temporary restrictions on specific features such as buy, deposit, or withdraw. When an asset is under a cooldown, the corresponding features will be temporarily unavailable until the cooldown period expires. ```json [expandable] theme={null} { "cooldowns": [ { "rule": "financial-promotions", "features": [ "buy", "deposit" ], "endsAt": "2025-04-02T09:07:41.952Z" } ] } ``` Below you can find the list of possible scenarios: * `financial-promotions`: The rule applies during the first 24 hours after a user's account is created, and affects the `buy` and `deposit` features. ## Networks Networks are the underlying protocols through which the transfer of certain assets is made. ### Network shape A network has the following base properties: ```json theme={null} { "code": "bitcoin", "type": "crypto", "name": "Bitcoin", "logo": "https://cdn.uphold.com/assets/BTC.svg" } ``` The network may have additional properties depending on the `type`. For example, the network above has two more properties named `exampleAddress` and `explorer`, which are available for networks of type `crypto`. ### Types of networks There are different types of networks available on the platform: Networks of type `crypto` are blockchains that use cryptography to secure transactions. Examples of such networks are `bitcoin`, `ethereum`, and `xrp-ledger`. Networks of type `card` are used for credit and debit card transactions. There is a single network of this type, which is `card`. Networks of type `bank` refer to networks that facilitate direct bank transactions. Examples of such networks are `sepa`, `fps`, and `ach`. ## Rails A rail is a combination of an asset and a network, which together determine whether a deposit or a withdrawal of a given asset under that network is possible. ### Rail shape A rail has the following base properties: ```json [expandable] theme={null} { "type": "crypto", "network": "ethereum", "method": "crypto-transaction", "asset": "USDC", "features": [ "deposit", "withdraw" ], "decimals": 6 } ``` The rail may have additional properties depending on the `type`. For example, the rail above has one more property named `contractAddress`, which is available for rails of type `crypto`. In some rare cases, the `decimals` property on the rail is different than the `decimals` defined in the asset. If you are initiating a deposit or a withdrawal transaction, use the `decimals` of the corresponding rail to truncate or round the decimal places in the user interface. ### Types of rails There are different types of rails available on the platform, which are analogous to the types of networks: Rails of type `crypto` are used for transferring crypto assets through blockchains. Rails of type `card` are used for credit and debit card transactions. Rails of type `bank` are used for direct bank transactions. ### Features and deposits / withdrawals A deposit is possible if: * The rail has the `deposit` feature. * The origin asset has the `deposit` feature. * The destination asset has: * The `transfer` feature if the origin and destination assets are the same. * The `buy` feature if converting between assets. A withdrawal is possible if: * The rail has the `withdraw` feature. * The origin asset has: * The `transfer` feature if the origin and destination assets are the same. * The `sell` feature if converting between assets. * The destination asset has the `withdraw` feature. # List assets Source: https://developer.uphold.com/rest-apis/core-api/assets/list-assets /_media/specs/core-openapi.mintlify.json get /core/assets List assets supported by the platform. If the [subject](../../authentication#subjects) calling the endpoint is a user, the response will be contextualized accordingly. As an example, the `features` field will indicate the available features for the user. You can retrieve several assets in a single request by using the [Get Many Assets](./get-many-assets) endpoint. ## Sorting You can sort the assets by using the `sort` query parameter. Below are the available sorting options: To sort by name, use the `sort=name:asc` or `sort=name:desc` to sort in ascending or descending order, respectively. To sort by code, use the `sort=code:asc` or `sort=code:desc` to sort in ascending or descending order, respectively. To sort by current price, use the `sort=price:asc` or `sort=price:desc` to sort in ascending or descending order, respectively. The `price-delta` allows you to sort assets by gainers and losers, showing assets with the highest and lowest price changes in a given interval. To sort by price delta, use the `sort=price-delta:asc:` or `sort=price-delta:desc:` to sort in ascending or descending order, respectively, where `interval` can be `one-hour`, `one-day`, `one-week`, `one-month`, `one-year`, or `five-years`. To sort by market cap, use the `sort=market-cap:asc` or `sort=market-cap:desc` to sort in ascending or descending order, respectively. # List networks Source: https://developer.uphold.com/rest-apis/core-api/assets/list-networks /_media/specs/core-openapi.mintlify.json get /core/networks List networks supported by the platform. # List rails Source: https://developer.uphold.com/rest-apis/core-api/assets/list-rails /_media/specs/core-openapi.mintlify.json get /core/rails List rails supported by the platform. If the [subject](../../authentication#subjects) calling the endpoint is a user, the response will be contextualized accordingly. As an example, the `features` field will indicate the available features for the user. # Skip assets cooldowns Source: https://developer.uphold.com/rest-apis/core-api/assets/test-helpers/skip-assets-cooldowns /_media/specs/core-openapi.mintlify.json put /core/assets/test-helpers/skip-assets-cooldowns Skips assets cooldowns for testing purposes. There are [cooldowns](../introduction#cooldowns) that temporarily restrict certain asset features. By calling this endpoint, you can immediately bypass those cooldowns, rather than waiting for them to expire naturally. For example, when a user residing in GB is created, a 24-hour cooldown is applied before they can access certain asset functionalities. Using this endpoint clears that cooldown instantly, allowing the user to proceed without delay. This is a [**Test Helper**](../../../test-helpers) endpoint and can only be used in Sandbox. # Validate network address Source: https://developer.uphold.com/rest-apis/core-api/assets/validate-network-address /_media/specs/core-openapi.mintlify.json post /core/networks/{network}/validate-address Validate an address for a specific network. # Assets cooldown ended Source: https://developer.uphold.com/rest-apis/core-api/assets/webhooks/assets-cooldown-ended /_media/specs/core-openapi.mintlify.json webhook core.assets.cooldown-ended The cooldown period for a set of assets has ended. # Request OAuth2 token Source: https://developer.uphold.com/rest-apis/core-api/authentication/request-oauth2-token /_media/specs/core-openapi.mintlify.json post /core/oauth2/token Request an access token using OAuth2 protocol. # Get capability Source: https://developer.uphold.com/rest-apis/core-api/capabilities/get-user-capability /_media/specs/core-openapi.mintlify.json get /core/capabilities/{capability} Retrieve a user capability by code. # Capabilities API introduction Source: https://developer.uphold.com/rest-apis/core-api/capabilities/introduction Capabilities define the actions and features users can perform on the Uphold platform. Learn the capability shape, statuses, and how to query user permissions. Capabilities define the important actions and features that a user can perform on the platform. ## Capability shape A capability has the following shape: ```json [expandable] theme={null} { "code": "trades", "name": "Trades", "enabled": true, "requirements": [ "user-must-submit-identity", "user-must-submit-customer-due-diligence", "user-must-submit-proof-of-address" ], "restrictions": [] } ``` ## Enabled / disabled When the capability has `enabled` set to false, then it has at least one restriction that prevents the user from performing the action. In this situation, there is nothing the user can do to enable the capability. When the capability is `enabled`, the user is able to perform the action but may be subject to requirements, if any. The requirements are usually tied to KYC processes or Terms of Service that were not accepted. # List capabilities Source: https://developer.uphold.com/rest-apis/core-api/capabilities/list-user-capabilities /_media/specs/core-openapi.mintlify.json get /core/capabilities List user capabilities. # Core API concepts and data model Source: https://developer.uphold.com/rest-apis/core-api/concepts Learn the core concepts and data model behind the Uphold Enterprise API Suite — users, accounts, transactions, and assets — to design your integration. This page introduces the core concepts and data model that underpin the API suite, providing a foundational understanding of how the financial infrastructure operates. Familiarity with these concepts will help you navigate the API documentation and design your integration effectively. ## Data model Core Data Model The diagram above outlines the key concepts and their relationships, providing a clear view of how the financial infrastructure operates. When a **User** registers, they must specify their **Country of residence** and accept the applicable **Terms of Service**, which establish the legal framework for using the platform. To ensure compliance, every **User** must complete **Know Your Customer (KYC) processes**, a set of verification steps required before accessing financial services. The platform supports three types of KYC processes: **basic, form-based, and file-based**. For the file-based verification steps, the **User** must upload the required **Files** (e.g., government ID, proof-of-address) to provide the necessary documentation for verification. A **User's KYC status** and the **Terms of Service** they accept directly influence their **Capabilities** on the platform. **Capabilities** define what important actions a **User** is allowed to perform. Depending on the **Capabilities** granted, the **User** may be restricted from performing certain operations, such as transacting or linking an **External Account**, e.g., a bank or a credit card. Each **User** owns one or more **Accounts**, each storing balance of **Asset**. **Users** interact with their accounts primarily through **Transactions**, which modify **Account balances** by **trading, depositing, or withdrawing assets**. Deposits and withdrawals are powered by **Rails**, which determine how assets are transferred within a **Network**. You can also create **External Accounts**, which enable users to link bank accounts or credit/debit cards. Unlike push deposits, **External Accounts** allow the platform to **pull funds** to an Account, with the **User**'s authorization. **External Accounts** can also be used as a withdrawal destination, enabling two-way transactions. ## Key concepts ### Denomination Denomination currency refers to the currency unit in which the value of a transaction, trade, or financial asset is expressed. It provides a consistent way to measure value across different assets or currencies. Additionally, knowing the denomination helps in assessing exposure to foreign exchange fluctuations. The denomination currency is not necessarily the same as the currency used to pay. A user can buy an asset priced in EUR (denomination currency), but pay in USD. As another example, users might: * Hold balances in one currency (e.g., GBP) * View prices in another (e.g., USD) * Transact in yet another (e.g., BTC) Specific examples: 1. **Buying Crypto** * You buy 1 BTC priced at \$60,000 USD. * USD is the denomination currency (price reference). * You pay in EUR → conversion happens at settlement. 2. **Cross-Border Payment** * A vendor invoice is denominated in GBP. * You pay using USD. * GBP remains the denomination currency (the expected value); USD is converted to match that amount. 3. **Portfolio Holdings** * A portfolio lists all assets valued in a single denomination currency, like USD, giving a unified view even if balances are held in multiple currencies. 4. **Statements** * A portfolio statement expresses all holdings in the requested denomination asset using period-end exchange rates. * A transactions statement records the denomination asset and exchange rates at the time each transaction completed. # Get country Source: https://developer.uphold.com/rest-apis/core-api/countries/get-country /_media/specs/core-openapi.mintlify.json get /core/countries/{country} Retrieve a country by code. # Countries API introduction Source: https://developer.uphold.com/rest-apis/core-api/countries/introduction Retrieve information about the countries supported by the Uphold platform, including the country shape, supported assets, and KYC requirements per country. The countries group of endpoints allows you to retrieve information about the countries supported by the platform. ## Country shape A country has the following shape: ```json [expandable] theme={null} { "country": { "code": "GB", "name": "United Kingdom", "asset": "GBP", "restrictions": [], "subdivisions": [ { "code": "GB-ABC", "name": "Armagh, Banbridge and Craigavon", "restrictions": [] }, { "code": "GB-ABD", "name": "Aberdeenshire", "restrictions": [] }, { "code": "GB-ABE", "name": "Aberdeen City", "restrictions": [] }, { "code": "GB-AGB", "name": "Argyll and Bute", "restrictions": [] }, { "code": "GB-AGY", "name": "Isle of Anglesey", "restrictions": [] }, // ... ] } } ``` Country and subdivision codes follow the [ISO 3166](https://en.wikipedia.org/wiki/ISO_3166-2) standard, more specifically, the two letter codes for country codes. ## Restrictions Countries and their subdivisions are subject to restrictions that may affect the availability of services, which are exposed through the API. Here's a list of different restrictions that may apply: * `residence` - Users whose official residence belongs to a country or subdivision with this restriction are not allowed. * `citizenship` - Users whose citizenship belongs to a country with this restriction are not allowed. * `geolocation` - Users accessing the platform from a country with this restriction are not allowed (inferred from `X-Uphold-User-Ip` and `X-Uphold-User-Country` headers). * `phone` - Phone numbers belonging to a country with this restriction are not allowed. # List countries Source: https://developer.uphold.com/rest-apis/core-api/countries/list-countries /_media/specs/core-openapi.mintlify.json get /core/countries List countries supported by the platform. # Create external account Source: https://developer.uphold.com/rest-apis/core-api/external-accounts/create-external-account /_media/specs/core-openapi.mintlify.json post /core/external-accounts Create an external account for the user. You can optionally include custom [entity metadata](../../entity-metadata) in the `metadata` field to store your own business data (e.g., cardholder name, tracking information, custom labels). If not provided during creation, you can add it later using [Set metadata](../metadata/set-metadata). # Delete external account Source: https://developer.uphold.com/rest-apis/core-api/external-accounts/delete-external-account /_media/specs/core-openapi.mintlify.json delete /core/external-accounts/{externalAccountId} Delete an existing external account. # Get external account Source: https://developer.uphold.com/rest-apis/core-api/external-accounts/get-external-account /_media/specs/core-openapi.mintlify.json get /core/external-accounts/{externalAccountId} Retrieve an existing external account by id. # External accounts API introduction Source: https://developer.uphold.com/rest-apis/core-api/external-accounts/introduction Manage external financial accounts, such as user-owned debit and credit cards or bank accounts, used for pull deposits and withdrawals out of Uphold. The external accounts group of endpoints allows you to manage financial accounts that users own outside the platform, such as debit or credit cards. External accounts can be used for both **pull deposits** (moving funds into Uphold) and **withdrawals** (sending funds out of Uphold). ## External account shape An external account includes the following properties: ```json [expandable] theme={null} { "id": "aa6e6efa-8d73-497c-8278-0347f459bd68", "ownerId": "1e32fca3-23f7-40ed-bc1b-de10c790182d", "type": "card", "status": "ok", "label": "My Visa Card", "asset": "GBP", "network": "visa", "features": ["deposit", "withdraw"], "createdAt": "2024-06-01T00:00:00Z", "updatedAt": "2024-07-15T00:00:00Z" } ``` Each external account has a `status` that reflects its availability: * `processing`: The account is being verified. * `ok`: The account is valid and ready to use. * `failed`: The verification failed — check `statusDetails.reason` for the failure reason. * `restricted` / `blocked`: The account is temporarily or permanently unusable. ## Types of external accounts There are different types of external accounts available on the platform: External accounts of type `card` represent **credit or debit cards** manually linked by the user for **fiat deposits and withdrawals**. External accounts of type `bank` represent **bank accounts** used for **fiat deposits and withdrawals** via networks like **FPS**, **SEPA** or **ACH**. These accounts are **automatically created** after the user's first successful deposit, using the bank details returned by [Set Up Account Deposit Method](../accounts/set-up-account-deposit-method) endpoint. ## When to use external accounts * **Bank push deposits**: A `bank` external account represents the user's originating bank account and is used to identify them in recurring transactions — [via API](/developer-guides/bank-transfers/deposit/via-rest-api) or [via Payment Widget](/developer-guides/bank-transfers/deposit/via-payment-widget). * **Bank withdrawals**: A `bank` external account is used as the destination of a quote to pay out to the user's bank account — [via API](/developer-guides/bank-transfers/withdrawal/via-rest-api) or [via Payment Widget](/developer-guides/bank-transfers/withdrawal/via-payment-widget). * **Card deposits**: A `card` external account is linked by the user and used as the origin of a quote. Card authorization may be required — [via API](/developer-guides/card-transfers/deposit/via-rest-api) or [via Payment Widget](/developer-guides/card-transfers/deposit/via-payment-widget). * **Card withdrawals**: A `card` external account is linked by the user and used as the destination of a quote — [via API](/developer-guides/card-transfers/withdrawal/via-rest-api) or [via Payment Widget](/developer-guides/card-transfers/withdrawal/via-payment-widget). ## Related guides Fund an account via bank transfer. Payout to a bank via quote-based transfers. Fund an account from a credit or debit card. Cash out to a credit or debit card. ## Managed UI option If you need a managed UI to collect payment or bank details, consider the Payment Widget (`select-for-deposit` / `select-for-withdrawal`) and then consume the returned `depositMethod` or `external-account` with the Core API. ## Testing To test card external accounts in the Sandbox environment, use the [Test Cards](../accounts/test-helpers/fund-sandbox-accounts#card-deposit-using-test-cards) reference, which lists available test card numbers and reserved amounts to simulate specific error scenarios. # List external accounts Source: https://developer.uphold.com/rest-apis/core-api/external-accounts/list-external-accounts /_media/specs/core-openapi.mintlify.json get /core/external-accounts List external accounts for the user. # Update external account Source: https://developer.uphold.com/rest-apis/core-api/external-accounts/update-external-account /_media/specs/core-openapi.mintlify.json patch /core/external-accounts/{externalAccountId} Update an existing external account. # External account created Source: https://developer.uphold.com/rest-apis/core-api/external-accounts/webhooks/external-account-created /_media/specs/core-openapi.mintlify.json webhook core.external-account.created An external account has been created. # External account deleted Source: https://developer.uphold.com/rest-apis/core-api/external-accounts/webhooks/external-account-deleted /_media/specs/core-openapi.mintlify.json webhook core.external-account.deleted An external account has been deleted. # External account status changed Source: https://developer.uphold.com/rest-apis/core-api/external-accounts/webhooks/external-account-status-changed /_media/specs/core-openapi.mintlify.json webhook core.external-account.status-changed The status of an external account has been changed. # Create file Source: https://developer.uphold.com/rest-apis/core-api/files/create-file /_media/specs/core-openapi.mintlify.json post /core/files Create a new file. Uploading a file is a two step process. You start by creating a placeholder for the file and then using the `upload` details of the response to actually upload the file. Each file has a unique `id` which is used across the API when you need to reference it. You can optionally include custom [entity metadata](../../entity-metadata) in the `metadata` field to store your own business data (e.g., descriptions, related entity IDs, processing metadata). If not provided during creation, you can add it later using [Set metadata](../metadata/set-metadata). Below you will find examples in several programming languages to do the upload: The following example uses [`file-type`](https://www.npmjs.com/package/file-type) package to determine the content-type of the file. ```js theme={null} import { fileTypeFromFile } from 'file-type'; import fs from 'node:fs'; // Function to upload file. // Takes `file` record from the response and `filePath` which points to the file on disk that needs to be uploaded. const uploadFile = async (file, filePath) => { const { url, formData } = file.upload; // Add form data. const form = new FormData(); for (const [key, value] of Object.entries(formData)) { form.append(key, value); } // Add content type, determined by file-type package. // This must match the `contentType` of the file record. const { mime } = await fileTypeFromFile(filePath); form.append('Content-Type', mime); // Add file blob. const blob = await fs.openAsBlob(filePath) form.append('File', blob); const response = await fetch(url, { method: 'POST', body: form, }); if (!response.ok) { throw new Error(`Failed to upload file: ${response.statusText}`); } console.log('File uploaded!') }; ``` # Get file Source: https://developer.uphold.com/rest-apis/core-api/files/get-file /_media/specs/core-openapi.mintlify.json get /core/files/{fileId} Retrieve an existing file by id. The returned download URL is a signed URL that is only valid for a limited time, noted by the `download.expiresAt` field. # List files settings Source: https://developer.uphold.com/rest-apis/core-api/files/list-files-settings /_media/specs/core-openapi.mintlify.json get /core/files/settings List files upload settings. # Get overview Source: https://developer.uphold.com/rest-apis/core-api/kyc/get-overview /_media/specs/core-openapi.mintlify.json get /core/kyc Get the KYC overview of a user. **Get overview** is the endpoint that provides a summary of the user's KYC process. By default, only the status of each KYC process is included in the response. If you need detailed information, you can use the `detailed` query parameter to specify the KYC processes you want more detail, which will bring their `input`, `output` fields as well as `hint` if applicable. Getting the detailed KYC information is more expensive in terms of performance and should be used only when necessary. # KYC API introduction Source: https://developer.uphold.com/rest-apis/core-api/kyc/introduction Manage KYC processes on the Uphold platform to keep users compliant with regulatory requirements. Learn the process model, statuses, and verification flow. The KYC group of endpoints allows you to manage KYC processes available on the platform, ensuring that your users are compliant with regulatory requirements. ## How it's designed Every KYC is designed as a process that has five fundamental properties: * `code`: A unique identifier for the process. * `status`: As the name implies, this is the status of the process. * `verification`: An object describing how the process should be completed, including who is responsible for verification, how the process is driven, and any dependencies on other processes. * `input`: The input is data provided by the user when prompted to complete the process. * `output`: The output is data that came out from verifying the input data provided by the user. ### Statuses The `status` field can have the following values: * `exempt`: The process is exempt from verification. * `pending`: The process is pending information from the user. * `processing`: The process is currently being verified. * `ok`: The process has been successfully verified. * `failed`: The process has failed verification. You may subscribe to [webhooks](./webhooks) for when the status of a KYC process changes. ### Verification Every KYC process exposes a `verification` object that describes how it must be completed for the current user and organization: ```json theme={null} { "verification": { "model": "partner-verified", "method": "manual", "dependencies": ["phone", "profile", "address"] } } ``` * `model`: who is responsible for verifying the data. * `uphold-verified`: Uphold always produces the `output`. Depending on the process, verification is either driven by a third-party provider session (no explicit update possible) or by `input` submitted by your organization. * `partner-verified`: Your organization always produces both `input` and `output`, performing the verification directly and typically resulting in immediate `ok` status. * `method`: whether the process requires explicit input or advances automatically. * `manual`: requires explicit input from the user or your organization (e.g., form answers, documents, or direct data submission). * `automatic`: Uphold runs this without user input, triggered automatically when the processes listed in `triggers` complete. * `triggers`: the list of processes whose completion causes this process to run automatically. Only present when `method` is `automatic` and at least one trigger is defined. * `dependencies`: the list of processes that must be completed before this process can be submitted. Only present when the process has at least one prerequisite. ## KYC processes ### List of processes * [`email`](./update-email): Process associated with updating the user's email. * [`phone`](./update-phone): Process associated with updating the user's phone number. * [`profile`](./update-profile): Process associated with updating the user's basic information, such as name, date of birth and citizenship. * [`address`](./update-address): Process associated with updating the user's address of residence. * [`identity`](./update-identity): Process associated with updating the user's identity, usually through a government-issued ID. * [`proofOfAddress`](./update-proof-of-address): Process associated with updating the user's proof-of-address, usually through a utility bill or bank statement. * [`customerDueDiligence`](./update-customer-due-diligence): Process associated with performing due diligence on the customer, ensuring compliance with regulatory requirements. * [`enhancedDueDiligence`](./update-enhanced-due-diligence): Process associated with providing proof after completing the `customerDueDiligence` process that is needed in certain high-risk scenarios. * [`cryptoRiskAssessment`](./update-crypto-risk-assessment): Process associated with performing analysis on the user's knowledge about crypto and associated risks (`GB` residents only). * [`selfCategorizationStatement`](./update-self-categorization-statement): Process associated with identifying the user's investor profile, including risk level and investment preferences (`GB` residents only). * [`taxDetails`](./update-tax-details): Process associated with collecting and verifying the user's tax-related information. * `screening`: Background process associated with checking user provided data against official lists of sanctioned parties. * `risk`: Background process associated with checking user provided data and activity patterns. ### Form-based processes Some processes, such as `profile`, `customerDueDiligence` and `taxDetails`, are composed of a form with questions the user must answer. For these types of processes, you get a `hint` property which includes a JSON Schema and UI Schema that define the form structure. To complete a form-based process: 1. **Fetch the form schema** — call [Get KYC Overview](./get-overview) with `?detailed={process}` to retrieve the `hint`. 2. **Render the form** — use the hint to display the current questions. See [Dynamic Forms](/developer-guides/resources/dynamic-forms/introduction) for rendering guidance. 3. **Submit answers** — call `PATCH /core/kyc/processes/{process}` with `input.formId` and `input.answers`. 4. **Repeat** — continue until `status` leaves `pending` and `output` is correctly populated. Forms are progressive — answers to one question may change which questions follow. Never hardcode the question set; always re-fetch the hint after each submission. When a form-based process is `partner-verified` for your organization, skip the form cycle — collect the data through your own means, then submit both `input` and `output` in a single PATCH. The process transitions to `ok` immediately. ### File-based processes Some processes, such as `identity`, `proofOfAddress` and `enhancedDueDiligence`, require users to upload necessary documents to complete the verification. To complete a file-based process: 1. **Create the file** — call [Create File](../files/create-file) for each document. Use the returned `upload` object to upload the file directly to the storage provider. 2. **Submit the process** — call `PATCH /core/kyc/processes/{process}` referencing the file `id` in `input.media`. You can look up the exact file categories each KYC process requires by checking the documentation for each endpoint. ### Background processes There are background processes, such as `screening` and `risk`, that happen behind the scenes as the user provides data in other KYC processes and engages in activities on the platform, such as transactions. There are no associated endpoints to update these processes, since their underlying verification is based on internal monitoring done by Uphold. ## Periodic review Some processes require periodic review, in which their status may change to `pending`, signaling that the user must provide updated information. * `profile`: The user must confirm their profile information is still accurate. * `address`: The user must confirm their address is still accurate. * `identity`: The user must provide up-to-date identity when their underlying document is about to expire. * `customerDueDiligence`: The user must redo the form after a certain period of time. * `selfCategorizationStatement`: The user must redo the form after a certain period of time. # Update address Source: https://developer.uphold.com/rest-apis/core-api/kyc/update-address /_media/specs/core-openapi.mintlify.json patch /core/kyc/processes/address Update the address process for a user. **Update address** is the endpoint through which the user can update their address of residence. This process solely updates the user's address of residence. It does **not** perform any verification. For address verification, refer to the [proof-of-address](./update-proof-of-address) process. ## Subdivision update requirements The `subdivision` field is only accepted in the request **if both** of the following conditions are met: * The [proof-of-address](./update-proof-of-address) process is [`partner-verified`](./introduction#verification) for your organization. * The `country` field is **not** set to `US`. If the `subdivision` field is provided without meeting the above conditions, the request is rejected with a `409 Conflict` error — `operation_not_allowed` with `subdivision-update-non-authoritative-proof-of-address` (when proof-of-address is `uphold-verified`) or `subdivision-update-restricted-by-residence-country` (when `country` is `US`) in `details.reasons`. # Update crypto risk assessment Source: https://developer.uphold.com/rest-apis/core-api/kyc/update-crypto-risk-assessment /_media/specs/core-openapi.mintlify.json patch /core/kyc/processes/crypto-risk-assessment Update the crypto risk assessment process for a user. **Update crypto risk assessment** is the endpoint used to submit the form assessing a user's knowledge of cryptocurrencies and associated risks. This process is **exempt** for all users residing outside of Great Britain (GB). When calling [`GET /core/kyc?detailed=cryptoRiskAssessment`](./get-overview), you will get a `hint` property which includes a [dynamic form](/developer-guides/resources/dynamic-forms/introduction) schema and UI schema. The `hint` property will also be available in responses of this endpoint, in case there are still questions to be answered. For more information about forms, refer to the [form-based processes](./introduction#form-based-processes) section. # Update customer due diligence Source: https://developer.uphold.com/rest-apis/core-api/kyc/update-customer-due-diligence /_media/specs/core-openapi.mintlify.json patch /core/kyc/processes/customer-due-diligence Update the customer due diligence process for a user. **Update customer due diligence** is the endpoint used to submit the form that collects financial information and determines the user's risk level. When calling [`GET /core/kyc?detailed=customerDueDiligence`](./get-overview), you will get a `hint` property which includes a [dynamic form](/developer-guides/resources/dynamic-forms/introduction) schema and UI schema. The `hint` property will also be available in responses of this endpoint, in case there are still questions to be answered. For more information about forms, refer to the [form-based processes](./introduction#form-based-processes) section. # Update email Source: https://developer.uphold.com/rest-apis/core-api/kyc/update-email /_media/specs/core-openapi.mintlify.json patch /core/kyc/processes/email Update the email process for a user. **Update email** is the endpoint through which the user can enter a verified email address. # Update enhanced due diligence Source: https://developer.uphold.com/rest-apis/core-api/kyc/update-enhanced-due-diligence /_media/specs/core-openapi.mintlify.json patch /core/kyc/processes/enhanced-due-diligence Update the enhanced due diligence process for a user. **Update enhanced due diligence** is the endpoint used to collect additional documentation when a user is categorized as **high risk** based on the outcome of the [customer-due-diligence](./update-customer-due-diligence) process. This process typically starts with a status of **exempt**, but it may be triggered automatically if the user's risk profile is elevated during standard due diligence. ## Source of Funds Requirement To complete the enhanced due diligence process, the user must provide **proof of source of funds**, based on the response provided for their primary source of income. As an example, if the declared source is **salary**, the user must submit a **salary receipt** as proof. The document is provided as a media file in `input.media`. At this time, only a single media file is supported for this process. A document that proves the user's source of funds. File Category: `document` (which allows pdf files as well as typical images mime-types) Files are created and uploaded using the [Create File](../files/create-file) endpoint. # Update identity Source: https://developer.uphold.com/rest-apis/core-api/kyc/update-identity /_media/specs/core-openapi.mintlify.json patch /core/kyc/processes/identity Update the identity process for a user. The **Update identity** endpoint is used for verifying that a user is who they claim to be. Identity verification can be done in two ways: ## Via document submission The user must provide actual ID documents (e.g., passport, ID card) and biometrics (e.g., selfie) to prove their identity. There are media files required for the identity verification process that must be passed as `input.media`. The following table lists the supported contexts and their respective file categories. The front side of the photo document. File Category: `document` (which allows pdf files as well as typical images content-types) The back side of the photo document. File Category: `document` (which allows pdf files as well as typical images content-types) A selfie of the user. File Category: `image` A selfie of the user holding the photo document. File Category: `image` A recording of the user going through the process of taking pictures. File Category: `video` Files are created and uploaded using the [Create File](../files/create-file) endpoint. ## Via electronic verification (e-IDV) Some KYC providers allow for identity verification through electronic means, i.e., submission of documents is not provided. This is usually done by checking the user's declared information against public and third-party records, such as government, credit, banking, and utility records. e-IDV must be explicitly requested from your assigned **Account Manager** and approved by **Uphold's Compliance team** before it can be enabled. ## When to use document submission verification vs electronic verification The table below indicates whether each method is adequate for proving user identity. | Transfer type | Electronic verification | Document submission | | ---------------------- | ------------------------------- | ------------------- | | **Card deposits** | Enough for FCA registered firms | Enough | | **Bank deposits** | Not enough | Enough | | **Bank withdrawals** | Not enough | Enough | | **Crypto deposits** | Not enough | Enough | | **Crypto withdrawals** | Not enough | Enough | ## Verification rejection reasons When the process completes with `status: failed`, `output.reason` indicates why the verification was rejected: * `data-mismatch`: the submitted data does not match the document. * `duplicate`: the document or identity is already associated with another user. * `underage`: the user does not meet the minimum age requirement. * `provider-verification-rejected`: the upstream provider rejected the submission. When present, `output.providerDetails.reasons[]` carries provider-specific codes that can be surfaced to the user. # Update phone Source: https://developer.uphold.com/rest-apis/core-api/kyc/update-phone /_media/specs/core-openapi.mintlify.json patch /core/kyc/processes/phone Update the phone process for a user. **Update phone** is the endpoint through which the user can enter a verified phone number. # Update profile Source: https://developer.uphold.com/rest-apis/core-api/kyc/update-profile /_media/specs/core-openapi.mintlify.json patch /core/kyc/processes/profile Update the profile process for a user. **Update profile** is the endpoint through which the user can update their basic information, such as name, date of birth and citizenship. This process solely updates the user's basic information. It does **not** perform identity verification. For identity verification, refer to the [identity](./update-identity) process. When calling [`GET /core/kyc?detailed=profile`](./get-overview), you will get a `hint` property which includes a [dynamic form](/developer-guides/resources/dynamic-forms/introduction) schema and UI schema. The `hint` property will also be available in responses of this endpoint, in case there are still questions to be answered. For more information about forms, refer to the [form-based processes](./introduction#form-based-processes) section. # Update proof-of-address Source: https://developer.uphold.com/rest-apis/core-api/kyc/update-proof-of-address /_media/specs/core-openapi.mintlify.json patch /core/kyc/processes/proof-of-address Update the proof-of-address process for a user. **Update proof-of-address** is the endpoint through which the user proves their declared address of residence. ## Via document submission If electronic verification is not possible, the user must provide a document to prove their address, usually by providing a utility bill or bank statement. The document is provided as a media file, passed in `input.media`. At this time, only a single media file is supported for this process. A document that proves the user's address of residence. File Category: `document` (which allows pdf files as well as typical images content-types) Files are created and uploaded using the [Create File](../files/create-file) endpoint. ## Via electronic verification Some KYC providers allow for address verification through electronic means, without requiring providing any documents. They usually do this by checking the user's declared address of residence against public and third-party records, such as government, credit, banking, and utility records. ## Verification rejection reasons When the process completes with `status: failed`, `output.reason` indicates why the verification was rejected: * `address-mismatch`: the submitted document does not match the declared address. * `name-mismatch`: the name on the submitted document does not match the user's profile. * `provider-verification-rejected`: the upstream provider rejected the submission. When present, `output.providerDetails.reasons[]` carries provider-specific codes that can be surfaced to the user. # Update self-categorization statement Source: https://developer.uphold.com/rest-apis/core-api/kyc/update-self-categorization-statement /_media/specs/core-openapi.mintlify.json patch /core/kyc/processes/self-categorization-statement Update the self-categorization statement process for a user. **Update self-categorization statement** is the endpoint used to submit the form that determines the user's investor profile, including risk level and investment preferences. This process is **exempt** for all users residing outside of Great Britain (GB). When calling [`GET /core/kyc?detailed=selfCategorizationStatement`](./get-overview), you will get a `hint` property which includes a [dynamic form](/developer-guides/resources/dynamic-forms/introduction) schema and UI schema. The `hint` property will also be available in responses of this endpoint, in case there are still questions to be answered. For more information about forms, refer to the [form-based processes](./introduction#form-based-processes) section. # Update tax details Source: https://developer.uphold.com/rest-apis/core-api/kyc/update-tax-details /_media/specs/core-openapi.mintlify.json patch /core/kyc/processes/tax-details Update the tax details process for a user. **Update tax details** is the endpoint used to collect and submit tax information required by applicable regulatory frameworks. This includes tax residency details, taxpayer identification numbers, and certification of the accuracy of the provided information. Requirements vary by region — for example, US citizens must provide information for submitting [IRS Form W-9](https://www.irs.gov/pub/irs-pdf/fw9.pdf), while EU residents must declare their countries of tax residence and relevant tax identification numbers. When calling [`GET /core/kyc?detailed=taxDetails`](./get-overview), you will get a `hint` property which includes a [dynamic form](/developer-guides/resources/dynamic-forms/introduction) schema and UI schema. The `hint` property will also be available in responses of this endpoint, in case there are still questions to be answered. For more information about forms, refer to the [form-based processes](./introduction#form-based-processes) section. # Address Status Changed Source: https://developer.uphold.com/rest-apis/core-api/kyc/webhooks/address-status-changed /_media/specs/core-openapi.mintlify.json webhook core.kyc.address.status-changed The address status has been changed. # Crypto Risk Assessment Status Changed Source: https://developer.uphold.com/rest-apis/core-api/kyc/webhooks/crypto-risk-assessment-status-changed /_media/specs/core-openapi.mintlify.json webhook core.kyc.crypto-risk-assessment.status-changed The crypto risk assessment status has been changed. # Customer Due Diligence Status Changed Source: https://developer.uphold.com/rest-apis/core-api/kyc/webhooks/customer-due-diligence-status-changed /_media/specs/core-openapi.mintlify.json webhook core.kyc.customer-due-diligence.status-changed The customer due diligence status has been changed. # Email Status Changed Source: https://developer.uphold.com/rest-apis/core-api/kyc/webhooks/email-status-changed /_media/specs/core-openapi.mintlify.json webhook core.kyc.email.status-changed The email status has been changed. # Enhanced Due Diligence Status Changed Source: https://developer.uphold.com/rest-apis/core-api/kyc/webhooks/enhanced-due-diligence-status-changed /_media/specs/core-openapi.mintlify.json webhook core.kyc.enhanced-due-diligence.status-changed The enhanced due diligence status has been changed. # Identity Status Changed Source: https://developer.uphold.com/rest-apis/core-api/kyc/webhooks/identity-status-changed /_media/specs/core-openapi.mintlify.json webhook core.kyc.identity.status-changed The identity status has been changed. # Phone Status Changed Source: https://developer.uphold.com/rest-apis/core-api/kyc/webhooks/phone-status-changed /_media/specs/core-openapi.mintlify.json webhook core.kyc.phone.status-changed The phone status has been changed. # Profile Status Changed Source: https://developer.uphold.com/rest-apis/core-api/kyc/webhooks/profile-status-changed /_media/specs/core-openapi.mintlify.json webhook core.kyc.profile.status-changed The profile status has been changed. # Proof-of-address Status Changed Source: https://developer.uphold.com/rest-apis/core-api/kyc/webhooks/proof-of-address-status-changed /_media/specs/core-openapi.mintlify.json webhook core.kyc.proof-of-address.status-changed The proof-of-address status has been changed. # Risk Status Changed Source: https://developer.uphold.com/rest-apis/core-api/kyc/webhooks/risk-status-changed /_media/specs/core-openapi.mintlify.json webhook core.kyc.risk.status-changed The risk status has been changed. # Screening Status Changed Source: https://developer.uphold.com/rest-apis/core-api/kyc/webhooks/screening-status-changed /_media/specs/core-openapi.mintlify.json webhook core.kyc.screening.status-changed The screening status has been changed. # Self Categorization Statement Status Changed Source: https://developer.uphold.com/rest-apis/core-api/kyc/webhooks/self-categorization-statement-status-changed /_media/specs/core-openapi.mintlify.json webhook core.kyc.self-categorization-statement.status-changed The self categorization statement status has been changed. # Tax Details Status Changed Source: https://developer.uphold.com/rest-apis/core-api/kyc/webhooks/tax-details-status-changed /_media/specs/core-openapi.mintlify.json webhook core.kyc.tax-details.status-changed The tax details status has been changed. # Delete metadata Source: https://developer.uphold.com/rest-apis/core-api/metadata/delete-metadata /_media/specs/core-openapi.mintlify.json delete /core/{entity}/{entityId}/metadata Delete metadata for a given entity. **Delete metadata** removes all custom metadata associated with a given entity. This is a permanent operation that cannot be undone. Use the `If-Match` header to ensure you're deleting the expected version of the metadata, preventing accidental deletion of data that was modified concurrently by another process. This operation permanently deletes all metadata. If you only need to remove specific properties, use the [Update metadata](./update-metadata) endpoint with a JSON Patch `remove` operation instead. # Get metadata Source: https://developer.uphold.com/rest-apis/core-api/metadata/get-metadata /_media/specs/core-openapi.mintlify.json get /core/{entity}/{entityId}/metadata Retrieve metadata for a given entity. **Get metadata** retrieves the custom metadata associated with a given entity. The response includes an `ETag` header representing the current version of the metadata. You can use this value with the `If-None-Match` header in subsequent requests to efficiently check if the metadata has changed—if it hasn't, the server returns `304 Not Modified` without the response body, saving bandwidth. Returns `404 Not Found` if no metadata exists for the entity. Create metadata using the [Set metadata](./set-metadata) endpoint. # Set metadata Source: https://developer.uphold.com/rest-apis/core-api/metadata/set-metadata /_media/specs/core-openapi.mintlify.json put /core/{entity}/{entityId}/metadata Create or update metadata for a given entity. **Set metadata** creates or updates custom metadata for a given entity. This endpoint follows idempotent PUT semantics: * **Create**: Returns `201 Created` if metadata doesn't exist * **Replace**: Returns `200 OK` if metadata already exists The response includes an `ETag` header representing the version of the metadata, which can be used with `If-Match` or `If-None-Match` headers for conditional requests to prevent concurrent update conflicts. Each PUT request replaces the entire metadata object. Make sure to include all properties you want to retain. # Update metadata Source: https://developer.uphold.com/rest-apis/core-api/metadata/update-metadata /_media/specs/core-openapi.mintlify.json patch /core/{entity}/{entityId}/metadata Update existing metadata for a given entity. **Update metadata** modifies specific properties using [JSON Patch](http://jsonpatch.com/) operations. Unlike [Set metadata](./set-metadata), this enables partial updates without replacing the entire object. Supported operations: **add**, **replace**, **remove**, **move**, **copy**, and **test**. The response includes an `ETag` header for the new version. Use `If-Match` to prevent conflicting concurrent modifications. Operations are applied sequentially and validated to ensure metadata remains valid. # Get account historical balance Source: https://developer.uphold.com/rest-apis/core-api/portfolio/get-portfolio-account-historical-balance /_media/specs/core-openapi.mintlify.json get /core/portfolio/accounts/{accountId}/historical-balance Retrieves the historical balance of a specific account, including the total and available balance over time. ## Denomination The `denomination` query parameter allows you to denominate rates against another asset. If no value is set for this parameter, it defaults to `USD`. There is a specific set of assets allowed, including but not limited to: `USD`, `EUR`, `GBP`, `AUD`, `CAD`, `NZD`, `MXN`, and `BTC`. If you need a particular asset added to this list, please reach out to your Account Manager. For a more detailed explanation of the denomination concept, check the [Core Concepts](../concepts#denomination) page. ## Interval The `interval` query parameter determines the overall timespan of historical data and the frequency of the data points (rollup frequency). Each interval value defines how far back in time the data spans, and how granular each returned data point is within that range: | Interval | Description | Rollup Frequency | | :---------- | :---------------------------- | :--------------- | | `one-hour` | Past hour of historical data | 30 seconds | | `one-day` | Past day of historical data | 10 minutes | | `one-week` | Past week of historical data | 1 hour | | `one-month` | Past month of historical data | 6 hours | | `one-year` | Past year of historical data | 2 days | # Get account performance Source: https://developer.uphold.com/rest-apis/core-api/portfolio/get-portfolio-account-performance /_media/specs/core-openapi.mintlify.json get /core/portfolio/accounts/{accountId}/performance Retrieves the performance of an account in the portfolio, including the average cost, total invested, and unrealized gains (gains and losses). You can retrieve the performance of several accounts in the portfolio by using the [Get Many Accounts Performance](./get-portfolio-many-accounts-performance) endpoint. All calculations are performed internally in USD. When a denomination asset other than USD is used, amounts are converted using current market exchange rates. This can lead to discrepancies in portfolio performance values, which tend to be minor for assets that remain relatively stable against USD. ## Denomination The `denomination` query parameter allows you to denominate rates against another asset. If no value is set for this parameter, it defaults to `USD`. For a more detailed explanation of the denomination concept, check the [Core Concepts](../concepts#denomination) page. ## Interval The `interval` query parameter determines the time period for which performance data is calculated and returned. This allows you to analyze account performance over different timeframes, from recent short-term activity to comprehensive long-term investment tracking. Each interval value corresponds to a specific historical period: | Interval | Description | | :--------- | :---------------------------- | | `one-hour` | Past hour of historical data | | `one-day` | Past day of historical data | | `all-time` | All historical data available | # Get asset historical balance Source: https://developer.uphold.com/rest-apis/core-api/portfolio/get-portfolio-asset-historical-balance /_media/specs/core-openapi.mintlify.json get /core/portfolio/assets/{asset}/historical-balance Retrieves the historical balance of a specific asset in the portfolio, including the total and available balance over time. ## Denomination The `denomination` query parameter allows you to denominate rates against another asset. If no value is set for this parameter, it defaults to `USD`. There is a specific set of assets allowed, including but not limited to: `USD`, `EUR`, `GBP`, `AUD`, `CAD`, `NZD`, `MXN`, and `BTC`. If you need a particular asset added to this list, please reach out to your Account Manager. For a more detailed explanation of the denomination concept, check the [Core Concepts](../concepts#denomination) page. ## Interval The `interval` query parameter determines the overall timespan of historical data and the frequency of the data points (rollup frequency). Each interval value defines how far back in time the data spans, and how granular each returned data point is within that range: | Interval | Description | Rollup Frequency | | :---------- | :---------------------------- | :--------------- | | `one-hour` | Past hour of historical data | 30 seconds | | `one-day` | Past day of historical data | 10 minutes | | `one-week` | Past week of historical data | 1 hour | | `one-month` | Past month of historical data | 6 hours | | `one-year` | Past year of historical data | 2 days | # Get asset performance Source: https://developer.uphold.com/rest-apis/core-api/portfolio/get-portfolio-asset-performance /_media/specs/core-openapi.mintlify.json get /core/portfolio/assets/{asset}/performance Retrieves the performance of an asset in the portfolio, including the average cost, total invested, and unrealized gains (gains and losses). You can retrieve the performance of several assets in the portfolio by using the [Get Many Assets Performance](./get-portfolio-many-assets-performance) endpoint. All calculations are performed internally in USD. When a denomination asset other than USD is used, amounts are converted using current market exchange rates. This can lead to discrepancies in portfolio performance values, which tend to be minor for assets that remain relatively stable against USD. ## Denomination The `denomination` query parameter allows you to denominate rates against another asset. If no value is set for this parameter, it defaults to `USD`. For a more detailed explanation of the denomination concept, check the [Core Concepts](../concepts#denomination) page. ## Interval The `interval` query parameter determines the time period for which performance data is calculated and returned. This allows you to analyze asset performance over different timeframes, from recent short-term activity to comprehensive long-term investment tracking. Each interval value corresponds to a specific historical period: | Interval | Description | | :--------- | :---------------------------- | | `one-hour` | Past hour of historical data | | `one-day` | Past day of historical data | | `all-time` | All historical data available | # Get historical balance Source: https://developer.uphold.com/rest-apis/core-api/portfolio/get-portfolio-historical-balance /_media/specs/core-openapi.mintlify.json get /core/portfolio/historical-balance Retrieves the historical balance of the portfolio, including the total and available balance over time. ## Denomination The `denomination` query parameter allows you to denominate rates against another asset. If no value is set for this parameter, it defaults to `USD`. There is a specific set of assets allowed, including but not limited to: `USD`, `EUR`, `GBP`, `AUD`, `CAD`, `NZD`, `MXN`, and `BTC`. If you need a particular asset added to this list, please reach out to your Account Manager. For a more detailed explanation of the denomination concept, check the [Core Concepts](../concepts#denomination) page. ## Interval The `interval` query parameter determines the overall timespan of historical data and the frequency of the data points (rollup frequency). Each interval value defines how far back in time the data spans, and how granular each returned data point is within that range: | Interval | Description | Rollup Frequency | | :---------- | :---------------------------- | :--------------- | | `one-hour` | Past hour of historical data | 30 seconds | | `one-day` | Past day of historical data | 10 minutes | | `one-week` | Past week of historical data | 1 hour | | `one-month` | Past month of historical data | 6 hours | | `one-year` | Past year of historical data | 2 days | # Get overview Source: https://developer.uphold.com/rest-apis/core-api/portfolio/get-portfolio-overview /_media/specs/core-openapi.mintlify.json get /core/portfolio Retrieves an overview of the portfolio, including the total portfolio value and a breakdown of individual holdings. ## Denomination The `denomination` query parameter allows you to denominate rates against another asset. If no value is set for this parameter, it defaults to `USD`. For a more detailed explanation of the denomination concept, check the [Core Concepts](../concepts#denomination) page. # Get performance Source: https://developer.uphold.com/rest-apis/core-api/portfolio/get-portfolio-performance /_media/specs/core-openapi.mintlify.json get /core/portfolio/performance Retrieves the performance of the portfolio, including the average cost, total invested, and unrealized gains (gains and losses). All calculations are performed internally in USD. When a denomination asset other than USD is used, amounts are converted using current market exchange rates. This can lead to discrepancies in portfolio performance values, which tend to be minor for assets that remain relatively stable against USD. ## Denomination The `denomination` query parameter allows you to denominate rates against another asset. If no value is set for this parameter, it defaults to `USD`. For a more detailed explanation of the denomination concept, check the [Core Concepts](../concepts#denomination) page. ## Interval The `interval` query parameter determines the time period for which performance data is calculated and returned. This allows you to analyze account performance over different timeframes, from recent short-term activity to comprehensive long-term investment tracking. Each interval value corresponds to a specific historical period: | Interval | Description | | :--------- | :---------------------------- | | `one-hour` | Past hour of historical data | | `one-day` | Past day of historical data | | `all-time` | All historical data available | # Portfolio API introduction Source: https://developer.uphold.com/rest-apis/core-api/portfolio/introduction Retrieve aggregated user financial position data on Uphold: portfolio overview, performance metrics, and historical balances across all accounts and assets. The portfolio group of endpoints provides aggregated insights into a user's financial position across all accounts. These endpoints allow you to track current holdings, performance metrics, and historical balances, offering a comprehensive view of investment performance and portfolio evolution over time. ## Core concepts ### Overview The portfolio **overview** provides a snapshot of current holdings and total portfolio value. This represents the 'What you own right now?' view of a user's financial position, including: * Total portfolio value across all assets * Breakdown of each asset holding with available and total balances ### Performance Portfolio **performance** focuses on investment analytics and profitability metrics. This answers the question 'How well are your investments doing?' by providing: * **Average cost**: The average price paid of an asset * **Total invested**: The total amount of funds invested in the portfolio * **Unrealized gains/losses**: The difference between current value and total invested Performance calculations consider both realized and unrealized gains, providing accurate investment analytics that reflect true portfolio value changes over time. The endpoints offer multiple levels of granularity: Aggregate performance across all user accounts, providing a unified view of investment success. Includes total returns, overall gain/loss percentages, and portfolio-wide investment metrics. Individual account performance metrics, allowing you to analyze which specific accounts are performing best. Essential for understanding asset allocation effectiveness. Bulk performance data retrieval for multiple accounts in a single API call. Optimized for dashboard applications that need to display performance across many accounts efficiently. Individual asset performance metrics, allowing you to analyze which specific asset types are performing best. Essential for understanding asset allocation effectiveness. Bulk performance data retrieval for multiple assets in a single API call. Optimized for dashboard applications that need to display performance across many assets efficiently. ### Historical balance **Historical balance** tracking provides time-series data showing how portfolio values have evolved. This enables trend analysis and 'How did we get here?' insights through: * Data spanning the past hour, day, week, month, or year * Total and available balance history * Account-specific or asset-specific balance evolution ## Use cases The portfolio endpoints power key features in financial applications. Here are examples of how these endpoints are used in the Uphold Wallet to provide comprehensive portfolio insights: Core Data Model Core Data Model The two images above demonstrate real-world implementations of portfolio data. Here's how each endpoint contributes to building these comprehensive views: #### Portfolio overview screen | Annotation | Endpoint | Description | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | 1 & 2 | [Get Portfolio Overview](./get-portfolio-overview) | Core Portfolio Data: The total portfolio balance and individual asset holdings | | 3 | [Get Portfolio Historical Balance](./get-portfolio-historical-balance) | Historical Performance Visualization: The portfolio performance graph displays historical balance trends | | 4 | [Get Portfolio Performance](./get-portfolio-performance) | Performance Metrics: The portfolio performance percentage is calculated | | 5 | [Get Portfolio Account Performance](./get-portfolio-account-performance), [Get Many Accounts Performance](./get-portfolio-many-accounts-performance) | Account-Level Performance: Individual account performance indicators, powered by either single or multiple account endpoints | #### Account detail screen The individual account view focuses on specific account analytics: | Annotation | Endpoint | Description | | ---------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------- | | 1 & 2 | [Get Portfolio Account Performance](./get-portfolio-account-performance) | Account Balance and Performance metrics | | 3 | [Get Portfolio Account Historical Balance](./get-portfolio-account-historical-balance) | Account-specific performance chart showing historical balance | # Get portfolio statement Source: https://developer.uphold.com/rest-apis/core-api/statements/get-portfolio-statement /_media/specs/core-openapi.mintlify.json get /core/statements/portfolio Retrieves the portfolio statement for a given period. # Get transactions statement Source: https://developer.uphold.com/rest-apis/core-api/statements/get-transactions-statement /_media/specs/core-openapi.mintlify.json get /core/statements/transactions Retrieves the transactions statement for a given period. # Introduction Source: https://developer.uphold.com/rest-apis/core-api/statements/introduction The statements group of endpoints provides monthly financial records for a user's portfolio and transactions. These are designed for reporting and reconciliation purposes, giving a complete view of a user's financial activity and holdings for a given period. ## Available statements | Type | Description | | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | | [Portfolio statement](./get-portfolio-statement) | Holdings per asset at the end of the period, with exchange rates in the requested denomination. | | [Transactions statement](./get-transactions-statement) | Paginated list of completed transactions during the period, with full transaction details and per-entry exchange rates. | ## Related guides Retrieve monthly portfolio snapshots and transaction history, and generate compliance reports for your users. # Accept terms of service Source: https://developer.uphold.com/rest-apis/core-api/terms-of-service/accept-terms-of-service /_media/specs/core-openapi.mintlify.json post /core/terms-of-service/{termsOfService}/accept Accept terms of service by code. The `X-Uphold-User-Ip` [user context](../../headers#user-context) header is mandatory on this request, so that the user's IP gets recorded when accepting any Terms of Service. # Get terms of service Source: https://developer.uphold.com/rest-apis/core-api/terms-of-service/get-terms-of-service /_media/specs/core-openapi.mintlify.json get /core/terms-of-service/{termsOfService} Retrieve terms of service by code. If the [subject](../../authentication#subjects) calling the endpoint is a user, the returned response will be contextualized accordingly. As an example, the response will contain the date of acceptance of terms the user has previously accepted. # Terms of Service API introduction Source: https://developer.uphold.com/rest-apis/core-api/terms-of-service/introduction Manage Uphold platform Terms of Service that users must accept to transact. Each ToS has a unique code, with content provided by Uphold during onboarding. The Uphold platform has a set of Terms of Service that users must accept to use the platform. Terms of Service are identified by a unique `code`, and the actual content is provided by Uphold's legal team during your onboarding. ## General terms In order to create a user on the platform, the user must accept the general Uphold Terms of Service applicable to their country. Upon [user creation](../users/create-user), you must provide the general Terms of Service that the user is agreeing to. You can retrieve the applicable general Terms of Service by calling [`GET /core/terms-of-service?type=general&country={country}`](./list-terms-of-service) based on the user's country of residence. ## Other types of terms Depending on what features you want to provide to users, they may need to accept additional Terms of Service. These will be visible for you in their [capabilities](../capabilities/introduction), as requirements. For example, the `unique-account-number-viban` capability will have a requirement of `user-must-accept-unique-account-number-viban-terms-of-service`. You should register acceptance of these additional Terms of Service by calling the [Accept terms of service](./accept-terms-of-service) endpoint. ## Updates to terms Uphold may modify the Terms of Service at any time. If changes are made, you will receive notice so that you can update your integration accordingly. Furthermore, users that agree with a specific Terms of Service also agree with its future modifications. Still, you are required to notify your users of those changes. # List terms of service Source: https://developer.uphold.com/rest-apis/core-api/terms-of-service/list-terms-of-service /_media/specs/core-openapi.mintlify.json get /core/terms-of-service List terms of service of the platform. If the [subject](../../authentication#subjects) calling the endpoint is a user, the returned response will be contextualized accordingly. As an example, the response will contain the date in which the user accepted each terms, if applicable. # Create quote Source: https://developer.uphold.com/rest-apis/core-api/transactions/create-quote /_media/specs/core-openapi.mintlify.json post /core/transactions/quote Create a quote for a transaction. ## Origin and destination The `origin` and `destination` objects identify the source and destination of funds respectively for the transaction. Read more about the [Anatomy of a quote request](/rest-apis/core-api/transactions/introduction) to understand the different types of nodes you can use as origin and destination. Refer to the API spec below to see how each node type is expressed in the request. ## Denomination The `denomination` object defines what is being moved, how much, and which side of the trade the amount applies to precisely. * `asset`: The currency or asset in which `amount` is expressed — for example, `GBP`, `USD`, or `BTC`. Does not need to match the asset of either account; Uphold will convert as needed. * `amount`: The amount to transfer, expressed as a decimal string (e.g. `"100.00"`). * `target`: Controls which side of the trade receives the exact `amount`: * `origin` — Debits exactly `amount` from the origin. Fees are deducted before the destination receives funds. * `destination` — Credits exactly `amount` to the destination. Fees are added on top of what is debited from the origin. #### Example: How target affects fee handling Consider a trade of 500 GBP to BTC with a 2% fee: | Target | Debited from origin | Credited to destination | | ------------- | -------------------------- | -------------------------------------------- | | `origin` | Exactly 500 GBP | BTC equivalent of 490 GBP (after 10 GBP fee) | | `destination` | 510 GBP (500 + 10 GBP fee) | BTC equivalent of exactly 500 GBP | Use `origin` when you want to control exactly how much leaves the sender. Use `destination` when you want to control exactly how much arrives for the recipient. For a more comprehensive explanation of the denomination concept, see the [Core Concepts](/rest-apis/core-api/concepts#denomination) page. ## TTL There are cases in which the default TTL may not be sufficient, such as when you are performing deposits or withdrawals through your own rails (e.g.: your own card processor). In such cases, you want a quote to remain valid for long enough to allow the user to complete the transaction on your side before it expires on our side. The `ttl` field allows you to extend the validity of the quote to accommodate those cases. When omitted, the platform applies a default TTL based on the transaction type. This feature must be enabled for your organization. Contact your **Account Manager** to request access and agree on the maximum allowed TTL. The `expiresAt` field in the response indicates the exact time when the quote will expire, which takes into account the default TTL or the custom `ttl` provided in the request. Use this field to drive quote refresh logic in your UI — schedule the **next refresh slightly before this time** (e.g. a few seconds earlier) to account for network and infrastructure latency, ensuring the user always sees a valid quote. # Create transaction Source: https://developer.uphold.com/rest-apis/core-api/transactions/create-transaction /_media/specs/core-openapi.mintlify.json post /core/transactions Create transaction from a quote. The transaction ID will be the same as the quote ID to allow for easy tracking and correlation. You can optionally include custom [entity metadata](../../entity-metadata) in the `metadata` field to store your own business data (e.g., reference numbers, categorization, reconciliation data). If not provided during creation, you can add it later using [Set metadata](../metadata/set-metadata). Some types of transactions require you to pass [user context](../../headers#user-context) headers, such as `X-Uphold-User-Ip` and `X-Uphold-User-Agent` Headers. # Get transaction Source: https://developer.uphold.com/rest-apis/core-api/transactions/get-transaction /_media/specs/core-openapi.mintlify.json get /core/transactions/{transactionId} Retrieve an existing transaction by id. # Transactions API introduction Source: https://developer.uphold.com/rest-apis/core-api/transactions/introduction Initiate and retrieve transactions on the Uphold platform. Transactions are defined by origin, destination, and direction nodes, with quotes and statuses. The transactions group of endpoints allows you to initiate and retrieve transactions from the platform. ## Transaction nodes Every transaction is defined by three fundamental properties: * `origin`: Defines the origin node of the transaction. * `destination`: Defines the destination node of the transaction. * `denomination`: Defines what and how much is being moved. The `origin` and `destination` are each represented by a node. The following node types are supported across the platform: Account nodes refer to the user's [accounts](../accounts), which are containers of funds of a given asset, stored in Uphold. External accounts refer to the user's [external accounts](../external-accounts), which allow users to deposit and withdraw funds associated with an external source. Crypto addresses enable the use of crypto wallet addresses as transaction nodes within the platform. Crypto transactions can be executed in different modes depending on the destination and environment. Most transactions are processed **on-chain**, directly on the blockchain network. However, when both parties are Uphold users, crypto withdrawals can be processed **off-chain** within Uphold's infrastructure, eliminating network fees and reducing processing time. For development and testing purposes, transactions can be **simulated** without affecting actual blockchain state (user balances will be affected though). The execution mode applied to each transaction is indicated by the [`execution.mode`](./create-transaction#response-transaction-destination-node-execution) property in the crypto address node of the API response. Bank address nodes represent the originating bank account of a push bank deposit, in the cases when adding the originating bank account as an external account is not supported by the platform. They carry the `network` used for the transfer (e.g. `ach`). ## Initiating a quote-based transaction Most transactions are RFQ (Request for Quote) based. This applies to: * **Withdrawals**: Moving funds out to external accounts or crypto addresses. * **Trades**: Converting between different assets. * **Transfers**: Moving funds between accounts of the same asset. * **Pull deposits**: Deposits where you initiate the transaction (e.g., credit or debit card deposits). To create a quote, call the [Create quote](/rest-apis/core-api/transactions/create-quote) endpoint and present the user with the quote details. Quotes have a unique ID and are valid for a limited time. While the user hasn't confirmed the quote, keep refreshing it before it expires. If the user accepts the quote, execute the transaction by calling the [Create transaction](/rest-apis/core-api/transactions/create-transaction) endpoint, passing the quote ID. ### Anatomy of a quote request The supported [node types](#transaction-nodes) differ between `origin` and `destination`: * **Origin**: account external-account * **Destination**: account external-account crypto-address This model allows you to create transactions between different types of nodes in a unified manner, such as between an account and an external account, or between an account and a crypto address. With Uphold's **Anything to Anything** system, you can handle different classes of assets in the origin and destination on a single transaction. For example, you can do a fiat deposit using an external account directly into a BTC account, or you can withdraw from a BTC account directly to an XRP crypto address. ### Requirements Some transactions need additional information before they can be executed. When you create a quote, the response includes a `requirements` array that tells you what extra information is needed — if it's empty, no extra information is needed. Check out the [Handle quote requirements](/developer-guides/crypto-transfers/withdrawal/via-rest-api#handle-quote-requirements) section of the crypto withdrawal guide for a detailed walkthrough. ### Related guides Withdraw crypto from an account. Payout to a bank via quote-based transfers. Fund an account from a credit or debit card. Cash out to a credit or debit card. ## Initiating an external transaction Some transactions are initiated externally: * **Crypto deposits**: Occur when crypto is sent to a deposit address generated via [Set up account deposit method](/rest-apis/core-api/accounts/set-up-account-deposit-method). * **Bank deposits**: Occur when funds are sent to the bank deposit details generated via [Set up account deposit method](/rest-apis/core-api/accounts/set-up-account-deposit-method). * **Bank disbursements**: Occur when funds are pulled from an unlinked external account (e.g., microdeposit disbursements during bank account ownership verification). These transactions are created automatically by the platform when incoming funds are detected. You can monitor them via [Webhooks](/rest-apis/core-api/transactions/webhooks) or by polling the transaction endpoints. The supported [node types](#transaction-nodes) for external transactions are: * **Origin**: account external-account crypto-address bank-address * **Destination**: account external-account crypto-address bank-address ### Related guides Fund an account with crypto. Fund an account via bank transfer. ### Requests for information (RFIs) When a transaction is placed on hold with reason `pending-requests-for-information`, that means it requires additional information to proceed. The pending requests for information (RFIs) can be managed using the [Requests for information](/rest-apis/core-api/transactions/rfis) endpoints. Check out the [Handle on-hold transactions](/developer-guides/crypto-transfers/deposit/via-rest-api#handle-on-hold-transactions) section of the crypto deposit guide for a detailed walkthrough. ## Transaction lifecycle Once a transaction is created, it goes through a series of statuses: * `processing`: The transaction is being processed. This is the initial status of a transaction. * `on-hold`: The transaction is on hold and requires further action by Uphold agents. Once the hold is resolved, the transaction will continue processing. * `completed`: The transaction has been successfully completed. * `failed`: The transaction has failed. ## Transaction types Transactions do not have a type per se, but they can be classified based on the nodes involved in the transaction. The following are the most common types of transactions: * **Deposit**: A transaction in which the origin is external, such as an external account or a crypto address. * **Withdrawal**: A transaction in which the destination is external, such as an external account or a crypto address. * **Trade**: A transaction in which the origin and destination are accounts of different assets. * **Transfer**: A transaction in which the origin and destination are accounts of the same asset. ## Relation with capabilities A transaction can require one or more capabilities to be in a valid state: * `trades`: Required when the underlying origin and destination assets are different. * `crypto-deposits`: Required when the underlying origin is a crypto address. * `bank-deposits`: Required when the underlying origin is an external account of type `bank`. * `deposits`: Required when the underlying origin is external, such as an external account of type `card`. * `receives`: Required when the user receives funds from another user. * `sends`: Required when the user sends funds to another user. * `crypto-withdrawals`: Required when the underlying destination network is of type `crypto`. * `bank-withdrawals`: Required when the underlying destination network is of type `bank`. * `card-withdrawals`: Required when the underlying destination network is of type `card`. ## Transaction amount errors The API returns `transaction_amount_invalid` error when the specified amount violates a platform rule. Below is a breakdown of the cases your integration should handle. ### Per-transaction limits Depending on the type of transaction and the amount on the denomination, you may hit minimum or maximum single limits. If the amount exceeds the maximum, the error `rule` will be set to `maximum-limit-exceeded` and the `limit` object provides the maximum allowed amount under `maximumAmount`: ```json theme={null} { "code": "transaction_amount_invalid", "message": "The amount maximum limit was exceeded", "details": { "context": "body", "property": "denomination", "rule": "maximum-limit-exceeded", "limit": { "maximumAmount": "150" } } } ``` If the amount is below the minimum, the error `rule` will be set to `minimum-limit-not-met` and the `limit` object provides the minimum allowed amount under `minimumAmount`: ```json theme={null} { "code": "transaction_amount_invalid", "message": "The amount minimum limit was not met", "details": { "context": "body", "property": "denomination", "rule": "minimum-limit-not-met", "limit": { "minimumAmount": "0.85" } } } ``` ### Periodic limits Users may have periodic spending limits (daily, weekly, or monthly) that are enforced when transacting. If the transaction amount would exceed the period's allowance, the error `rule` encodes the period that was exceeded. The `limit` object provides the total allowed amount for that period (`maximumAmount`) and how much of it is still available (`remainingAmount`): ```json theme={null} { "code": "transaction_amount_invalid", "message": "The amount exceeds the daily limit", "details": { "context": "body", "property": "denomination", "rule": "amount-exceeds-daily-limit", "limit": { "maximumAmount": "100.00", "remainingAmount": "80.00" } } } ``` Possible rules: `amount-exceeds-daily-limit`, `amount-exceeds-weekly-limit`, `amount-exceeds-monthly-limit`. ### Unsettled funds Some deposit networks (e.g. ACH) do not settle immediately. While a deposit is pending settlement, the respective funds will be locked for withdrawal. If a transaction that moves funds off the user — such as a withdrawal — relies on those locked funds, the request is rejected with `rule` set to `transacting-unsettled-funds`. The `unsettledFunds` object provides the settlement timestamp (`settlementAt`) — which indicates when the funds will be unlocked — the amount locked required to perform the transaction (`shortfallAmount`), and the reasons the funds are locked (`reasons`): ```json theme={null} { "code": "transaction_amount_invalid", "message": "The amount is invalid due to unsettled funds", "details": { "context": "body", "property": "denomination", "rule": "transacting-unsettled-funds", "unsettledFunds": { "settlementAt": "2024-07-26T13:00:00.000Z", "shortfallAmount": "0.95", "reasons": [ "pending-ach-deposit-settlement" ] } } } ``` # List account transactions Source: https://developer.uphold.com/rest-apis/core-api/transactions/list-account-transactions /_media/specs/core-openapi.mintlify.json get /core/accounts/{accountId}/transactions List transactions for an account. # List transactions Source: https://developer.uphold.com/rest-apis/core-api/transactions/list-transactions /_media/specs/core-openapi.mintlify.json get /core/transactions List transactions associated with a user. # Get request for information Source: https://developer.uphold.com/rest-apis/core-api/transactions/rfis/get-request-for-information /_media/specs/core-openapi.mintlify.json get /core/transactions/{transactionId}/requests-for-information/{requestForInformationId} Retrieve a request for information associated with a transaction. # List requests for information Source: https://developer.uphold.com/rest-apis/core-api/transactions/rfis/list-requests-for-information /_media/specs/core-openapi.mintlify.json get /core/transactions/{transactionId}/requests-for-information List requests for information associated with a transaction. # Update request for information Source: https://developer.uphold.com/rest-apis/core-api/transactions/rfis/update-request-for-information /_media/specs/core-openapi.mintlify.json put /core/transactions/{transactionId}/requests-for-information/{requestForInformationId} Update a request for information associated with a transaction. # Transaction created Source: https://developer.uphold.com/rest-apis/core-api/transactions/webhooks/transaction-created /_media/specs/core-openapi.mintlify.json webhook core.transaction.created A new transaction has been created. # Transaction status changed Source: https://developer.uphold.com/rest-apis/core-api/transactions/webhooks/transaction-status-changed /_media/specs/core-openapi.mintlify.json webhook core.transaction.status-changed The transaction status has been changed. # Create user Source: https://developer.uphold.com/rest-apis/core-api/users/create-user /_media/specs/core-openapi.mintlify.json post /core/users Create a new user. **Create user** registers a new user with their identity and compliance information. Users must read and agree with the [general Terms of Service](../terms-of-service/introduction#general-terms) associated with their country of residence, and you must provide it in the `termsOfService` field of the request. You can optionally include custom [entity metadata](../../entity-metadata) in the `metadata` field to store your own business data (e.g., external IDs, settings, tracking parameters). If not provided during creation, you can add it later using [Set metadata](../metadata/set-metadata) endpoint. The `X-Uphold-User-Ip` [user context](../../headers#user-context) header is **mandatory** to record the user's IP when accepting the Terms of Service. # Delete user Source: https://developer.uphold.com/rest-apis/core-api/users/delete-user /_media/specs/core-openapi.mintlify.json delete /core/users/me Delete an existing user. Deleting a user is a sensitive action that must be taken with caution. It's reversible but it will require a manual process to restore the user. The platform will not allow you to delete a user if they have pending transactions or a positive balance. In case of a positive balance, you must ask the user to withdraw their funds before proceeding with the deletion. If a deposit is received to an account of a deleted user, the funds will be kept captive and a manual process to release them will be required. # Get user Source: https://developer.uphold.com/rest-apis/core-api/users/get-user /_media/specs/core-openapi.mintlify.json get /core/users/me Retrieve an existing user. # User created Source: https://developer.uphold.com/rest-apis/core-api/users/webhooks/user-created /_media/specs/core-openapi.mintlify.json webhook core.user.created A new user has been created. # User deleted Source: https://developer.uphold.com/rest-apis/core-api/users/webhooks/user-deleted /_media/specs/core-openapi.mintlify.json webhook core.user.deleted A user has been deleted. # Request webhook management link Source: https://developer.uphold.com/rest-apis/core-api/webhooks/request-management-link /_media/specs/core-openapi.mintlify.json post /core/webhooks/management-link Request a webhook management link to configure webhooks. # Entity metadata Source: https://developer.uphold.com/rest-apis/entity-metadata Attach custom JSON metadata to Uphold entities like users, accounts, and transactions to extend them with your business data and external system mappings. Metadata allows you to store custom, persistent data associated with various entities in the Uphold system. Each entity instance can have its own metadata as a JSON object with a flexible schema tailored to your integration needs. ## Use cases Metadata enables you to extend Uphold entities with your own business data: * **External system integration**: Map Uphold entities to your internal records using custom identifiers * **Application state**: Store user preferences, feature flags, or configuration per entity * **Analytics & tracking**: Add custom tags, cohort identifiers, or attribution parameters * **Business logic**: Persist data needed for your workflows, compliance, or operational rules ## Working with metadata All metadata operations are performed via dedicated endpoints for each entity type. The endpoints follow a consistent pattern: * **Set metadata**: `PUT ////metadata` - Create or replace metadata * **Get metadata**: `GET ////metadata` - Retrieve metadata * **Update metadata**: `PATCH ////metadata` - Partially update metadata using JSON Patch * **Delete metadata**: `DELETE ////metadata` - Remove all metadata Where: * `` is the API name (e.g., `core`) * `` is the entity type supported within the `` (e.g., `users`, `accounts`) * `` 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. Core Data Model Core Data Model ## 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')); ```