# Implementing cash balance transfers

## Prerequisites

Before implementing cash balance transfers, ensure you have:

* **Authentication scopes** -
  * `cash_balance_transfers:admin` to create and cancel transfers.
  * `cash_balance_transfers:read` to retrieve and list them.
  * `positions:read`: to check the cash balance.
* **Webhook handler ready** - Set up a webhook endpoint to listen for transfer lifecycle events and subscribe to `CASH_BALANCE_TRANSFER.*` events.


## Transferring cash between account groups

### Step 1: Confirm the available cash balance

Retrieve the cash balance of the source account group and check that `available_for_withdrawal` covers the amount you intend to transfer.

Example request for the source cash balance

[`GET /account_groups/{account_group_id}/payments/cash_balances`](/api/cash-balances/retrieve_cash_balances)

Response
```json
{
 "meta": {
  "count": 1,
  "total_count": 1,
  "offset": 0,
  "limit": 100
 },
 "data": [
  {
   "account_group_id": "2596db3b-0d03-4651-9eda-970910479dfb",
   "currency": "EUR",
   "balance": "250",
   "locked_for_trading": "50",
   "pending_settlement": "0",
   "available_for_withdrawal": "200",
   "available_for_trading": "200"
  }
 ]
}
```

In this example, EUR 200.00 can be transferred. The remaining EUR 50.00 is reserved for a pending order as shown by the `locked_for_trading` value.

### Step 2: Create the transfer

Create the transfer by specifying the source and target account groups, the amount, and the currency. The request requires an idempotency-key header so that a retried request does not create a second transfer.

Example request to create the transfer

[`POST /payments/cash_balance_transfers`](/api/cash-balance-transfers/create_cash_balance_transfer)

Request
```json
{
 "source_account_group_id": "2596db3b-0d03-4651-9eda-970910479dfb",
 "target_account_group_id": "e56c14b3-8370-4c04-bc67-7092f7ec113c",
 "amount": "200.00",
 "currency": "EUR"
}
```

Response
```json
{
 "id": "b3f6a0c2-1d7e-4a3b-9f21-0c2d4e6f8a10",
 "created_at": "2026-06-25T14:15:22Z",
 "updated_at": "2026-06-25T14:15:22Z",
 "source_account_group_id": "2596db3b-0d03-4651-9eda-970910479dfb",
 "target_account_group_id": "e56c14b3-8370-4c04-bc67-7092f7ec113c",
 "amount": "200.00",
 "currency": "EUR",
 "status": "ISSUED",
 "cancellation_reason": null
}
```

The request fields are as follows.

| **Field** | **Required** | **Description** |
|  --- | --- | --- |
| `source_account_group_id` | Required | The account group the cash is transferred from. Must differ from the `target_account_group_id`. |
| `target_account_group_id` | Required | The account group the cash is transferred to. Must belong to the same user and client as the source, and be of the same [account group type](/products/tol/guides/accounts/accounts_overview#account-group-types). Only `PERSONAL`, `CHILD`, or `BUSINESS` account group types are supported. |
| `amount` | Required | The amount to transfer, expressed as a whole number or with exactly two decimal places. Must not exceed the source account group’s `available_for_withdrawal` amount. |
| `currency` | Required | Three-letter [ISO 4217](https://www.iso.org/iso-4217-currency-codes.html) currency code. This field supports `EUR` (euro) and `GBP` (pound sterling). |


br
The response returns the transfer object, which adds the following fields.

| **Field** | **Required** | **Description** |
|  --- | --- | --- |
| `id` | N/A | Unique identifier of the transfer. Use it as your reference for the transfer. |
| `status` | N/A | Reflects the transfer’s current status. This field supports `ISSUED`, `CONFIRMED`, and `CANCELLED`. |
| `cancellation_reason` | N/A | The reason the transfer was cancelled. Present only when the status is `CANCELLED`. Otherwise, the value is `null`. |
| `created_at`, `updated_at` | N/A | When the transfer was created and last updated. |


All validation happens before any cash moves, so a rejected request never creates a transfer. A request is rejected with a `400` error code if any of the following occurs:

- The source and target account group are the same.
- The account groups do not belong to the same holder.
- The source and target account group types do not match.
- The account group types are different than either `PERSONAL`, `CHILD`, or `BUSINESS`.
- The source account group does not hold enough settled cash.


Webhooks notify you of events in business processes, not of the success or failure of initialising them. Check the HTTP status of the response to your request and handle failure cases accordingly.

### Step 3: Track the transfer status

Rather than polling, listen for the following events:

- `CASH_BALANCE_TRANSFER.ISSUED`
- `CASH_BALANCE_TRANSFER.CONFIRMED`
- `CASH_BALANCE_TRANSFER.CANCELLED`


Each event carries the full transfer details in the `object` field.

Example event for a confirmed transfer

CASH_BALANCE_TRANSFER.CONFIRMED
```json
{
	 "id": "a59c81ee-7770-4f6b-9ebe-8c268b78beb4",
	 "created_at": "2026-06-25T14:15:23Z",
	 "type": "CASH_BALANCE_TRANSFER.CONFIRMED",
	 "object": {
		 "id": "b3f6a0c2-1d7e-4a3b-9f21-0c2d4e6f8a10",
		 "created_at": "2026-06-25T14:15:22Z",
		 "updated_at": "2026-06-25T14:15:23Z",
		 "source_account_group_id": "2596db3b-0d03-4651-9eda-970910479dfb",
		 "target_account_group_id": "e56c14b3-8370-4c04-bc67-7092f7ec113c",
		 "amount": "200.00",
		 "currency": "EUR",
		 "status": "CONFIRMED",
		 "cancellation_reason": null
	 },
	 "webhook_id": "9df39835-be87-4243-9018-f2500b39cee6"
 }
```

You will also receive a `CASH_BALANCE.UPDATED` event for each of the two account groups once the transfer is confirmed.

### Retrieve and list transfers

You can retrieve details of a specific cash balance transfer, and list all transfers for an end user or a business, at any time.

To retrieve a specific transfer:

[`GET /payments/cash_balance_transfers/{cash_balance_transfer_id}`](/api/cash-balance-transfers/retrieve_cash_balance_transfer).

To retrieve a list of transfers for an end user:

[`GET /users/{user_id}/payments/cash_balance_transfers`](/api/cash-balance-transfers/list_user_cash_balance_transfers).

To retrieve a list of transfers for a business:

[`GET /businesses/{business_id}/payments/cash_balance_transfers`](/api/cash-balance-transfers/list_business_cash_balance_transfers).

List results can be sorted by `created_at` or `id`.

### Step 4: Cancel a transfer

A transfer can only be cancelled while it has the status `ISSUED`.

*Example request to cancel a cash balance transfer*

[`DELETE /payments/cash_balance_transfers/{cash_balance_transfer_id}`](/api/cash-balance-transfers/cancel_cash_balance_transfer)

A successful cancellation returns a `204 No Content` status code. No cash is moved, the transfer’s status becomes `CANCELLED`, and `cancellation_reason` explains why. Attempting to cancel a transfer that is already `CONFIRMED` or `CANCELLED` is rejected.