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

# ACH bank withdrawal via the Payment Widget

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

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

## Prerequisites

* The user has [completed onboarding](/developer-guides/user-onboarding/overview) and has the required capabilities enabled.
* The Payment Widget SDK is installed 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
  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 contains a `session` object. Pass `response.session` to your frontend 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'));
};
```

<Info>
  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).
</Info>

### 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
});
```

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

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

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

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

<Check>You now support ACH bank transfer withdrawals via the Payment Widget.</Check>
