Skip to main content
You resent a request with the same idempotency key — either on purpose, as a retry, or by accident — and want to know what comes back. Looking for what to do about an actual double charge instead? See Handle duplicate payments.
TL;DR — Same key, same body → the original result, whatever its status (201/200, even PENDING). Same key, different body → 409. Which 409 depends on the key: request_id compares the body; operation_id never does.

When this happens

  • A byte-identical replay: the same request_id (purchase, authorize, refund, standalone refund) or operation_id (capture, void) with the same body. This is the safe, expected retry path after a timeout. Balance inquiry also takes a request_id field, but it’s not deduplicated — see the note below.
  • A body-mismatch replay: the same request_id with a different body — 409 urn:radiumone:transaction:idempotency-body-mismatch.
  • A cross-operation replay: the same request_id first used for one operation type (say, a purchase) and then sent to a different operation (say, a referenced refund) — also 409 urn:radiumone:transaction:idempotency-body-mismatch. A request_id is scoped to the operation type it was first used for.
  • A conflicting operation replay: the same operation_id reused for a different operation type on the same transaction (for example, a capture’s key reused for a void) — 409 urn:radiumone:tx:duplicate-operation.

How the gateway decides

  1. A request arrives carrying a request_id or operation_id.
  2. If your merchant account hasn’t used that key before, the gateway creates a new transaction.
  3. If the key was used before and it’s a request_id (purchase, authorize, refund, standalone refund): the gateway compares amount, card, channel, metadata, and order_reference against the first attempt, and confirms the key was used for the same operation type both times. It does not compare three_ds or loyalty — changing either on a retry replays the original silently instead of failing.
    • Match, and the first attempt already finished → the original result, at the original HTTP status — whatever the status, including DECLINED or FAILED.
    • Match, and the first attempt is still processing → the existing transaction comes back, usually PENDING, with its id — not a new transaction. Poll status or wait for the webhook.
    • Mismatch on any of the compared fields, or the same key reused for a different operation type → 409 idempotency-body-mismatch.
  4. If the key was used before and it’s an operation_id (capture, void): the gateway only checks whether it’s the same operation type on the same transaction — the body is never compared.
    • Same operation type → the original result, at the original HTTP status, regardless of what you sent this time.
    • A different operation type reusing the key → 409 tx:duplicate-operation.
Referenced refunds match a narrower set of fields: the same original transaction, the same amount, and a key already used for a refund — reason isn’t compared. The replay check runs before the batch/window/cap gates, so a retry always replays (never 422), even if another refund changed the refundable amount in between. Because a DECLINED or FAILED refund replays too, retry after a decline with a new request_id, not the same one. A wrong transaction_id in the path returns 404 before the replay check runs.
Balance inquiry also takes a request_id field, but it isn’t an idempotency key — there’s no dedup or replay store, so every call re-queries the rewards host, even with the same request_id.

What you see

What to do

1

Resend byte-identical for a genuine retry

If you’re retrying after a timeout or a 5xx, resend the exact stored body with the exact same key (API reference):
The response is the original result, at the original HTTP status — this is always safe to do, any number of times.
2

Fix the body if you get a mismatch

A 409 idempotency-body-mismatch means a request_id was reused with a body that doesn’t match the first attempt. Don’t retry as-is — resend the original body, or mint a new request_id if this is genuinely a different attempt (a different card, a different amount).
3

Use one key per attempt, and one per operation

Generate one request_id per order attempt and one operation_id per operation (a capture and a later void on the same transaction are two different operations — they each need their own key). Reusing an old key for a different attempt or a different operation type is what causes a conflict in the first place.

Prevent it

  • Generate the idempotency key once per attempt, before you send the first request, and reuse that exact value for every retry of that same attempt.
  • Keep a local record of which key you used for which order attempt (and which operation), so a retry — even after a restart — reuses the right one.
  • Never derive a key from mutable data (a timestamp, a request counter) that changes between retries — that guarantees a mismatch instead of a safe replay.

Test it

See Test your integration and Body mismatch for scenario coverage.

Prevent duplicate payments

The cross-product guide to avoiding duplicate charges.

Charge or authorize a payment

Idempotency rules for purchase and authorize.

Capture an authorization

Idempotency on operation_id.

Problem format and retries

The full retry-rules reference.
Last modified on September 15, 2026