request_id.
When you need it
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 yourrequest_id, scoped to your source account and to whoever signed the payment request, so it can’t collide with another account’s.
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_idis what stops a duplicate.
Keys
Three types of identifiers:Reusing a key with different terms is rejected
Arequest_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_deadlineandfulfillment_deadlineare 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 laterquote_deadlinedoes 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_iditself unchanged.
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.Telling a retry from a first attempt
Successful payment and quote-request responses includeidempotent_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.
Retrying by status
What each status means is on Payment status. What it means for a retry:pending,finalizingorrefunding: still in progress. Keep the samerequest_id, or poll/status. Do not start a new payment.refund_available: do not pay again. Submit the refund yourself. See Refunds.completed: nothing to do.failedorrefunded: final. Sending the samerequest_idagain returns the same payment, so a fresh attempt needs a newrequest_id. If afailedpayment hastransactions.depositset, reconcile first: see A failed payment with a deposit.
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 failed payment; minting a new key while a payment is still pending is what causes a double charge.
When a request is refused
An error tells you whether you may send the samerequest_id again. Read its retry_classification, and its request_id_reusable when present, or look the code up on Error codes. The ones about the key itself:
A
503 usually means nothing was accepted, so the same request_id is safe to send again after a delay. The exception is a SERVICE_BUSY that comes with a payment_id: the payment was recorded but could not start, so that request_id is used up and a new attempt needs a new one. request_id_reusable on the error tells you which case you have.
When a payment is still settling
The submit call waits a short time for the result (see Track a payment). A payment that takes longer is answered as pending. That is not a failure.Direct API, SDK and CLI
Any of these work, and you can mix them:- Poll
/statuswith thepayment_id— cheapest once you hold one. - Resolve the
request_id—GET /v1/payments/resolve?request_id=<id>returns thepayment_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.
x402 and MPP
The payer re-attempts the same purchase: it fetches a fresh402, signs it again, and keeps the same purchase identifier. The gateway resolves onto the original payment, so a re-attempt cannot charge twice, however many times it runs. A stored credential is not reused: it is refused once its quote window has closed.
A payment that is
refund_available is also reported as still settling, so re-attempting never ends: stop and submit the refund. How to spot it: Refunds.
Using it per integration path
Payment Gateway SDK / direct API
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.Payment Gateway CLI
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.Facilitated payments (x402)
Facilitated payments (x402)
The payer supplies a purchase identifier via the The wrapped
paymentIdentifier option, which becomes the request_id:fetch pays once. Re-attempting while the payment is still settling is your code’s job: see When a payment is still settling.Direct payments (MPP)
Direct payments (MPP)
The acceptance side stamps the id into the payment challenge, usually from a value the route already has:The payer re-attempts by requesting the same resource again: the new challenge carries the same
intentId, and the payer signs it again.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 payment id, which the receipt carries:- 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.
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
pendingpayment as failed and retrying with a new key.pendingis not failure; retry with the same key or poll/status. - Retrying a terminal failure with the same key. Resending that key returns the same failed payment — recovering means a new key, the opposite of the
pendingcase.