# Withdraw from the User's Balance

> Coming soon, upon request — this feature is turned on for your tenant by the Aurea team. [Contact us](https://www.aureahub.com/contact)

Pay from the user's balance on the local rails to a bank account, a mobile money number or a wallet.

## Overview

`destination` is one of:

- `bank`: `institutionId` (from [Banks & Mobile Money](https://docs.aureahub.com/docs/local-institutions.md)), `accountNumber`, `beneficiaryName`. A KES account number is 5 to 20 digits; the name is not checked against the account, so ask the user to check it.
- `momo`: `institutionId`, `mobileNumber` (international format, `+254…`), `beneficiaryName`.
- `wallet`: `network` (`ethereum`, `solana` or `tron`), `address` (an `ethereum` address in its checksummed form), `beneficiaryName`, `beneficiaryType` (`individual` or `business`), `beneficiaryCountry` (two letters) and, for an individual, `beneficiaryDateOfBirth` (`YYYY-MM-DD`). Networks that need a memo are not supported.
- `beneficiary`, for `USD` only: `beneficiaryId`, a USD beneficiary Aurea registers for your tenant on request. A USD withdrawal pays nothing else. An unknown beneficiary answers `404` `BANK_RAMP_BENEFICIARY_NOT_FOUND`, one not active yet `409` `BANK_RAMP_BENEFICIARY_NOT_ACTIVE`; a beneficiary that is a third party needs an invoice this route does not take (`400` `BANK_RAMP_PAYOUT_DOCUMENT_REQUIRED`).

- **Another currency.** To pay `asset` from a balance in another asset — local currency from a stablecoin, for example — name `sourceAsset` and the most it may give, `sourceAmountLimit`.
- **Fees.** The bank ramp's fee and your tenant's markup are reserved with the amount and settled when known (`fee`, `rate`, `sourceAmount`).
- **Status**: `prepared`, `sending`, `paused` (the bank ramp is paused: Aurea sends it again), `unknown` (the answer was lost: Aurea checks it and never pays twice), `refused`, `held` (the money is reserved at the bank ramp), `pending` (sent to the bank or the operator), `succeeded`, `failed` (the reserved money returns to the balance) or `reversed` (the bank returned it after it succeeded). Each change is sent as `ramp.withdrawal.updated`.
- **The user's passkey.** Confirmed with the user's passkey step-up for `bank_ramp.payout`, in the `x-aurea-step-up` header ([Passkeys & Step-Up](https://docs.aureahub.com/docs/guide-passkeys.md) › Where It Is Asked): without it `403` `STEP_UP_REQUIRED`, with one that is not for this action `403` `STEP_UP_INVALID`. The same `Idempotency-Key` after an answer needs none.
- A failed withdrawal is not sent again on its own: a new attempt is a new withdrawal, with `retryOf` naming the failed one.
- Amounts below the bank ramp's minimum or above its maximum for the asset and destination answer `BANK_RAMP_AMOUNT_BELOW_MINIMUM` or `BANK_RAMP_AMOUNT_ABOVE_MAXIMUM`.
- `409` `BANK_RAMP_PAYOUTS_HELD_RECONCILIATION` while your tenant's payouts in that environment are held for a reconciliation of the bank ramp's ledger: nothing is reserved, try again later.
- `409` `BANK_RAMP_PAYOUT_BALANCE_LOW` when the balance does not cover the amount and its fees; `422` `BANK_RAMP_LIMIT_EXCEEDED` or `BANK_RAMP_VELOCITY_EXCEEDED` past your tenant's 24-hour limits; `423` when your tenant or the user is frozen.

Called with the user's access token, in one environment (`isTestnet`). See [Bank Ramp: Local Rails](https://docs.aureahub.com/docs/guide-bank-local.md).

## Endpoint

### `POST /v1/ramp/bank/customer/withdrawals`

Authentication: bearer token required.

Pays from the user's balance to a bank account, a mobile money number or a wallet. Send an Idempotency-Key header: the same key with the same body answers with the first answer, with the response header idempotency-replayed: true (see Idempotency).

**Request body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `asset` | string | yes | What the beneficiary receives |
| `amount` | string | yes | How much, as decimal text in major units |
| `sourceAsset` | string | no | To pay from a balance in another asset |
| `sourceAmountLimit` | string | no | The most the balance in `sourceAsset` may give |
| `destination` | object | yes | A `bank`, `momo`, `wallet` or, for USD, `beneficiary` destination (above) |
| `travelRule` | object | no | The originator and beneficiary data, where your tenant's rules ask for it |
| `isTestnet` | boolean | no | `true` for the sandbox; `false` or omitted for production |

**Responses**

`201` Withdrawal

```json
{
  "id": "9a2c4e6f-1b3d-4a5c-8e7f-0d2b4c6e8a10",
  "environment": "production",
  "purpose": "off_ramp",
  "attempt": 1,
  "retryOf": null,
  "asset": "KES",
  "amount": "20000.00",
  "sourceAsset": "KES",
  "sourceAmount": "20000.00",
  "fee": "150.00",
  "rate": null,
  "destinationType": "momo",
  "destination": {
    "institutionId": "mpesa-ke",
    "mobileNumber": "+2547•••••678",
    "beneficiaryName": "J•• W••••"
  },
  "status": "pending",
  "failureCode": null,
  "sentAt": "2026-10-23T09:00:02.000Z",
  "completedAt": null,
  "createdAt": "2026-10-23T09:00:00.000Z",
  "updatedAt": "2026-10-23T09:00:02.000Z"
}
```

`403` Not verified

```json
{
  "statusCode": 403,
  "error": "ForbiddenError",
  "message": "The user's verification with the bank ramp is not complete.",
  "details": {
    "code": "BANK_RAMP_KYC_REQUIRED"
  }
}
```

`422` Past the limit

```json
{
  "statusCode": 422,
  "error": "ValidationError",
  "message": "This would take this user KES past its limit of 300000.00 in 24 hours (290000.00 used).",
  "details": {
    "code": "BANK_RAMP_LIMIT_EXCEEDED",
    "asset": "KES"
  }
}
```

`409` Balance too low

```json
{
  "statusCode": 409,
  "error": "ConflictError",
  "message": "The balance does not cover this payout with its fees",
  "details": {
    "code": "BANK_RAMP_PAYOUT_BALANCE_LOW"
  }
}
```

---

Web version: https://docs.aureahub.com/#local-withdrawal-create
