---
updatedAt: 2026-09-30T15:54:20.000Z
---

Fetch the complete documentation index at: https://docs.avvio.xyz/llms.txt. Use this file to discover all available pages before exploring further. Append .md to any documentation page URL to get its markdown version.

# Errors

Every failure has the same shape. Branch on `type`, which is stable across
versions; the HTTP status and the wording are not.

```jsonc
{
  "type": "RATE_DRIFT_EXCEEDED",
  "status": 400,
  "detail": "Refusing to send: quoted 3384.65 but you expected ~9999 (6615 bps of drift, limit 200). Nothing was sent.",
  "resolution": "Nothing was sent. Re-quote, show the payer the new amount, and send again.",
  "requestId": "req-1c",
}
```

`detail` is always a string. `resolution` is optional, so fall back to
`detail`. Validation failures add `errors`, the fields that failed. Every error
body has `requestId`; on a success it is the `x-request-id` header, so log it.
There is no `retryable` field; the Node client derives one from `type`.

A payout that was accepted and later failed is not an error response. It has
`status: failed` and a `failureCode` ([Track status & failures](/status/#failure-codes)).

## Request errors

| `type` | Status | What to do |
| --- | --- | --- |
| `VALIDATION_ERROR` | 400 | Fix the fields named in `errors` |
| `UNAUTHORIZED` | 401 | Key missing, malformed, unknown, revoked or copied incompletely |
| `FORBIDDEN` | 403 | The key can't access that organization or operation. Check `AVVIO_ORG_ID` |
| `NOT_FOUND` | 404 | No such payout or recipient. Use ids we returned |
| `ROUTE_NOT_FOUND` | 404 | No route matches. Payouts are sent to `/payouts` and read from `/orders` |
| `RATE_LIMITED` | 429 | Wait for `Retry-After`, then resend |
| `BAD_REQUEST` | 400 | A non-field 400 (expired quote, above the corridor maximum, non-positive amount). `detail` names it |
| `PROVIDER_REJECTED` | 400 | The network refused before submission, usually below the corridor minimum |
| `CONFLICT` | 409 | Funding state changed under you (another transaction or request won). Re-read the payout |
| `ACCOUNT_BLOCKED` | 403 | Your organization is suspended. Contact us |
| `LIVE_KEY_ORG_NOT_APPROVED` | 403 | A live key wrote before business verification. GETs work; use a test key until approved |
| `INSUFFICIENT_BALANCE` | 400 | Nothing was sent. Top up, then retry. A payroll run must branch on this |
| `CRYPTO_PAYOUTS_DISABLED` | 400 | Use a key with scopes `["write", "crypto_payouts"]` |
| `WALLET_NOT_PROVISIONED` | 400 | No EVM wallet to pay a wallet recipient. Finish wallet setup in the dashboard |
| `HISTORY_TOO_LARGE` | 413 | Your organization has more orders than `GET /balance/history` will replay. Page `GET /balance_transactions` instead |
| `ORDERS_TEMPORARILY_UNAVAILABLE` | 503 | A payment-network read failed while building the page, and we won't return a partial list as complete. Retry. (A `cursor` we didn't issue is a `400 VALIDATION_ERROR`, `cursor: unrecognized`) |
| `INTERNAL` | 500 | Nothing was recorded and the key was released. Retry with the same key; send the `requestId` if it persists |
| `PAYOUT_LINKS_UNAVAILABLE` | 503 | Hosted payout links aren't configured on this environment. Contact support |
| `CORRIDOR_UNAVAILABLE` | 400 | Node client: the corridor isn't on your routing. Pick one the corridors call lists |
| `TIMEOUT` | 504 | Node client. A send may exist. Retry with the same `Idempotency-Key` |
| `NETWORK_ERROR` | 502 | Node client (DNS, TLS, dropped connection). Retry with the same key, then read the payout back |

> [!CAUTION]
> **A timeout is not a failure**
>
> After `TIMEOUT`, `NETWORK_ERROR` or `PAYOUT_OUTCOME_UNKNOWN` the payout may
> exist. Retry with a new `Idempotency-Key` and you pay twice.

## API keys

| `type` | Status | What to do |
| --- | --- | --- |
| `KEY_EXPIRED` | 401 | Issue a new key; an expired key can't be rotated |
| `KEY_IP_NOT_ALLOWED` | 401 | The key is pinned and the request came from another address |

## Idempotency

| `type` | Status | What to do |
| --- | --- | --- |
| `IDEMPOTENCY_KEY_REQUIRED` | 400 | Add an `Idempotency-Key`, unique per operation |
| `IDEMPOTENCY_KEY_INVALID` | 400 | Use 1–255 characters of `A-Z a-z 0-9 _ . : -`. A UUID works |
| `IDEMPOTENCY_KEY_CONFLICT` | 409 | Same key, different body. Don't retry; a different request needs a new key |
| `IDEMPOTENCY_KEY_REQUEST_IN_PROGRESS` | 409 | An identical request is still running. Back off and retry the same key |
| `IDEMPOTENCY_UNAVAILABLE` | 503 | We couldn't record the key and nothing executed. Retry the same key |
| `PAYOUT_OUTCOME_UNKNOWN` | 500, 409 | No proof a money operation didn't execute; the payout may exist and the key is kept. Replays answer `409`. Do not retry with a new key. Poll `GET /orders?reference=`, send support the `requestId` ([Timeouts](/idempotency/#timeouts)) |
| `DUPLICATE_REQUEST_DETECTED` | 409 | An identical body under a different key within 15 minutes. Nothing executed. See below |

Some HTTP clients mint a new key per attempt, so every retry would pay.
`DUPLICATE_REQUEST_DETECTED` catches that for 15 minutes, and its body names
`originalIdempotencyKey`, plus `originalPayoutId` when the first request
finished with a payout.

> [!WARNING]
> Nothing was executed. Reading this as "already paid" leaves a worker unpaid
> while your ledger says otherwise.

- If it was a retry, send it again with `originalIdempotencyKey` in the
  `Idempotency-Key` header (not the body, which rejects unknown properties). It
  replays and returns the original payout.
- If you meant two payments, add `X-Allow-Duplicate: true`. This sends a second
  real payment.

Fifteen minutes covers a crashed job that requeues on a backoff. It is shorter than the seven-day key retention because content can't
tell a retry from a genuine repeat: two identical advances in a week are
ordinary payroll. A unique `reference` per payment means the guard never fires
falsely. [Idempotency](/idempotency/#key-release-versus-terminal-failures)
covers retention and exceptions.

## Sending and funding

| `type` | Status | What to do |
| --- | --- | --- |
| `DESTINATION_ACCOUNT_NOT_FOUND` | 404 | Refused before pricing, so a stale id can't report `completed` with nobody paid. Pay only ids we returned |
| `RATE_DRIFT_EXCEEDED` | 400 | The quote moved past `expectDestination`. Re-quote, show the new amount, send again |
| `QUOTE_UNVERIFIABLE` | 400 | We couldn't compare the quote to your expectation, so nothing was sent. `detail` says why |
| `QUOTE_NOT_POSITIVE` | 400 | Nothing would arrive after fees. Increase the amount and re-quote |
| `EXACT_OUTPUT_UNSUPPORTED` | 400 | The routing can't lock the receiving amount. Check `capabilities.exactOutput` |
| `INDICATIVE_PRICING_UNAVAILABLE` | 400 | The routing prices only against a recipient (`capabilities.indicativePricing: false`). Create the recipient, then price |
| `PAYOUT_NOT_CANCELABLE` | 400 | Only a payout awaiting your own funds can be canceled |
| `INSUFFICIENT_SCOPE` | 403 | Read-only key, or a missing consent (`refunds`). Use a `write` key; an owner or admin can add `refunds` |
| `LIVE_MODE_UNSUPPORTED` | 400 | A live key on a sandbox-only route. Use an `avvio_test_*` key |
| `PAYOUT_LIMIT_EXCEEDED` | 422 | Over a single, daily or per-end-user limit, named in the message. Split it, or ask us to raise it |
| `PAYOUT_REFUSED` | 422 | Final refusal for this recipient. Contact us with the `requestId` |
| `USE_POST_PAYOUTS` | 403 | An approval threshold or velocity cap applies, which only `POST /payouts` enforces. Send it there; it may answer `202` |
| `BENEFICIARY_EXTERNAL_ID_CONFLICT` | 409 | The `externalId` names a recipient with other account details. Use a new one, or read the existing recipient |
| `BANK_ACCOUNT_ALREADY_LINKED` | 409 | The account is on another recipient. Pay `existingRecipientId` / `existingMethodId` |
| `PAYOUT_ACCOUNT_PROVIDER_UNAVAILABLE` | 503 | The account couldn't be registered and nothing was saved. Retry with the same `Idempotency-Key` |
| `FUNDING_NOT_APPLICABLE` | 501 | The payout settles from your balance. Fund only when `requiresFunding` is `true` |
| `FUNDING_TRANSACTION_INVALID` | 400 | The transaction doesn't fund this payout (reverted, wrong token, address or wallet, short). Nothing recorded |
| `FUNDING_NOT_YET_VERIFIABLE` | 409 | Not mined or readable yet. Nothing recorded. Confirm the same hash later |
| `FUNDING_TRANSACTION_ALREADY_USED` | 409 | It funded another payout, named in `detail`. Send a separate transfer |
| `PAYOUT_NOT_FUNDABLE` | 400 | The payout is canceled or finished. If you still owe the recipient, create a new payout |
| `FUNDING_QUOTE_EXPIRED` | 409 | Dashboard wallet funding: the quote expired and the payout wasn't sent. Funding already sent stays recorded. Refresh and review the new price |
| `LINK_EXPIRED` | 400 | A checkout draft's `expiresAt` has passed. `PATCH` a later one (or `null`) and publish. In a `publish: true` create it is `publishError.type` on a `201` |

## Refunds

Every refusal here left the money where it was. Only `REFUND_OUTCOME_UNKNOWN`
may have moved it.

| `type` | Status | What to do |
| --- | --- | --- |
| `REFUND_NOT_ALLOWED` | 400 | Already refunded in full, charged back, failed or pending |
| `REFUND_DISPUTED` | 400 | A dispute decides. Wait; a loss arrives as `checkout_payment.reversed` |
| `REFUND_EXCEEDS_REMAINING` | 400 | More than `amountBase - refundedBase`. Lower it, or omit it to refund the rest |
| `REFUND_IN_PROGRESS` | 409 | One refund at a time. Once `refundedBase` moves, send another if needed |
| `REFUND_OUTCOME_UNKNOWN` | 502 | It may have gone through. Don't resend; the payment is re-read and the event follows if it did |

## Batches and payout links

| `type` | Status | What to do |
| --- | --- | --- |
| `BATCH_NOT_FOUND` | 404 | No batch with that id in your organization |
| `PAYOUT_BATCH_NOT_CONFIRMABLE` | 409 | Not `awaiting_confirmation`. Read it back; it may still be validating |
| `PAYOUT_BATCH_NOT_CANCELABLE` | 409 | Creation started, so the run is committed. Cancel single `pending` payouts with `POST …/payouts/{payoutId}/cancel` |
| `PAYOUT_BATCH_AWAITING_APPROVAL` | 409 | The run needs approvers, or one rejected it. `approvalId` names the request |
| `PAYOUT_BATCH_DUPLICATE_REFERENCE` | 409 | `externalReferenceId` already names run `originalBatchId`. Read it, or pick a new id |
| `MASS_PAYOUTS_DISABLED` | 403 | Your organization opted out of batches. Contact support |
| `PAYOUT_LINK_UNUSABLE` | 400 | Expired, spent, or failed at execution. Links are single-use; mint a new one |

A failed line inside an accepted batch is an item with `status` `invalid` or
`create_failed` and `errors[]`, read with `?status=invalid`. Submitting a spent
payout link returns the original payout with `status: "already_submitted"`. A
`404` on a link route covers expired, spent, forged and unknown links alike, so
probing learns nothing.
