# Initiate Fiat Payout

Create a hosted payout in the bank ramp's sandbox: the user picks a bank account on the hosted page and receives fiat for the crypto amount you specify. Production payouts are currently refused.

## Overview

Aurea asks the bank ramp for the sell channel `cryptoCurrency` → `fiatCurrency`, computes the fiat amount at the channel's rate, checks the channel's minimum and maximum, and creates a hosted payout session. Open `checkoutUrl`: the user selects or adds a bank account and confirms in the bank ramp's interface. The request contains no IBAN.

- The user must be KYC-approved — the same bank ramp onboarding used for pay-in.
- `cryptoCurrency` must have a sandbox payout pair in [Currencies & Networks](https://docs.aureahub.com/docs/bank-currencies.md): today `EURC_TEST`, `PYUSD_TEST`, `USDC_TEST` or `USDG_TEST`. An asset code such as `EURC` is read as `EURC_TEST`. Any other currency answers `400` `NOAH_PAIR_UNAVAILABLE` before anything is read or sent.
- **Sandbox only.** Send `isTestnet: true` and Aurea uses the bank ramp's sandbox. A production request (`isTestnet` `false` or omitted) is refused with `403` `PAYOUT_PRODUCTION_UNAVAILABLE` before anything is read or sent: this hosted payout is funded from the bank ramp balance Aurea keeps for your tenant, not from the user's wallet.
- **Payouts must be switched on** for your tenant in the sandbox, and `cryptoCurrency` offered for payout ([Tenant Settings](https://docs.aureahub.com/docs/bank-settings.md)): otherwise `403` `NOAH_FUNCTION_OFF`, or `400` `NOAH_PAIR_UNAVAILABLE` naming the currencies your tenant offers. Both are checked after the production refusal, before anything is sent, and before an `Idempotency-Key` is replayed. A tenant that does not use the bank ramp gets `403`.
- In the sandbox, Aurea checks that balance first and lets the bank ramp take up to 2% more than `cryptoAmount` (capped at the balance) to absorb rate movements.
- `returnUrl` may be an `https://` URL or an app deep link: the hosted page returns the user to Aurea, which redirects them to your `returnUrl` unchanged. It must be one your tenant allows: once the Aurea operator has added your tenant's return URLs ([Bank Ramp Setup](https://docs.aureahub.com/docs/bank-setup.md)), any other answers `400` `RETURN_URL_NOT_ALLOWED` before anything is read or sent. A tenant with no return URLs yet is not checked.
- **Retries.** Send an `Idempotency-Key` header to make a retry safe: the same key with the same body answers with the first answer, and the response header `idempotency-replayed` is `true`. See [Idempotency](https://docs.aureahub.com/docs/idempotency.md). A retry after a failed attempt sends the bank ramp the request of the first attempt again, so the bank ramp does not see a second payout.
- Follow the payout with [Get Payout Transaction](https://docs.aureahub.com/docs/payout-get.md). The user gets a push notification when it completes or fails.

## Endpoint

### `POST /v1/ramp/bank/payout/initiate`

Authentication: bearer token required.

Creates a hosted payout session in the bank ramp's sandbox and returns the URL where the user completes it. Production requests return 403.

**Headers**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | no | Optional. Up to 255 letters, digits, - or _. The same key with the same body answers with the first answer instead of doing it again; see [Idempotency](https://docs.aureahub.com/docs/idempotency.md) |

**Request body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `cryptoCurrency` | string | yes | Crypto to sell: a currency with a payout pair in Currencies & Networks, e.g. USDC_TEST (an asset code such as USDC is read as USDC_TEST) |
| `cryptoAmount` | string | yes | Integer string in the token's smallest unit, with the decimals Currencies & Networks lists (6 for every current pair): "25000000" = 25 |
| `fiatCurrency` | string | yes | Uppercase ISO 4217 code, e.g. EUR |
| `returnUrl` | string | yes | Where the user lands afterwards: https URL or app deep link |
| `isTestnet` | boolean | no | Must be true (bank ramp sandbox). false or omitted is refused with 403 |

**Responses**

`201` Created

```json
{
  "payoutId": "e8b2d4f6-1a3c-4e5b-8d7f-9c0a2b4d6e81",
  "checkoutUrl": "<hosted payout URL>",
  "fiatAmount": "23.12",
  "fiatCurrency": "EUR",
  "exchangeRate": "0.9248",
  "expiresAt": "2026-09-11T10:30:00.000Z"
}
```

`403` Production

```json
{
  "statusCode": 403,
  "error": "ForbiddenError",
  "message": "Bank payouts are temporarily unavailable. No funds have been moved.",
  "details": { "code": "PAYOUT_PRODUCTION_UNAVAILABLE" }
}
```

`403` Payouts switched off

```json
{
  "statusCode": 403,
  "error": "ForbiddenError",
  "message": "The bank ramp's payout is not switched on for this tenant in sandbox.",
  "details": { "code": "NOAH_FUNCTION_OFF", "function": "payout", "environment": "sandbox" }
}
```

`400` KYC required

```json
{
  "statusCode": 400,
  "error": "BadRequestError",
  "message": "KYC verification required before initiating a payout. Current KYC status: pending.",
  "details": { "kycStatus": "pending", "onboardingStatus": "pending" }
}
```

Other `400` errors, all with a descriptive `message` and, where useful, `details`:

- Return URL your tenant doesn't allow: `RETURN_URL_NOT_ALLOWED`.
- Currency without a sandbox payout pair: `NOAH_PAIR_UNAVAILABLE`, e.g. `USDT_TEST payouts are not available with Noah sandbox. Available: EURC_TEST, PYUSD_TEST, USDC_TEST, USDG_TEST.`
- User never onboarded: `You must complete onboarding before initiating a payout. Please use the onboarding flow first.`
- No the bank ramp channel for the pair: `No payout channel available for USDC → EUR…`
- Amount outside the channel limits: `Payout amount … is below the minimum of …` / `… exceeds the maximum of …` (details include the limit).
- Balance too low: `Insufficient platform balance to process this payout…`

`expiresAt` is `null` when the bank ramp doesn't return an expiry.

## Amounts

`cryptoAmount` is an integer string in the token's smallest unit, and Aurea converts it with the currency's `decimals` from [Currencies & Networks](https://docs.aureahub.com/docs/bank-currencies.md) — 6 for every pair enabled today, so `"25000000"` means 25. The balance check and the 2% cap are computed in that smallest unit, without rounding. `fiatAmount` in the response is a decimal string with 2 decimals (crypto amount × channel rate), and `exchangeRate` is the channel rate as a string.

## Implementation

```javascript
// "25" -> "25000000", "12.5" -> "12500000" (6 decimals, extra digits truncated)
function toSixDecimals(amount) {
  const [whole, frac = ''] = String(amount).split('.');
  return BigInt(whole + frac.padEnd(6, '0').slice(0, 6)).toString();
}

// idempotencyKey: made once for this payout (e.g. crypto.randomUUID()) and sent again on every retry
async function startPayout(token, amountUsdc, idempotencyKey) {
  const res = await fetch('https://api.aureahub.com/v1/ramp/bank/payout/initiate', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${token}`,
      'Idempotency-Key': idempotencyKey
    },
    body: JSON.stringify({
      cryptoCurrency: 'USDC_TEST',
      cryptoAmount: toSixDecimals(amountUsdc),
      fiatCurrency: 'EUR',
      returnUrl: 'myapp://payout/done',
      isTestnet: true // production payouts are refused with 403
    })
  });

  const data = await res.json();
  if (res.status === 403 && data.details?.code === 'PAYOUT_PRODUCTION_UNAVAILABLE') {
    return showPayoutsUnavailable();
  }
  if (!res.ok) throw new Error(data.message); // e.g. below the channel minimum

  savePayoutId(data.payoutId);
  if (await confirmQuote(`You will receive ${data.fiatAmount} ${data.fiatCurrency}`)) {
    window.location.href = data.checkoutUrl; // or open it in an in-app browser
  }
}
```

---

Web version: https://docs.aureahub.com/#payout-initiate
