> ## Documentation Index
> Fetch the complete documentation index at: https://docs.atum.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Idempotency

> Retry a write safely: reuse a request_id and the gateway won't act twice.

Every write endpoint is idempotent. Reuse the same `request_id` on a retry and the gateway returns that payment or quote-request with its **current** status. Using the same `request_id` prevents making a second payment or opening a second quote-request batch.

This page is the API-reference summary. For the concepts behind it — the two guarantees, per-integration usage (SDK, CLI, x402, MPP), and deliver-once guidance on the acceptance side — see the [Idempotency guide](/payment-protocols/idempotency).

<Note>
  Unlike header-based schemes, the key is a field **in the request body**, not an HTTP header, and it is **not** required to be a UUID. See [`request_id`](#the-key-request-id) below.
</Note>

## The key: `request_id`

|              |                                                                                                                                                                                                                                                |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Location** | `request_id` field in the JSON request body                                                                                                                                                                                                    |
| **Scope**    | Per originator — a `request_id` only needs to be unique among your own requests.                                                                                                                                                               |
| **Format**   | Letters, digits, underscore (`_`), hyphen (`-`), and dot (`.`). No UUID requirement.                                                                                                                                                           |
| **Examples** | `req_123456789`, `order-8421`, `qr-2026-04-29-001`                                                                                                                                                                                             |
| **Lifetime** | Retained at least while the payment is in flight. After `failed`, mint a new `request_id` for a genuine re-attempt. After `completed`, you may still replay the same key to recover the original payment. Do not rely on indefinite retention. |

You choose the value. A stable, unique-per-operation string works best (an order id, or a UUID if you already mint them).

Store it **before** you send the request — it's the only thing that links a retry to its original.

## Supported endpoints

Idempotency applies to state-changing (`POST`) endpoints. Each has its own key: reusing one across the two does not carry over.

| Endpoint                                                                                  | Key                                         | A repeat returns                                                  |
| ----------------------------------------------------------------------------------------- | ------------------------------------------- | ----------------------------------------------------------------- |
| [`POST /v1/payments`](/api-reference/payment-gateway/submit-payment)                      | `request_id`                                | the original payment — same `payment_id`, with its current status |
| [`POST /v1/payments/quote-requests`](/api-reference/payment-gateway/create-quote-request) | `request_id` (independent of the payment's) | the original batch — same `quote_request_id` and its quotes       |

`GET` reads (`/status`, `/resolve`, `/timeline`) have no side effects, so there is nothing to deduplicate.

## Detecting a replay

Every write response carries `idempotent_replay` (boolean, always present):

* `true` — you were handed a resource that already existed; nothing new was created or charged.
* `false` — this request created it.

## Reusing a key with different terms

A `request_id` locks in one set of economics — **who pays, who receives, in which assets, and how much** (`source`, `destination`, `fulfillment_amount`, `max_source_amount`). Reuse it with any of those changed and the gateway rejects the request rather than merging the change or silently returning the original.

**Exempt** (you may change these freely on a retry):

* **Deadlines** — `quote_deadline` and `fulfillment_deadline`. An expired deadline on a retry is not an error; the request resolves onto what already exists.
* **The signature** — re-sign, or resend the original bytes. Only `request_id` must stay fixed.

## Errors

| HTTP  | Code                                                               | When                                                  | What to do                                       |
| ----- | ------------------------------------------------------------------ | ----------------------------------------------------- | ------------------------------------------------ |
| `409` | [`IDEMPOTENCY_TERMS_MISMATCH`](/errors#idempotency-terms-mismatch) | Same `request_id`, different economics                | Use a new `request_id` for the different request |
| `409` | [`REQUEST_ID_TAKEN`](/errors#request-id-taken)                     | The `request_id` is already in use by another account | Use a new `request_id`                           |
| `400` | [`INVALID_REQUEST_ID`](/errors#invalid-request-id)                 | `request_id` contains disallowed characters           | Fix the format                                   |
| `503` | —                                                                  | Nothing was accepted (a dependency was down)          | Safe to retry the **same** `request_id`          |

## Retrying by status

A safe retry depends on the payment's `status`:

| `status`                 | What to do                                                         |
| ------------------------ | ------------------------------------------------------------------ |
| `completed`              | Nothing — settled.                                                 |
| `pending` / `finalizing` | Retry the **same** `request_id`, or poll `/status`. Not a failure. |
| `failed`                 | Terminal. A fresh attempt needs a **new** `request_id`.            |

Note that `pending` and `failed` call for **opposite actions**, so make sure to preserve the mental model:

* **`pending`** — still settling, so reuse the **same** `request_id` (or just poll). Minting a *new* key here starts a **second** payment: a double charge.
* **`failed`** — terminal. Reusing its `request_id` only hands you back the same failed payment, so a genuine re-attempt needs a **new** `request_id`.

## Example

```bash theme={null}
curl https://payment-gw.production-testnet.atum.xyz/v1/payments \
  -H "Content-Type: application/json" \
  -d '{
    "request_id": "order-8421",
    "source": { "...": "..." },
    "destination": { "...": "..." },
    "fulfillment_amount": "10.00",
    "sender_auth": { "...": "..." }
  }'
```

The request authorizes itself through its `sender_auth` signature — there is no API key. Re-send the exact same request, with the same `request_id`, to recover the original result after a timeout or crash; the response's `idempotent_replay` tells you whether it was created now or already existed.

<Tip>
  Using the SDK? You still pass `requestId` — the SDK derives the on-chain deposit nonce from it so both guarantees line up. The CLI generates a `request_id` if you omit `--request-id`; copy that value to retry.
</Tip>
