# Pay Out a Quote from Your Wallet

Bind a payout quote whose rate is locked to a bank ramp payout rule, and get the deposit the user's own wallet must send.

## Overview

A [payout quote](https://docs.aureahub.com/docs/payout-quote.md) that is `ready` and still locks its rate becomes a payout here. Aurea asks the bank ramp for an automated payout rule for the source address, and answers where to send: `deposit.amountUnits` of `cryptoCurrency` to `deposit.address`, from `sourceAddress`, on `network`. The user's wallet sends that deposit; the bank ramp recognises it by the address it comes from and pays the beneficiary. Aurea never holds the money and never sends the deposit for the user.

- **The body** is the quote (`quoteId`), the `network` the deposit travels on, and the `source` it comes from — exactly one of `source.walletId` and `source.address`. Both, or neither, answers `400`, as does a `network` that is not one of the bank ramp's names. Unknown fields are ignored. `isTestnet: true` uses the bank ramp's sandbox; `false` or omitted, production.
- **The pair.** The quote's `cryptoCurrency` on `network` must be a payout pair the Aurea registry enables and your tenant offers ([Currencies & Networks](https://docs.aureahub.com/docs/bank-currencies.md)): otherwise `400` `NOAH_PAIR_UNAVAILABLE`, with `details.available` naming what is offered. The quote's currency is the one it was created with; you choose only the network.
- **Two kinds of quote, two kinds of payout.** A quote that **locks a rate** (`quoted` true, signed by the bank ramp, not past `quote.expiresAt`) fixes the rate and both amounts until it expires. A quote the bank ramp answered but **did not sign** is paid too: the beneficiary still receives the quote's `fiatAmount`, and the bank ramp strikes the rate when the deposit lands — so `deposit.amount` is an estimate and the crypto actually sold may differ. Read `quote.locked` on [Get a Quote](https://docs.aureahub.com/docs/payout-quote-get.md) to know which you hold.
- **What is still refused**: `409` `NOAH_QUOTE_NOT_LOCKED` with `details.reason` `not_ready` (the bank ramp still asks for a form step, so the quote has no amounts yet) or `expired` (a locked rate the bank ramp will no longer honour). Ask for a new quote; nothing is stored. A quote that leaves the beneficiary nothing — below the channel's `cryptoLimits.min` the fixed fee takes the whole payout, and the bank ramp prices it at `fiatAmount` `0` rather than refusing it — answers `400` `NOAH_PAYOUT_AMOUNT_TOO_SMALL` with `details.quoteId`: quote above that minimum. Nothing is stored for it either.
- **A payout without a locked rate holds your source address for its whole window** — 24 hours by default. The bank ramp fires that rule on **any** deposit from the address, because matching an exact amount is fragile: a deposit a fraction short would be ignored and the money would sit at an address with no rule to act on it. A second payout from the same source answers `409` `NOAH_PAYOUT_SOURCE_BUSY` until the first is paid, refused or expired.
- **A quote pays one payout.** A quote already bound, whatever that payout's status, answers `409` `NOAH_QUOTE_USED` with `details.quoteId` and `details.payoutId`.
- **One source, one waiting payout per network and currency** — across every tenant, not only yours. While a payout from that address is waiting for its deposit of that currency on that network, another answers `409` `NOAH_PAYOUT_SOURCE_BUSY` with `details.network` and `details.cryptoCurrency`; the payout holding it is never described. The same address on another network, or with another currency, does not collide. The hold ends when the deposit is paid out, when the bank ramp refuses the rule, or when the rule expires — **30 minutes after the quote's `expiresAt`**, so that a deposit sent just before the rate ran out can still be matched.
- **Before anything is stored**, and before an `Idempotency-Key` is looked at: your tenant must bank with the bank ramp and have payouts switched on in that environment (`403`, `403` `NOAH_FUNCTION_OFF`); the user needs a bank ramp profile there (`404` `NOAH_CUSTOMER_NOT_FOUND`) with a completed onboarding and an approved KYC (`422` `NOAH_KYC_NOT_APPROVED`); the quote must be the user's own in that environment (`404` `NOAH_QUOTE_NOT_FOUND`); and the pair and the source must be allowed.
- **The bank ramp must be connected** for your tenant in that environment before the payout is stored: otherwise the call answers `503` `NOAH_NOT_CONFIGURED`, and no payout waits for a rule nobody asked for.
- **Retries.** Send an `Idempotency-Key` header: the same key with the same body answers with the first payout, the bank ramp is not asked twice, and the response header `idempotency-replayed` is `true` — a replay works even after the quote has expired. Every check above is made before a key is replayed. See [Idempotency](https://docs.aureahub.com/docs/idempotency.md).
- The source address, the deposit address, the bank ramp's form session and the signed quote never reach Aurea's logs, and the answer never carries the form session, the signed quote, the bank ramp's rule id, the rule's reference or the bank ramp customer id.

## The Source

The bank ramp attributes the deposit to the rule by the address it came from, so the source must be an address the user controls. Which kinds you may send depends on your tenant's integration modes (`modes` in [Tenant Settings](https://docs.aureahub.com/docs/bank-settings.md)):

- `source.walletId` — one of the user's Aurea wallets, with the *Aurea wallets* mode. The key decides, not the wallet's own chain or environment: an EVM wallet sends on every EVM network — an Ethereum mainnet wallet is a valid source on `PolygonTestAmoy`, because it is the same key — and a Solana wallet on Solana networks. Without the mode, `403` `NOAH_MODE_OFF` with `details.mode` `aureaWallets`.
- `source.address` — an address outside Aurea that the user proved they own in that environment and has not revoked ([Ask for a Challenge](https://docs.aureahub.com/docs/bank-address-challenge.md), [List Addresses](https://docs.aureahub.com/docs/bank-addresses-list.md)), with the *standalone* mode. A proven EVM address is matched in any letter case. Without the mode, `403` `NOAH_MODE_OFF` with `details.mode` `standalone`.

A source the user may not send from answers `400` `NOAH_SOURCE_NOT_ALLOWED` with `details.reason`, and the answer never repeats the address:

- `wallet_not_found`: the wallet is not one of this user's wallets in your tenant.
- `watch_only`: a watch-only import proves nothing about who holds the key, so it cannot send a payout.
- `wrong_network`: an EVM wallet named for a Solana network, or a Solana wallet for an EVM network.
- `address_not_proven`: the address is not one the user proved in that environment for the network's address format, or it has been revoked — including a revocation that lands while this very payout is being stored. Prove it first with [POST /v1/ramp/bank/addresses/challenge](https://docs.aureahub.com/docs/bank-address-challenge.md).

`sourceAddress` in the answer is the form Aurea stores and compares: an EVM address with its checksum, a Solana address as base58. Send the deposit from exactly that address.

While a payout of yours is waiting for its deposit, **the proof of that address cannot be revoked**: [Revoke an Address](https://docs.aureahub.com/docs/bank-address-revoke.md) answers `409` `NOAH_ADDRESS_IN_USE` until the rule lets go. The two are decided together, so a revocation never leaves a payout waiting on an address the user no longer holds, and a payout never opens on one that has just been revoked.

## Endpoint

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

Authentication: bearer token required.

Binds a locked payout quote to a bank ramp payout rule and returns the deposit to send.

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

**Request body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `quoteId` | string | yes | A ready payout quote of the user that still locks its rate (UUID) |
| `network` | string | yes | the bank ramp's network the deposit is sent on, e.g. PolygonTestAmoy; a payout pair of Currencies & Networks |
| `source` | object | yes | Where the deposit comes from: exactly one of the two fields below |
| `source.walletId` | string | no | One of the user's Aurea wallets (UUID). Needs the Aurea wallets mode |
| `source.address` | string | no | An address the user proved they own (1 to 128 characters). Needs the standalone mode |
| `isTestnet` | boolean | no | `true` for the bank ramp's sandbox; `false` or omitted for production |

**Responses**

`201` Waiting for the deposit

```json
{
  "payoutId": "6b1f0d24-9c3a-4f18-b7e5-2a8c4d6e0f13",
  "environment": "sandbox",
  "status": "pending",
  "quoteId": "3f6c2a1e-8b4d-4c7a-9e2f-5d1b0a9c8e7f",
  "network": "PolygonTestAmoy",
  "cryptoCurrency": "USDC_TEST",
  "sourceAddress": "<the source wallet's address>",
  "deposit": {
    "address": "<the bank ramp's deposit address>",
    "amount": "10.3",
    "amountUnits": "10300000",
    "tokenAddress": "0xae1D7d8B36E9AbA7D95A75c69d50b38E7e02A9DD",
    "decimals": 6,
    "chainId": 80002,
    "expiresAt": "2026-09-17T13:00:00.000Z",
    "uri": "ethereum:0xae1D7d8B36E9AbA7D95A75c69d50b38E7e02A9DD@80002/transfer?address=<the bank ramp's deposit address>&uint256=10300000"
  },
  "cryptoAuthorizedAmount": "10.506",
  "cryptoAuthorizedAmountUnits": "10506000",
  "fiatCurrency": "EUR",
  "fiatAmount": "9.5",
  "rate": "0.95",
  "totalFee": "0.285",
  "quote": { "expiresAt": "2026-09-17T12:30:00.000Z" },
  "createdAt": "2026-09-17T12:02:00.000Z",
  "updatedAt": "2026-09-17T12:02:00.000Z"
}
```

`409` Source busy

```json
{
  "statusCode": 409,
  "error": "ConflictError",
  "message": "This source already has a payout waiting for its deposit of USDC_TEST on PolygonTestAmoy: send that deposit, or wait until it expires.",
  "details": { "code": "NOAH_PAYOUT_SOURCE_BUSY", "network": "PolygonTestAmoy", "cryptoCurrency": "USDC_TEST" }
}
```

`409` Quote not locked

```json
{
  "statusCode": 409,
  "error": "ConflictError",
  "message": "This payout quote cannot be paid out: its locked rate has expired. Ask for a new quote.",
  "details": { "code": "NOAH_QUOTE_NOT_LOCKED", "quoteId": "3f6c2a1e-8b4d-4c7a-9e2f-5d1b0a9c8e7f", "reason": "expired" }
}
```

`400` Source refused

```json
{
  "statusCode": 400,
  "error": "BadRequestError",
  "message": "A watch-only wallet cannot send a payout: the Hub has no proof you control its key.",
  "details": { "code": "NOAH_SOURCE_NOT_ALLOWED", "environment": "sandbox", "reason": "watch_only" }
}
```

`502` Outcome unknown

```json
{
  "statusCode": 502,
  "error": "NoahApiError",
  "message": "The bank ramp answered with an unexpected response (HTTP 200)",
  "details": { "code": "NOAH_UNEXPECTED_RESPONSE", "noahStatus": 200, "payoutId": "6b1f0d24-9c3a-4f18-b7e5-2a8c4d6e0f13", "outcome": "unknown" }
}
```

## The Answer

- `payoutId` is what you keep: read the payout again with [Get a Wallet Payout](https://docs.aureahub.com/docs/payout-wallet-get.md). `environment` is `sandbox` or `production`.
- `status` is one of `pending`, `processing`, `completed`, `failed`, `cancelled` and `expired`. It starts at `pending`: the payout is waiting for the deposit. `failed` means the bank ramp refused the rule; `expired`, that no deposit came before the rule's time ran out.
- `deposit.address` is where to send. It is `null` when Aurea could not learn the bank ramp's answer — see **Unknown Outcome** below.
- `deposit.amount` is what to send, written in `cryptoCurrency` (the quote's estimate, `"10.3"`); `deposit.amountUnits` is the same amount in the token's smallest unit (`"10300000"`). **Send exactly `amountUnits`**: it is the number a token transfer takes, and it needs no rounding of your own.
- `deposit.tokenAddress` is the token contract on an EVM network or the mint on Solana, `decimals` its precision, and `chainId` the EVM chain id — `null` on Solana. All three are `null` when the Aurea registry no longer holds that pair with its token details; the amounts already stored on the payout do not change.
- `cryptoAuthorizedAmount`, with `cryptoAuthorizedAmountUnits` beside it, is the most the rule may take from the deposit. It is the quote's authorised amount and is usually above `deposit.amount`.
- `fiatAmount` in `fiatCurrency` is what the beneficiary receives, `rate` the quote's rate and `totalFee` every fee of the quote, in the fiat currency.
- `quote.expiresAt` is when the deposit must have arrived and cleared for the quote's rate to hold. The source stays held for 30 minutes after it.
- `deposit.expiresAt` is **the deadline the deposit itself has**: when the payout rule stops holding the source address — 30 minutes after `quote.expiresAt`. A deposit that arrives after it is matched by no rule. It is `null` only while Aurea never learned the bank ramp's answer.
- `deposit.uri` is that same transfer written in the standard the chain family has, so no one retypes an amount: **EIP-681** on an EVM network (`ethereum:<token>@<chainId>/transfer?address=<deposit>&uint256=<amountUnits>`, the smallest unit) and **Solana Pay** on Solana (`solana:<deposit>?spl-token=<mint>&amount=<amount>`, whole tokens). It carries no reference and no memo: The bank ramp matches the deposit by the address it comes from. It is `null` when the request cannot be written exactly — no deposit address yet, no token details — and the rest of the answer still says where and how much. Wallets honour these requests unevenly: **check the amount before signing**, and treat `amountUnits` as the truth.

## Unknown Outcome

the bank ramp's trigger carries no nonce of its own, so asking twice would make two rules. Aurea therefore asks the bank ramp **once**, and when it cannot tell what happened it keeps the payout and the source's hold rather than trying again.

- **The bank ramp refused** (it answered `400`-`499`): no rule exists. The payout becomes `failed`, the source is free again, and the bank ramp's refusal passes through as Aurea maps it — the bank ramp's `400` becomes `400` `NOAH_INVALID_REQUEST`, `402` becomes `400` `NOAH_INSUFFICIENT_BALANCE`, `403` becomes `403` `NOAH_FORBIDDEN`, `404` becomes `404` `NOAH_RESOURCE_NOT_FOUND`, `401` becomes `502` `NOAH_AUTHENTICATION_FAILED` and `429` becomes `503` `NOAH_RATE_LIMITED`. The same quote then answers `409` `NOAH_QUOTE_USED`; a new quote from the same source works.
- **Anything else** — the bank ramp's `500` (`502` `NOAH_UPSTREAM_ERROR`), its `502`, `503` or `504` (`503` `NOAH_UNAVAILABLE`), no answer in time (`503` `NOAH_TIMEOUT`), no connection (`503` `NOAH_UNREACHABLE`), or an answer Aurea cannot read, one without a deposit address for the network included (`502` `NOAH_UNEXPECTED_RESPONSE`) — may have left a rule at the bank ramp. The answer then carries `details.payoutId` and `details.outcome` `unknown`: the payout stays `pending` with no deposit address, and the source stays held. The same happens when the bank ramp answered and Aurea could not write the answer down.

> ⚠️ **Never retry blindly on an unknown outcome.** Read the payout with [Get a Wallet Payout](https://docs.aureahub.com/docs/payout-wallet-get.md) using `details.payoutId`, and tell the user to send the deposit only once an address is there. Sending the request again — with any key, or none — answers `409` `NOAH_QUOTE_USED` and asks the bank ramp nothing; another quote from the same source answers `409` `NOAH_PAYOUT_SOURCE_BUSY` until the rule expires.

## Errors

Every body is the envelope of [Error Handling](https://docs.aureahub.com/docs/errors.md), with the code in `details.code`.

| Status | details.code | When |
| --- | --- | --- |
| 400 | `—` | The body does not fit: `quoteId` or `source.walletId` is not a UUID, `network` is not one of the bank ramp's network names, `source` carries both fields or neither. Nothing is stored and the bank ramp is not called. |
| 403 | `—` | The token names no tenant, or your tenant does not use the bank ramp. |
| 403 | `NOAH_FUNCTION_OFF` | Payouts are switched off for your tenant in that environment (`details.function` `payout`, `details.environment`). |
| 404 | `NOAH_CUSTOMER_NOT_FOUND` | The user has no bank ramp profile in that environment yet (`details.environment`). |
| 422 | `NOAH_KYC_NOT_APPROVED` | The onboarding is not completed or the KYC is not approved (`details.onboardingStatus`, `details.kycStatus`). |
| 404 | `NOAH_QUOTE_NOT_FOUND` | No payout quote with this id for this user in that environment (`details.environment`). |
| 400 | `NOAH_PAIR_UNAVAILABLE` | The quote's currency on `network` is not enabled for payout by the registry, or not offered for payout by your tenant (`details.environment`, `details.cryptoCurrency`, `details.network`, `details.available`). |
| 403 | `NOAH_MODE_OFF` | `source.walletId` without the Aurea wallets mode, or `source.address` without the standalone mode (`details.mode`, `details.environment`). |
| 400 | `NOAH_SOURCE_NOT_ALLOWED` | The source cannot send this payout (`details.reason`: `wallet_not_found`, `watch_only`, `wrong_network`, `address_not_proven`; `details.environment`). The address is never repeated. |
| 409 | `NOAH_QUOTE_NOT_LOCKED` | The quote cannot be paid (`details.quoteId`, `details.reason`: `not_ready` or `expired`). A quote that simply locks no rate is paid by a rule instead, not refused. |
| 400 | `NOAH_PAYOUT_AMOUNT_TOO_SMALL` | The quote leaves the beneficiary nothing: below the channel's `cryptoLimits.min` the fixed fee takes the whole payout and the bank ramp prices it at zero (`details.quoteId`). Quote above the minimum; nothing is stored. |
| 409 | `NOAH_QUOTE_USED` | The quote is already bound to a payout, whatever its status (`details.quoteId`, `details.payoutId`). |
| 409 | `NOAH_PAYOUT_SOURCE_BUSY` | The source already has a payout waiting for its deposit of that currency on that network (`details.network`, `details.cryptoCurrency`). |
| 400 / 409 | `IDEMPOTENCY_KEY_INVALID, IDEMPOTENCY_KEY_REUSED, IDEMPOTENCY_IN_PROGRESS` | The `Idempotency-Key` is not one, was used with a different body, or its first request is still running. See [Idempotency](https://docs.aureahub.com/docs/idempotency.md). |
| 503 | `NOAH_NOT_CONFIGURED` | Aurea has not connected the bank ramp for your tenant in that environment; the bank ramp was not called and nothing is stored. |
| 400 / 403 / 404 / 502 / 503 | `the bank ramp's own codes` | the bank ramp refused the rule: the payout is `failed` and the source is free again. See Unknown Outcome above. |
| 502 / 503 | `with details.outcome unknown` | Aurea cannot tell whether the bank ramp made the rule. The payout stays `pending` and `details.payoutId` names it — read it, don't retry. |

## Implementation

```javascript
// idempotencyKey: made once for this payout (e.g. crypto.randomUUID()) and sent again on every retry
async function payQuoteFromWallet(token, { quoteId, network, walletId }, idempotencyKey) {
  const res = await fetch('https://api.aureahub.com/v1/ramp/bank/payout/payouts', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': idempotencyKey
    },
    body: JSON.stringify({
      isTestnet: true,
      quoteId,
      network,                    // e.g. "PolygonTestAmoy"
      source: { walletId }        // or { address } for an address the user proved
    })
  });
  const payout = await res.json();

  if (!res.ok) {
    // Aurea could not tell whether the payout rule was made: read the payout, never resend
    if (payout.details?.outcome === 'unknown') {
      return readWalletPayout(token, payout.details.payoutId, true);
    }
    throw Object.assign(new Error(payout.message), { status: res.status, details: payout.details });
  }

  // Send exactly deposit.amountUnits of the token to deposit.address, from payout.sourceAddress
  return payout;
}
```

---

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