# Create a Payout Quote

Price a payout to a bank account with the bank ramp: fees, the amount the beneficiary receives, the rate, and a quote that locks them.

## Overview

After the user has chosen a channel ([Search Channels](https://docs.aureahub.com/docs/payout-channels.md)) and filled in its form ([Channel Form](https://docs.aureahub.com/docs/payout-channel-form.md)) or picked a [saved beneficiary](https://docs.aureahub.com/docs/payout-beneficiaries.md), ask for a quote. Aurea reads the channel, adds your tenant's payout fee and asks the bank ramp to prepare the payout. Nothing moves: a quote is not a payout, and [List Payout Transactions](https://docs.aureahub.com/docs/payout-list.md) does not show it.

- Send exactly one amount: `fiatAmount`, what the beneficiary receives, or `cryptoAmount`, what the user sends. Each is a decimal above zero written as text, up to 38 characters; Aurea sends and keeps it written plainly (`"09.50"` is `"9.5"`).
- `cryptoCurrency` must be offered for payout by your tenant ([Currencies & Networks](https://docs.aureahub.com/docs/bank-currencies.md)): otherwise `400` `NOAH_PAIR_UNAVAILABLE`. In the sandbox an asset code such as `EURC` is read as `EURC_TEST`.
- **Your fee.** The payout fee of your [Tenant Settings](https://docs.aureahub.com/docs/bank-settings.md) for the channel's payment method type — or the one for every type — goes to the bank ramp with the quote. It shows in `totalFee` and as the `BusinessFee` line of `breakdown`.
- **A quote that locks.** With `quoted` (`true` unless you send `false`), once every form step is done the bank ramp signs a quote that fixes the rate and the beneficiary's amount until `quote.expiresAt`: `quote.locked` is `true`. The signed quote itself stays in Aurea. **Locked quotes are enabled per tenant**; where they are not, asking for one answers `400` `NOAH_INVALID_REQUEST` naming the `Quoted` field, and `quoted: false` is the way through — such a quote is still paid, by a rule that strikes the rate when the deposit lands ([Pay from a Wallet](https://docs.aureahub.com/docs/payout-wallet-pay.md)).
- **Mind the channel's minimum.** A payout below a channel's `cryptoLimits.min` leaves the beneficiary nothing once the fixed fee is taken — the bank ramp prices it at `fiatAmount` `0` rather than refusing, and paying that quote answers `400` `NOAH_PAYOUT_AMOUNT_TOO_SMALL` ([Pay from a Wallet](https://docs.aureahub.com/docs/payout-wallet-pay.md)). Read `cryptoLimits` from [Search Channels](https://docs.aureahub.com/docs/payout-channels.md) and quote above it: a US wire rail charging a fixed $20, for instance, needs more than about 20 of the token before anything reaches the beneficiary.
- **Card channels are refused** with `400` `NOAH_CHANNEL_NOT_SUPPORTED`: The bank ramp pays a card only through its hosted checkout. A channel the bank ramp describes without what a payout needs answers `502` `NOAH_UNEXPECTED_RESPONSE`.
- **Payouts must be switched on** for your tenant in that environment: otherwise `403` `NOAH_FUNCTION_OFF`. A tenant that does not use the bank ramp, or a token without a tenant, gets `403`.
- **KYC.** The user needs a bank ramp profile in that environment (`404` `NOAH_CUSTOMER_NOT_FOUND`) with a completed onboarding and an approved KYC: otherwise `422` `NOAH_KYC_NOT_APPROVED` with `onboardingStatus` and `kycStatus`. See [Onboarding Session](https://docs.aureahub.com/docs/payin-session.md).
- the bank ramp's own refusals pass through with their status and `details`, for example `400` `NOAH_INVALID_REQUEST` with the form's `fields`. Nothing is stored then.
- **Retries.** Send an `Idempotency-Key` header to make a retry safe: the same key with the same body answers with the first quote, the bank ramp is not asked twice, and the response header `idempotency-replayed` is `true`. Every check above is made before a key is replayed. See [Idempotency](https://docs.aureahub.com/docs/idempotency.md).
- The form, the payment method id — it contains the account number — and the bank ramp's form session travel only in the request body and are never written to Aurea's logs. `isTestnet: true` uses the bank ramp's sandbox; `false` or omitted, production.

## Endpoint

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

Authentication: bearer token required.

Prices a payout with the bank ramp, with your tenant's payout fee, and keeps the quote.

**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 quote; see [Idempotency](https://docs.aureahub.com/docs/idempotency.md) |

**Request body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `channelId` | string | yes | A channel of Search Channels (UUID) |
| `cryptoCurrency` | string | yes | The currency the user pays out, e.g. EURC_TEST |
| `fiatAmount` | string | no | What the beneficiary receives, in the channel's currency. Send this or cryptoAmount |
| `cryptoAmount` | string | no | What the user sends, in cryptoCurrency. Send this or fiatAmount |
| `paymentMethodId` | string | no | A saved beneficiary of the user (1 to 150 characters) |
| `form` | object | no | The beneficiary details, as the channel's formSchema asks; the bank ramp validates them |
| `quoted` | boolean | no | Ask for a quote that locks the rate once every step is done; `true` unless `false` is sent |
| `isTestnet` | boolean | no | `true` for the bank ramp's sandbox; `false` or omitted for production |

**Responses**

`201` Ready and locked

```json
{
  "quoteId": "3f6c2a1e-8b4d-4c7a-9e2f-5d1b0a9c8e7f",
  "environment": "sandbox",
  "status": "ready",
  "channelId": "ebb9736b-08b3-599d-886b-10ee8aea82b5",
  "paymentMethodType": "BankSepa",
  "country": "DE",
  "fiatCurrency": "EUR",
  "cryptoCurrency": "EURC_TEST",
  "requestedFiatAmount": "9.5",
  "requestedCryptoAmount": null,
  "nextStep": null,
  "totalFee": "0.285",
  "cryptoAmountEstimate": "10.3",
  "cryptoAuthorizedAmount": "10.3",
  "fiatAmount": "9.5",
  "rate": "0.95",
  "breakdown": [
    { "type": "ChannelFee", "amount": "0.2", "fixedAmount": "0.2", "variableAmount": null },
    { "type": "BusinessFee", "amount": "0.1", "fixedAmount": null, "variableAmount": "0.1" },
    { "type": "Remaining", "amount": "10", "fixedAmount": null, "variableAmount": null }
  ],
  "quote": { "locked": true, "expiresAt": "2026-09-17T12:30:00.000Z" },
  "createdAt": "2026-09-17T12:00:00.000Z",
  "updatedAt": "2026-09-17T12:00:00.000Z"
}
```

`201` A form step first

```json
{
  "quoteId": "3f6c2a1e-8b4d-4c7a-9e2f-5d1b0a9c8e7f",
  "environment": "sandbox",
  "status": "needs_step",
  "channelId": "ebb9736b-08b3-599d-886b-10ee8aea82b5",
  "paymentMethodType": "BankSepa",
  "country": "DE",
  "fiatCurrency": "EUR",
  "cryptoCurrency": "EURC_TEST",
  "requestedFiatAmount": "9.5",
  "requestedCryptoAmount": null,
  "nextStep": {
    "stepId": "Vop",
    "stepType": "Ack",
    "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { } }
  },
  "totalFee": null,
  "cryptoAmountEstimate": null,
  "cryptoAuthorizedAmount": null,
  "fiatAmount": null,
  "rate": null,
  "breakdown": [],
  "quote": { "locked": false, "expiresAt": null },
  "createdAt": "2026-09-17T12:00:00.000Z",
  "updatedAt": "2026-09-17T12:00:00.000Z"
}
```

`422` KYC not approved

```json
{
  "statusCode": 422,
  "error": "ValidationError",
  "message": "A payout needs a completed onboarding with the bank ramp and an approved KYC.",
  "details": { "code": "NOAH_KYC_NOT_APPROVED", "onboardingStatus": "completed", "kycStatus": "pending" }
}
```

`400` Card channel

```json
{
  "statusCode": 400,
  "error": "BadRequestError",
  "message": "The bank ramp pays a card only through its hosted checkout: a quote goes through a bank or identifier channel.",
  "details": { "code": "NOAH_CHANNEL_NOT_SUPPORTED", "paymentMethodCategory": "Card" }
}
```

## Form Steps

The bank ramp may ask for a step before it prices the payout — a payee check (`Vop`), for instance. The quote is then `needs_step`: render `nextStep.schema` (`stepType` `Ack` is an acknowledgement, `DataEntry` asks for new details) and send the answers with [Answer a Form Step](https://docs.aureahub.com/docs/payout-quote-step.md). Repeat until the quote is `ready`. The amounts are the payout's only then.

- `breakdown` is in `cryptoCurrency`: `ChannelFee` + `BusinessFee` + `Remaining` = `cryptoAmountEstimate`. `totalFee` is in the fiat currency.
- Read a quote again at any time with [Get a Quote](https://docs.aureahub.com/docs/payout-quote-get.md); `quote.locked` turns `false` once it has expired. Ask for a new quote then.

## Implementation

```javascript
// idempotencyKey: made once for this quote (e.g. crypto.randomUUID()) and sent again on every retry
async function quotePayout(token, { channelId, fiatAmount, paymentMethodId, form }, idempotencyKey) {
  const res = await fetch('https://api.aureahub.com/v1/ramp/bank/payout/quotes', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': idempotencyKey
    },
    body: JSON.stringify({
      isTestnet: true,
      channelId,
      cryptoCurrency: 'EURC_TEST',
      fiatAmount,          // e.g. "9.5": what the beneficiary receives
      paymentMethodId,     // a saved beneficiary, or leave it out and send form
      form
    })
  });
  const quote = await res.json();
  if (!res.ok) throw Object.assign(new Error(quote.message), { status: res.status, details: quote.details });
  return quote;            // status "needs_step": show quote.nextStep; "ready": show the amounts
}
```

---

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