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

# Card deposit via the 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.
* The Payment Widget is set up in your frontend. See [Installation and setup](/widgets/payment/installation-and-setup).

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

Pass `response.session` to your frontend to initialize the widget.

### Set up the widget

<CodeGroup>
  ```javascript Web SDK [expandable] 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'));
  };
  ```

  ```html JavaScript [expandable] theme={null}
  <div id="payment-container"></div>

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

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

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

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

        switch (event.data?.type) {
          case 'load':
            iframe.contentWindow.postMessage({ ...session, options: {}, type: 'init' }, sessionOrigin);
            break;
          case 'ready':
            break;
          case 'complete':
            console.log('Complete', JSON.stringify(event.data.value));
            teardown();
            break;
          case 'cancel':
            console.log('Cancelled');
            teardown();
            break;
          case 'error':
            console.error('Error', event.data.error);
            teardown();
            break;
        }
      }

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

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

### Handle the complete event

The `complete` event fires after the user selects a card. The event payload includes the selected card external account.

<CodeGroup>
  ```javascript Web SDK 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();
  });
  ```

  ```javascript JavaScript theme={null}
  // Inside the onMessage switch from Set up the widget
  case 'complete': {
    const { via, selection } = event.data.value;

    if (via === 'external-account') {
      // selection is the selected card external account
      // use selection.id as the origin in the quote
      handleCardSelected(selection);
    }

    teardown();
    break;
  }
  ```
</CodeGroup>

Once you have the selected card, prompt the user to select a destination account, then create a quote.

### Handle cancellations

<CodeGroup>
  ```javascript Web SDK theme={null}
  widget.on('cancel', () => {
    widget.unmount();
    // Return the user to the previous screen
  });
  ```

  ```javascript JavaScript theme={null}
  // Inside the onMessage switch from Set up the widget
  case 'cancel':
    teardown();
    // Return the user to the previous screen
    break;
  ```
</CodeGroup>

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

<CodeGroup>
  ```javascript Web SDK theme={null}
  widget.on('error', (event) => {
    console.error('Widget error:', event.detail.error);
    widget.unmount();
    // Show a user-friendly error message
  });
  ```

  ```javascript JavaScript theme={null}
  // Inside the onMessage switch from Set up the widget
  case 'error':
    console.error('Widget error:', event.data.error);
    teardown();
    // Show a user-friendly error message
    break;
  ```
</CodeGroup>

<Warning>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.</Warning>

***

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

<Info>Quotes typically **expire** quickly. Prompt for user confirmation within the expiry window and requote if needed.</Info>

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

<CodeGroup>
  ```javascript Web SDK [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;
      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'));
  };
  ```

  ```javascript javascript [expandable] theme={null}
  // session is `response.session` from your backend
  async function initializeAuthorizeWidget(session) {
    const container = document.getElementById('payment-container');
    const sessionOrigin = new URL(session.url).origin;

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

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

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

      switch (event.data?.type) {
        case 'load':
          iframe.contentWindow.postMessage({ ...session, options: {}, type: 'init' }, sessionOrigin);
          break;
        case 'complete': {
          const { transaction, trigger } = event.data.value;
          console.log('Complete', transaction.status, trigger.reason);
          teardown();
          break;
        }
        case 'cancel':
          console.log('Cancelled');
          teardown();
          break;
        case 'error':
          console.error('Error', event.data.error);
          teardown();
          break;
      }
    }

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

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

### Handle the complete event

<Warning>The `complete` event does not guarantee success. Always check `transaction.status` and `trigger.reason`.</Warning>

<CodeGroup>
  ```javascript Web SDK 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();
  });
  ```

  ```javascript javascript theme={null}
  // Inside the onMessage switch from Set up the widget
  case 'complete': {
    const { transaction, trigger } = event.data.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
    }

    teardown();
    break;
  }
  ```
</CodeGroup>

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.

<CodeGroup>
  ```javascript Web SDK theme={null}
  widget.on('cancel', () => {
    widget.unmount();
    // Return the user to the previous screen
  });
  ```

  ```javascript javascript theme={null}
  // Inside the onMessage switch from Set up the widget
  case 'cancel':
    teardown();
    // Return the user to the previous screen
    break;
  ```
</CodeGroup>

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

<CodeGroup>
  ```javascript Web SDK 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
  });
  ```

  ```javascript javascript theme={null}
  // Inside the onMessage switch from Set up the widget
  case 'error': {
    const { code, details } = event.data.error;
    console.error('Widget error:', code, details);
    teardown();
    // Show a user-friendly error message
    break;
  }
  ```
</CodeGroup>

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                     |

<Warning>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.</Warning>

### Complete implementation example

<CodeGroup>
  ```javascript Web SDK [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;
    }
  };
  ```

  ```javascript javascript [expandable] theme={null}
  // session is `response.session` from your backend
  async function initializeAuthorizeWidget(session) {
    const container = document.getElementById('payment-container');
    const sessionOrigin = new URL(session.url).origin;

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

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

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

      switch (event.data?.type) {
        case 'load':
          iframe.contentWindow.postMessage({ ...session, options: {}, type: 'init' }, sessionOrigin);
          break;
        case 'complete': {
          const { transaction, trigger } = event.data.value;
          handleAuthorizeComplete(transaction, trigger);
          teardown();
          break;
        }
        case 'cancel':
          teardown();
          // Return the user to the previous screen
          break;
        case 'error': {
          const { code, details } = event.data.error;
          handleAuthorizeError(code, details);
          teardown();
          break;
        }
      }
    }

    window.addEventListener('message', onMessage);
    container.appendChild(iframe);
  }

  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;
    }
  };
  ```
</CodeGroup>

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.

<Check>You now support card deposits via the Payment Widget.</Check>
