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 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_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
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.
Reading the outcome (payments)
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
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 If the acceptance side reports
paymentIdentifier option, which becomes the request_id: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.Direct payments (MPP)
Direct payments (MPP)
The acceptance side stamps the id into the payment challenge, usually from a value the route already has:
Checking on a pending payment
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.
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.
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. A failed payment keeps its key forever — recovering means a new key, the opposite of the
pendingcase.