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

# Bank withdrawal (End-to-end)

> Support FPS and US bank withdrawals in a single Payment Widget session that handles account selection, bank account selection, quoting and order status.

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

The Widget supports **FPS** (GBP) for UK users, and **ACH** and **FedNow / RTP** (USD) for US users. When a US bank account supports both, the user picks the network — standard ACH or instant FedNow / RTP.

> [!NOTE]
>
> If you need to build the amount entry and review screens yourself, see the select flow guides for [FPS](https://developer.uphold.com/developer-guides/bank-transfers/withdrawal/via-payment-widget/fps.md) and [ACH and FedNow / RTP](https://developer.uphold.com/developer-guides/bank-transfers/withdrawal/via-payment-widget/ach.md) instead. To compare all flows, see [Flows](https://developer.uphold.com/widgets/payment/flows.md).

## Prerequisites

* The user has [completed onboarding](https://developer.uphold.com/developer-guides/user-onboarding/overview.md) and has the required capabilities enabled.
* The Payment Widget is set up in your frontend. See [Installation and setup](https://developer.uphold.com/widgets/payment/installation-and-setup.md).
* `@uphold/enterprise-payment-widget-web-sdk` is on version 0.19.0 or later.
* For FPS, the user has a linked bank account. FPS bank accounts are linked automatically after the user's first [FPS deposit](https://developer.uphold.com/developer-guides/bank-transfers/deposit/via-payment-widget/end-to-end.md). For ACH, the user can add one in the Widget.

## 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: Create widget session
  B->>A: Create widget session (withdrawal)
  A-->>B: { session }
  B-->>U: { session }
  U->>P: Initialize widget
  Usr->>P: Select origin account, select/add bank account, enter amount, review quote, confirm
  P->>A: Create transaction
  A-->>P: { transaction }
  A-->>B: webhook: transaction.created (processing)
  P-->>U: complete { transaction, trigger }
  A->>F: Submit bank transfer
  F-->>A: Settlement confirmed
  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 bank account selection through the amount, quote and confirmation.

### Create a widget session

Call [Create widget session](https://developer.uphold.com/rest-apis/widgets-api/payment/create-session.md) 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](https://developer.uphold.com/widgets/payment/installation-and-setup.md#1-create-a-session-on-your-backend) and the [SDK reference](https://developer.uphold.com/widgets/payment/sdk-reference.md#options) for all available options.

By default, the end-to-end flows offer both card and bank. To offer bank only, set `paymentMethods` to `bank`:

```http theme={null}
POST /widgets/payment/sessions
{
  "flow": "withdrawal",
  "options": {
    "paymentMethods": [{ "type": "bank" }]
  }
}
```

Withdrawals can be sourced from any account. If the origin account is not in the bank's currency, the balance is converted at the time of the transaction using Uphold's prevailing rate.

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

```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 Without SDK [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>
```

> [!NOTE]
>
> 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 Web SDK](https://developer.uphold.com/widgets/payment/installation-and-setup.md#native-apps-with-the-web-sdk) for the SDK's native-bundling pattern, or [Setup without the SDK](https://developer.uphold.com/widgets/payment/installation-and-setup.md#setup-without-the-sdk) for the no-SDK approach that loads the session `url` directly as the WebView's top-level page.

### 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, the same as for [card withdrawals](https://developer.uphold.com/developer-guides/card-transfers/withdrawal/via-payment-widget/end-to-end.md#handle-the-complete-event).

```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 is still processing
    handleWithdrawalPending(transaction);
  }

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

```javascript Without SDK 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') {
      handleWithdrawalCompleted(transaction);
    } else if (transaction.status === 'failed') {
      handleWithdrawalFailed(transaction);
    }
  } else if (trigger.reason === 'max-retries-reached') {
    handleWithdrawalPending(transaction);
  }

  teardown();
  break;
}
```

> [!NOTE]
>
> ACH withdrawals can take up to 2 business days to settle, so `complete` will often have `trigger.reason` set to `max-retries-reached` with the transaction still `processing`. Rely on webhooks for the final status.

> [!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.

### Handle cancellations

The `cancel` event fires when the user closes the widget before confirming. 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.

The widget recovers from quote and transaction 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.

***

## Monitor for settlement

Bank withdrawal transactions remain in `processing` while the payout settles. Monitor until the transaction reaches a terminal state.

* Webhook events (recommended):
  * [core.transaction.created](https://developer.uphold.com/rest-apis/core-api/transactions/webhooks/transaction-created.md)
    * `status: processing` → payout submitted to the bank network
  * [core.transaction.status-changed](https://developer.uphold.com/rest-apis/core-api/transactions/webhooks/transaction-status-changed.md)
    * `status: completed` → funds delivered to the user's bank
    * `status: failed` → irrecoverable error
* Polling (fallback): [Get transaction](https://developer.uphold.com/rest-apis/core-api/transactions/get-transaction.md)

## Sample transaction

> [tab] FPS
>
> 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",
>         "via": "bank",
>         "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"
>     }
>   }
> }
> ```

> [tab] ACH and FedNow / RTP
>
> FedNow / RTP transactions have the same structure as ACH — only `destination.node.network` differs: `"ach"` or `"fednow"` (FedNow and RTP).
>
> 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",
>         "via": "bank",
>         "id": "bb7f7fab-9e84-5a8e-9389-1458f56ac790",
>         "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"
>     }
>   }
> }
> ```

## Notify the user

Display an in-app confirmation when the transaction is `completed`, and send an email if applicable.

> [!TIP]
>
> You now support end-to-end bank withdrawals via the Payment Widget.
