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

> How Atum deduplicates retries so a payment or quote request is never created twice.

Send the same write request twice — during a retry, a timeout, or a crash — and it should count once, returning the same answer both times.

For a payment, that means you are charged once. The key that makes this work is `request_id`.

## When you need it

| Operation                             | Idempotency key                                                               |
| ------------------------------------- | ----------------------------------------------------------------------------- |
| Submit a payment (`submitPayment`)    | `request_id` on the payment request                                           |
| Request quotes (`createQuoteRequest`) | `request_id` on the quote request — independent of the payment's `request_id` |

These are two separate keys, one per operation. Reusing one does not carry over to the other. Reads (`/status`, `/timeline`) have no side effects, so there's nothing to deduplicate there.

## The two guarantees

### 1. Gateway deduplication — every write endpoint, every caller

The gateway records your `request_id`, scoped to your source account and signing identity.

Send the same one again and you get back what it created the first time; nothing new is created. This holds for SDK and direct API callers alike, as long as the `request_id` is stable across the retry.

### 2. On-chain single settlement — payments only, SDK-managed

Where funds move (`submitPayment`), the chain adds a second layer on top of the guarantee above. Each deposit authorization carries a nonce, and a given nonce can settle at most once.

The SDK derives that nonce from your `request_id` automatically, so a retry reuses it without any work on your part. Should you sign your own nonce on each attempt instead, on-chain single settlement is no longer guaranteed.

This second layer lasts a different amount of time depending on the chain:

* **EVM / Tron** — permanent. A spent nonce can never be reused.
* **Solana** — about two minutes. After that, there's no on-chain protection left — the gateway's own record of your `request_id` is what stops a duplicate.

## Keys

Three types of identifiers:

