> ## 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 withdrawal (End-to-end)

> Support card withdrawals in a single Payment Widget session that handles account selection, card selection, quoting, authorization and order status.

The Payment Widget's `withdrawal` flow handles the entire card withdrawal in one session: the user picks the origin Uphold account, links or selects a card, enters the amount, reviews a live quote, confirms the transaction and sees its status — all without your backend creating a quote or a second session.

<Info>
  If you need to build the amount entry and review screens yourself, see [Select for Withdrawal](/developer-guides/card-transfers/withdrawal/via-payment-widget/select-for-withdrawal) instead. To compare all flows, see [Flows](/widgets/payment/flows).
</Info>

## 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).
* `@uphold/enterprise-payment-widget-web-sdk` is on a version that supports the end-to-end flows.

## 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: Create widget session
  B->>A: Create widget session (withdrawal)
  A-->>B: { session }
  B-->>U: { session }
  U->>P: Initialize widget
  Usr->>P: Select origin account, link/select card, enter amount, review quote, confirm
  P->>A: Create transaction
  A-->>P: { transaction }
  A-->>B: webhook: transaction.created (processing)
  P-->>U: complete { transaction, trigger }

  A-->>B: webhook: transaction.status-changed (completed/failed)
  B-->>Usr: Notify the user
```

***

## Complete the withdrawal in the widget

The widget session takes the user from account and card selection through the amount, quote and authorization.

### Create a widget session

Call [Create widget session](/rest-apis/widgets-api/payment/create-session) to start the `withdrawal` flow. No `data` is required — the widget gathers everything from the user. Use `options` to customize the Widget's `theme` and `layout`, as well as the flow itself. See the example [here](/widgets/payment/installation-and-setup#1-create-a-session-on-your-backend) and the [SDK reference](/widgets/payment/sdk-reference#options) for all available options.

```http theme={null}
POST /widgets/payment/sessions
{
  "flow": "withdrawal",
  "options": {
    "layout": "fluid",
    "theme": { "appearance": "dark" },
    "initialSelection": { "origin": { "asset": "BTC" } }
  }
}
```

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

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

### Set up the widget

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

  const initializeWithdrawalWidget = async (session) => {
    const widget = new PaymentWidget<'withdrawal'>(session, { debug: true });

    widget.on('complete', (event) => {
      console.log('Complete', JSON.stringify(event.detail.value));
      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'));
  };
  ```

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

  <script>
    // session is `response.session` from your backend
    async function initializeWithdrawalWidget(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'; payment '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({ 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 when the user closes the terminal status screen. Its value is the authorization result — the transaction plus what triggered the completion.

<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') {
        handleWithdrawalCompleted(transaction);
      } else if (transaction.status === 'failed') {
        handleWithdrawalFailed(transaction);
      }
    } else if (trigger.reason === 'max-retries-reached') {
      // Polling timed out — the transaction may still be processing
      handleWithdrawalPending(transaction);
    }

    widget.unmount();
  });
  ```

  ```javascript JavaScript theme={null}
  case 'complete': {
    const { transaction, trigger } = event.data.value;

    if (trigger.reason === 'transaction-status-changed' && transaction.status === 'completed') {
      handleWithdrawalCompleted(transaction);
    } else {
      handleWithdrawalPending(transaction);
    }

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

<Warning>
  `complete` is only emitted once the user closes the status screen. A user who abandons the widget there leaves a real transaction behind that your app never hears about. Treat webhooks as the source of truth and `complete` as a UX signal.
</Warning>

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",
        "via": "card",
        "id": "7d5928c5-8ac4-4b0d-8b45-f332ba6a9de7",
        "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a",
        "network": "visa"
      }
    },
    "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"
    }
  }
}
```

### Handle cancellations

The `cancel` event fires when the user closes the widget before authorizing. No transaction exists at that point.

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

### Handle errors

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

The widget recovers from quote and authorization failures internally by returning the user to the review screen with an appropriate action — change the amount, change the payment method, or try again. Insufficient balance is handled this way rather than as an `error` event.

These are the known errors:

| Error | When it happens | What to do |
| - | - | - |
| Session could not be loaded | The `sessionToken` in `session.url` is expired, invalid or already used | Create a new session and initialize the Widget again |

***

## Monitor for settlement

Card withdrawal transactions may remain in `processing` while the payout 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 end-to-end card withdrawals via the Payment Widget.</Check>
