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

> Support FPS and US bank deposits in a single Payment Widget session that handles account selection, bank setup and the transfer instructions.

The Payment Widget's `deposit` flow handles the entire bank deposit setup in one session: the user picks the destination Uphold account, selects **Bank**, completes any bank setup the network requires and gets the transfer instructions to use in their banking app — all without your backend setting up a deposit method or creating a second session.

The Widget picks the network from the bank rails available to the user: **FPS** (GBP) for UK users, and **ACH**, **FedNow / RTP** and **Wire** (USD) for US users, which share the same transfer instructions.

> [!WARNING]
>
> Bank deposits are push transfers: the user sends the funds from their own bank, so the Widget does not create any transaction. Uphold creates the transaction when the transfer arrives. Monitoring and processing it must be handled by your backend via webhooks or polling.

> [!NOTE]
>
> If you need to build the deposit experience yourself, see the select flow guides for [FPS](https://developer.uphold.com/developer-guides/bank-transfers/deposit/via-payment-widget/fps.md) and [ACH, FedNow / RTP and Wire](https://developer.uphold.com/developer-guides/bank-transfers/deposit/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.

## 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 deposit
  U->>B: Create widget session
  B->>A: Create widget session (deposit)
  A-->>B: { session }
  B-->>U: { session }
  U->>P: Initialize widget
  Usr->>P: Select destination account, select bank, complete bank setup
  P-->>Usr: Display bank transfer instructions
  Usr->>P: Close
  P-->>U: complete { via: "deposit-method", selection }
  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
```

***

## Complete the deposit in the widget

The widget session takes the user from account selection through bank setup to the transfer instructions.

### Create a widget session

Call [Create widget session](https://developer.uphold.com/rest-apis/widgets-api/payment/create-session.md) to start the `deposit` 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": "deposit",
  "options": {
    "paymentMethods": [{ "type": "bank" }]
  }
}
```

> [!NOTE]
>
> In the end-to-end flows, use `availableAssets` to restrict the assets the user can deposit into. It replaces any `assets` filter set on the `bank` payment method.

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 initializeDepositWidget = async (session) => {
  const widget = new PaymentWidget<'deposit'>(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 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'; payment 'src'; web-share '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.

> [!NOTE]
>
> The transfer instructions screen offers a **Share** action that opens the device's share sheet, only when the browser supports the Web Share API and the iframe allows `web-share`. The Web SDK's `mountIframe()` grants `web-share`. Without the SDK, add `web-share 'src'` to the iframe `allow` attribute, as in the example above. When sharing isn't available, the action is hidden and the user can still copy each detail.

### Account rules for US deposits

US bank deposits (ACH, FedNow / RTP and Wire) can only be credited to the user's default USD account. When the user picks another account, the Widget handles it:

* **Other payment methods are available:** Bank is shown as available for the default account only, with a **Switch account** action. It switches to the user's default USD account, creating it if needed, and shows the transfer instructions with a notice that the funds will go to that account.
* **Bank is the only payment method:** the Widget shows that the action is unavailable for the chosen account and lets the user go back and pick another one. If the account came from `initialSelection`, going back closes the Widget with a `cancel` event.

If `availableAssets` excludes USD, the user can't pick the default USD account, so US bank deposits are not offered. In a bank-only session, the Widget shows that the action is unavailable, and going back closes the Widget with a `cancel` event.

> [!TIP]
>
> To skip the account-selection screen for US users, set `initialSelection` to `{ "destination": { "asset": "USD" } }`, which resolves to the user's default USD account, or pass that account's ID as `{ "destination": { "accountId": "..." } }`.

### Handle the complete event

Unlike card deposits, the `complete` event of a bank deposit carries a selection rather than a transaction: it fires when the user closes the transfer instructions screen, with `via: "deposit-method"` and the deposit method that was shown. Because the `deposit` flow can complete with either a card transaction or a bank selection, check which one you received: only the bank selection has a `via` property, and checking it with `in` also narrows the type in TypeScript.

```javascript Web SDK theme={null}
widget.on('complete', (event) => {
  const result = event.detail.value;

  if ('via' in result) {
    // Bank deposit: no transaction yet — it is created when the transfer arrives
    const { depositMethod, account } = result.selection;
    handleBankDepositInstructionsShown(depositMethod, account);
  } else {
    // Card deposit: see the card end-to-end guide
    handleCardDeposit(result.transaction, result.trigger);
  }

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

```javascript Without SDK theme={null}
// Inside the onMessage switch from Set up the widget
case 'complete': {
  const result = event.data.value;

  if ('via' in result) {
    // Bank deposit: no transaction yet — it is created when the transfer arrives
    const { depositMethod, account } = result.selection;
    handleBankDepositInstructionsShown(depositMethod, account);
  } else {
    // Card deposit: see the card end-to-end guide
    handleCardDeposit(result.transaction, result.trigger);
  }

  teardown();
  break;
}
```

For bank deposits, the event payload contains:

* `via` — set to `deposit-method`.
* `selection` — contains the `account` that will receive the funds and the `depositMethod` with the transfer instructions. For FPS, the user can still change the receiving account from the transfer instructions screen, so `account` may differ from the one picked at the start.

The widget presents these details directly to the user, so you don't need to display them separately. `depositMethod.details.network` indicates the network: `"fps"` for UK users, and `"ach"` or `"fednow"` (FedNow and RTP) for US users, with `secondaryNetworks` listing the other US networks that share the same instructions. For US deposits, the network the transfer actually arrives on depends on how the user sends it from their bank — for example, a high-priority transfer arrives via FedNow / RTP — and is set in the transaction's `origin.node.network`.

```json FPS theme={null}
{
  "via": "deposit-method",
  "selection": {
    "account": {
      "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8",
      "label": "My GBP account",
      "asset": "GBP"
    },
    "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"
        }
      }
    }
  }
}
```

```json ACH, FedNow / RTP and Wire theme={null}
{
  "via": "deposit-method",
  "selection": {
    "account": {
      "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "label": "My USD account",
      "asset": "USD"
    },
    "depositMethod": {
      "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"]
      }
    }
  }
}
```

> [!WARNING]
>
> `complete` only means the user saw the transfer instructions, not that they sent the transfer. A user can also close the Widget and send the transfer later using the same instructions. 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 reaching the transfer instructions.

```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 handles bank setup issues in-flow — for example, when the bank details can't be set up, Bank is shown as unavailable so the user can pick another payment method. The `error` event is reserved for failures the widget cannot recover from.

***

## 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](https://developer.uphold.com/rest-apis/core-api/transactions/webhooks/transaction-created.md)
    * `status: processing` → bank transfer received, pending posting
  * [core.transaction.status-changed](https://developer.uphold.com/rest-apis/core-api/transactions/webhooks/transaction-status-changed.md)
    * `status: completed` → funds settled
    * `status: failed` → irrecoverable error
* Polling (fallback): [Get transaction](https://developer.uphold.com/rest-apis/core-api/transactions/get-transaction.md)

In sandbox, use the [Simulate bank deposit](https://developer.uphold.com/rest-apis/core-api/accounts/test-helpers/simulate-bank-deposit.md) test helper with the `depositMethod` details from the `complete` event to simulate the incoming transfer.

## Sample transaction

> [tab] FPS
>
> 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",
>         "via": "bank",
>         "id": "aa6e6efa-8d73-497c-8278-0347f459bd68",
>         "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a",
>         "network": "fps"
>       }
>     },
>     "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](https://developer.uphold.com/rest-apis/core-api/external-accounts/get-external-account.md) 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"
>   }
> }
> ```

> [tab] ACH, FedNow / RTP and Wire
>
> Transactions for the other US networks have the same structure as ACH — 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.

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