---
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.

# Idempotency

An idempotency key makes a retry safe. Send the same `Idempotency-Key` with the
same request and it runs once, however many times you send it. The repeat
returns the original response with `Idempotency-Replayed: true`.

Every `POST`, `PATCH` and `DELETE` accepts the header, except the routes
[that ignore it](#routes-that-ignore-the-header). An API key must send it on the
writes that move money or create something durable, or the call is refused with
`400 IDEMPOTENCY_KEY_REQUIRED`:

| Required on | Route |
| --- | --- |
| Recipients | `POST /recipients/{orgId}`, `POST /recipients/{orgId}/{recipientId}/methods` |
| Payouts | `POST /payments/organizations/{orgId}/payouts`, `…/quotes/accept`, `…/payouts/{payoutId}/cancel` |
| Batches | `POST …/payouts/batches`, `…/batches/{batchId}/confirm`, `…/batches/{batchId}/cancel` |
| Links and funding | `POST …/payout-links`, `…/payouts/{payoutId}/funding/confirm`, `…/sandbox/fund` |
| Checkout | Under `/checkout/organizations/{orgId}`: `POST`/`PATCH`/`DELETE` on `products` and `links`, `…/links/{linkId}/publish`, `…/links/{linkId}/pause`, `…/payments/{paymentId}/refund`, and the sandbox `…/simulate` routes |

On every other write, such as correcting a recipient, pricing a payout or
pausing a webhook endpoint, the header is optional; without it nothing is
stored for replay. Dashboard sessions don't have to send one, except on the
checkout refund route, which requires it from every caller.

## Keys

A key is 1–255 characters from `[A-Za-z0-9_.:-]`. Use a UUID v4, one per
logical operation (one payout, one recipient), and persist it before you send.
A retry repeats the method, path, body and key. Keys are scoped to your
organization, not to the API key that sent them: the same key sent by another
of your API keys (in the same mode) replays or conflicts with the first
request. A completed record is
kept for seven days, then deleted, and the same key after that runs again.

```http
POST /payments/organizations/cmsx…/payouts
x-api-key: avvio_live_…
Idempotency-Key: 8f14e45f-ea0f-4b1a-9b0e-2c1d3e4f5a6b
Content-Type: application/json

{"amount":"200.00","destinationAccountId":"sbx_acct_MXN_4471_ae66cbc5"}
```

Money-moving routes also refuse an identical body under a different key for 15
minutes with `409` [`DUPLICATE_REQUEST_DETECTED`](/errors/#idempotency). They
are `POST …/quotes/accept`, `…/payouts`, `…/payouts/batches`,
`…/payouts/{payoutId}/funding/confirm` and the checkout
`…/payments/{paymentId}/refund`. On each of them, `X-Allow-Duplicate: true`
turns that guard off. It does not replace
`Idempotency-Key`.

> [!WARNING]
> `X-Allow-Duplicate: true` sends a second real payment. Use it only for an
> intentional, byte-identical second payment under a different key.

## Routes that ignore the header

- `POST /payout-links/{token}/submit`. The single-use link token is already the
  idempotency, and there is no caller identity to scope a key to.
- Routes that return a secret once: creating and rotating an API key, and
  creating a webhook endpoint (including the sandbox one) or rotating its
  secret. A stored replay would keep the secret for seven days, so if you lose
  the response, rotate again.
- Requests whose body is not JSON, such as multipart uploads.

## Responses

| Response | Type or header | Meaning | What to do |
|---|---|---|---|
| `400` | `IDEMPOTENCY_KEY_REQUIRED` | The header is missing on a route that requires it | Add it and retry |
| `400` | `IDEMPOTENCY_KEY_INVALID` | Not 1–255 characters of `[A-Za-z0-9_.:-]` | Use a UUID v4 |
| `409` | `IDEMPOTENCY_KEY_CONFLICT` | The key was used with a different body | Don't retry. A different request needs a new key |
| `409` | `IDEMPOTENCY_KEY_REQUEST_IN_PROGRESS` | An identical request is still running | Back off, retry with the same key |
| `500` | `PAYOUT_OUTCOME_UNKNOWN` | The network didn't answer after the send left. The payout may exist and the key is kept | Keep the key. **Do not retry with a new key.** Poll `GET /orders?reference=` or replay, and send support the `requestId` |
| `409` | `PAYOUT_OUTCOME_UNKNOWN` | A replay of that key. Nothing re-executed | As above, with `originalRequestId`; still no new key. Once resolved, the replay returns the real receipt |
| `503` | `IDEMPOTENCY_UNAVAILABLE` | We couldn't store the key. Nothing executed | Back off, retry with the same key |
| `200`/`201`/`202` | `Idempotency-Replayed: true` | The original result, with the original code: `201` on creates (`/recipients`, `/methods`, `/payout-links`, checkout `products` and `links`), `202` on `POST …/payouts/batches` and on a payout held for approval, `200` on the rest | Treat it as success. Don't re-send |

## Timeouts

> [!IMPORTANT]
> **Retry a timeout with the same key**
>
> A timeout or `500` does not prove the payment failed; the payout may have
> reached the network first. A retry under a new key is a new payment, and the
> 15-minute duplicate guard is a backstop, not a plan.

After a timeout or connection reset, retry the identical request with the same
key, or look it up with `GET /payments/organizations/{orgId}/orders?reference=`.
Never re-quote and send under a new key.

When our upstream times out you get `500 PAYOUT_OUTCOME_UNKNOWN`, and replays
answer `409 PAYOUT_OUTCOME_UNKNOWN` until we confirm the outcome with the
network. A plain `500 INTERNAL` is different: nothing was recorded, the key was
released, and retrying with it is safe.

## Key release versus terminal failures

- A `4xx` validation failure releases the key. Fix the payload and reuse it.
- A payout the rail refuses at once with a `4xx` (`PROVIDER_REJECTED`) releases
  the key like any other `4xx`. A payout that is accepted and then fails later
  (invalid bank routing, for example) is a terminal attempt: a new attempt needs
  a new key, and it is a second payment, so send it only once the first shows
  `fundsReturned: true` ([Track status & failures](/status/#failure-codes)).
- A `PAYOUT_OUTCOME_UNKNOWN` key is held until support resolves it and is exempt
  from the seven-day sweep, so it never quietly becomes re-executable.
- A request that dies mid-flight holds its key in progress for about two
  minutes. Then a money key becomes `PAYOUT_OUTCOME_UNKNOWN`, and a key that
  moves no money (creating a recipient) is released so your retry runs.
