# Idempotency

The bank ramp endpoints that create something take an `Idempotency-Key` header. Everywhere else, know what repeating a call does before you retry it.

## Overview

Most endpoints don't detect duplicate requests: a repeated `POST` is processed again. Two mechanisms make a retry safe: the optional `Idempotency-Key` header of the bank ramp endpoints that create a KYC session, a virtual IBAN, a payout, a payout quote or a payout paid from the user's wallet, and the `idempotencyKey` body field of [agent payment executions](https://docs.aureahub.com/docs/agentic-api.md).

## Idempotency-Key on Bank Ramp Requests

[Onboarding Session](https://docs.aureahub.com/docs/payin-session.md), [Initiate Deposit](https://docs.aureahub.com/docs/payin-initiate.md) (and its alias `/initiate-deposit`), [Initiate Payout](https://docs.aureahub.com/docs/payout-initiate.md), [Create a Payout Quote](https://docs.aureahub.com/docs/payout-quote.md) and [Pay Out a Quote from Your Wallet](https://docs.aureahub.com/docs/payout-wallet-pay.md) accept an optional `Idempotency-Key` header of up to 255 letters, digits, `-` or `_`. Make one key per operation — a UUID works — store it with the pending action, and send the same key with the same body on every retry. Every response of these endpoints carries `idempotency-replayed`: `true` when it is a stored answer, `false` otherwise.

| Request | Answer |
| --- | --- |
| **No header** | Behaves as it always has: every request runs. |
| **A new key** | Runs, and Aurea keeps the answer when the request succeeds. |
| **The same key and the same body, after a success** | The stored answer — same status, same body — with `idempotency-replayed: true`. Nothing is created again and the bank ramp is not called. |
| **The same key and a different body** | `409` with `details.code` `IDEMPOTENCY_KEY_REUSED`. Nothing runs. |
| **The same key while the first request is still running** | `409` `IDEMPOTENCY_IN_PROGRESS`. Wait a moment and send it again. |
| **The same key and the same body, after a refusal or a failure** | Runs again: only a successful answer is kept. A payout retried this way sends the bank ramp the request of the first attempt again, so the bank ramp does not see a second payout. |
| **A key with other characters, or longer than 255** | `400` `IDEMPOTENCY_KEY_INVALID`. Nothing runs. |

A key belongs to one user and one endpoint: the same key sent by another user, or to another endpoint, is a different key. Two bodies are the same when they have the same fields and values, in any order.

```javascript
// Create the key once, keep it with the pending payout, and send it again on every retry
const idempotencyKey = crypto.randomUUID();

async function createPayout() {
  const res = await fetch('https://api.aureahub.com/v1/ramp/bank/payout/initiate', {
    method: 'POST',
    headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey },
    body: JSON.stringify(payoutRequest) // the same body on every retry
  });
  if (res.status === 409) return retryLater(); // IDEMPOTENCY_IN_PROGRESS, or the key was used for another body
  return res.json(); // the first answer, even when this is a retry
}
```

## What Happens on a Retry

| Call | Repeating it |
| --- | --- |
| GET requests | Safe — they don't change anything. |
| POST /v1/transactions/ (custodial wallet) | Sends a second transaction. |
| POST /v1/swap/execute | Sends, or prepares, a second swap. |
| POST /v1/ramp/bank/payout/initiate | Without `Idempotency-Key`, creates a second payout session. With the key of the first request, answers with the first payout. |
| POST /v1/ramp/bank/payin/onboard-session | Without a key, returns the user's unexpired session or creates a new one. With the key of the first request, answers with the first answer. |
| POST /v1/ramp/bank/payin/initiate | Without a key, assigns the virtual IBAN again; when the bank ramp returns the same payment method, Aurea updates the existing record. With the key of the first request, answers with the first answer. |
| POST /v1/ramp/bank/payin/checkout | Card top-up is switched off: every request answers `403`. |
| Broadcasting a prepared transaction | A prepared transaction can be broadcast once; another attempt is rejected with `409` `PREPARED_TX_ALREADY_USED`. |
| Tenant-signed requests | Each signature is accepted once, so a retry has to be signed again — see [Authentication](https://docs.aureahub.com/docs/authentication.md). |

## Agent Payment Executions

`POST /v1/agent-payments/executions` accepts an optional `idempotencyKey` (up to 255 characters). If your tenant already has an execution with that key, Aurea returns that execution unchanged instead of running the payment again. It doesn't compare the rest of the request, and it doesn't return an error.

```javascript
// Create the key once, store it with the pending payment, and reuse it on every retry
const idempotencyKey = crypto.randomUUID();

const res = await fetch('https://api.aureahub.com/v1/agent-payments/executions', {
  method: 'POST',
  headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ ...executionRequest, idempotencyKey })
});
```

## Retrying Safely

- If a request that moves money times out or the connection drops, don't resend it blindly. First check whether it went through — for example in [List Transactions](https://docs.aureahub.com/docs/tx-list.md), [List Payout Transactions](https://docs.aureahub.com/docs/payout-list.md) or the [Unified Feed](https://docs.aureahub.com/docs/agg-tx-list.md).
- Prevent double submission in your UI (disable the button until the response arrives).
- On `429`, wait for `retry-after` before retrying — see [Rate Limiting](https://docs.aureahub.com/docs/rate-limiting.md).

---

Web version: https://docs.aureahub.com/#idempotency