| Identifier          | What it is                                                                                                                                                                                                                                                |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `request_id`        | Your idempotency key. Required when you call `preparePaymentRequest` or `prepareQuoteRequest` directly.                                                                                                                                                   |
| Purchase identifier | The id you supply instead when paying over x402 or MPP — `paymentIdentifier` for x402, `intentId` for MPP. The SDK turns it into `request_id` for you.                                                                                                    |
| `payment_id`        | Atum's id for the payment, returned as soon as the gateway accepts it — settled or not. Not a key you send; it's what you get back, and what you poll `/status` and key delivery on (see [If you accept payments](#if-you-accept-payments-deliver-once)). |

<Warning>
  **Save the key before you send the request.** It's the only thing that connects a retry to its original. If your process restarts and can't recall the key it used, it starts a second payment.

  It must also not outlive the purchase — a key left set in config re-attempts the same purchase forever instead of starting the next one. The [examples](https://github.com/Atum-Labs/examples) mint a fresh id per run and print it back so you can re-attempt deliberately: `PURCHASE_ID=<id> npm run pay`.
</Warning>

## Reusing a key with different terms is rejected

A `request_id` locks in one payment (or one quote-request batch). Reuse it with different economics — the **accounts, assets, or amounts** — and the gateway won't merge the change, and it won't quietly hand back the original either. It answers `409 IDEMPOTENCY_TERMS_MISMATCH` instead.

That covers who pays, in what, up to how much, who receives, in what, and how much they receive. The source spend cap (`max_source_amount`) is included: it is the amount your signature *authorizes*, not just your own risk limit, so a retry carrying a different cap is a different payment.

**Two things are exempt, though — you can freely change these on a retry:**

* **Deadlines.** `quote_deadline` and `fulfillment_deadline` are short-lived on purpose, so they've often expired by the time you retry. That's fine — an expired deadline on a retry isn't an error, it just resolves onto the request that already exists. On a quote request, note that re-asking with a *later* `quote_deadline` does not extend a window already open: the window belongs to the batch, so the response reports the batch's own deadline rather than the one you just sent.
* **The signature.** Re-sign, or resend the original signature — either works. Just keep the `request_id` itself unchanged.

<Note>
  Accepting payments over x402 or MPP? The same rule binds you: quote the **same amount and the same accounts** every time you re-issue a challenge for one purchase identifier. Re-price it and the payer's next attempt is refused with a `409` instead of resuming, and they have no way to act on it.
</Note>

## Telling a retry from a first attempt

Payment and quote-request responses include `idempotent_replay`: `true` means you got something that already existed, `false` means this request just created it.

Check it whenever you need to tell "this just happened" apart from "this was already there," which can be useful for logging, reconciliation, or avoiding double counts.

## Reading the outcome (payments)

| `status`     | What it means                                                                                                                        | What to do                                                                           |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
| `completed`  | Settled.                                                                                                                             | Nothing — `fulfillment_confirmation` carries the proof.                              |
| `pending`    | Accepted, still settling.                                                                                                            | Retry with the **same** `request_id`, or poll `/status`. Do not start a new payment. |
| `finalizing` | The transaction paying the recipient is on chain and gathering the confirmations its chain requires. Made, but not yet irreversible. | Treat it exactly as `pending`. Keep the **same** `request_id` and keep polling.      |
| `failed`     | Terminal. This payment can never complete.                                                                                           | A fresh attempt needs a **new** `request_id`.                                        |

`completed` and `failed` are the only statuses that end a payment. `finalizing` is published so that a client handles it before the gateway starts sending it. No service returns it today, so treating it as unreachable is safe, but a caller that branches exhaustively on `status` should add it now rather than discover it later.

Getting `pending` and a terminal failure the wrong way round is the whole risk here: reusing a key after a terminal failure just returns the same dead payment forever; minting a new key while a payment is still `pending` is what causes a double charge.

## Using it per integration path

<AccordionGroup>
  <Accordion title="Payment Gateway SDK / direct API">
    `requestId` is a required parameter on `preparePaymentRequest` / `prepareQuoteRequest`. Generate it once per logical payment, store it, and pass the same value on every retry.

    ```ts theme={null}
    const requestId = `order-${orderId}`;
    const paymentRequest = await client.preparePaymentRequest({ requestId /* ...*/ });
    ```
  </Accordion>

  <Accordion title="Payment Gateway CLI">
    `send-payment` and `request-quote` accept `--request-id`. Omit it and the CLI generates one and prints it back — copy that value to retry the same payment. A bare re-run with no flag is a **second** payment.
  </Accordion>

  <Accordion title="Facilitated payments (x402)">
    The payer supplies a purchase identifier via the `paymentIdentifier` option, which becomes the `request_id`:

    ```ts theme={null}
    await pay(MERCHANT_URL, {}, { paymentIdentifier: purchaseId });
    ```

    If the acceptance side reports `settlement_pending`, **your code** has to re-attempt the same purchase (fresh `402`, re-signed credential) until settlement reaches a terminal outcome — the wrapped `fetch` does not do this for you.
  </Accordion>

  <Accordion title="Direct payments (MPP)">
    The acceptance side stamps the id into the payment challenge, usually from a value the route already has:

    ```ts theme={null}
    buildChargeChallenge(corridor, source, amount, { intentId: purchaseId });
    ```
  </Accordion>
</AccordionGroup>

## Checking on a pending payment

Any of these work, and you can mix them:

* **Poll `/status`** with the `payment_id` — cheapest once you hold one.
* **[Resolve the `request_id`](/api-reference/payment-gateway/resolve-payment)** — `GET /v1/payments/resolve?request_id=<id>` returns the `payment_id`, for when you lost the submit response but kept your key.
* **Re-submit the same `request_id`** — idempotent, and it returns the payment's current state. Heaviest of the three, since it needs the full signed request again.

<Tip>
  If you're a merchant mediating someone else's HTTP request (x402/MPP), prefer having the payer re-attempt over polling inside that request. Polling holds the connection open for the whole settlement window; the payer's own retry doesn't.
</Tip>

## If you accept payments: deliver once

The idempotency key stops you from being **paid** twice. It does not automatically stop you from **delivering** twice.

Always key delivery on Atum's receipt id (`payment_id`):

* First successful delivery for that receipt wins.
* If the payer-side retries and never got the success response, send the **same** result again — do not ship a second time.
* In production, that's usually a column on the payments or orders table you already use for [reconciliation](/get-started/support/reconciliation).

## Common mistakes

* **Minting a new key after a timeout or crash.** If the prior attempt might still be in flight, this creates a second payment.
* **Reusing one key across two genuine purchases.** The second resolves onto the first — you deliver twice, you're paid once.
* **Treating a `pending` payment as failed and retrying with a new key.** `pending` is not failure; retry with the *same* key or poll `/status`.
* **Retrying a terminal failure with the same key.** A failed payment keeps its key forever — recovering means a *new* key, the opposite of the `pending` case.

## See it working

| What                                        | Where                                                                                                                                                                                                                     |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Retry loop while settling                   | [x402 payer-side](https://github.com/Atum-Labs/examples/blob/main/x402-make-payments/src/purchase.ts) · [MPP payer-side](https://github.com/Atum-Labs/examples/blob/main/mpp-make-payments/src/purchase.ts)               |
| Deliver once per receipt                    | [x402 acceptance-side](https://github.com/Atum-Labs/examples/blob/main/x402-accept-payments/src/payments.ts) · [MPP acceptance-side](https://github.com/Atum-Labs/examples/blob/main/mpp-accept-payments/src/payments.ts) |
| Try the "still settling" path with no funds | `npm run test:smoke:pending` in the [examples](https://github.com/Atum-Labs/examples) repo                                                                                                                                |

## Next steps

| Topic                    | Link                                                                                                              |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------- |
| Make a payment           | [x402](/payment-protocols/x402/make-payments/overview) · [MPP](/payment-protocols/mpp/make-payments/overview)     |
| Accept payments          | [x402](/payment-protocols/x402/accept-payments/overview) · [MPP](/payment-protocols/mpp/accept-payments/overview) |
| What to record for later | [Reconciliation](/get-started/support/reconciliation)                                                             |
| Something going wrong?   | [Troubleshooting](/get-started/support/troubleshooting)                                                           |
