DEV Community

Payneteasy
Payneteasy

Posted on

Same idempotency key, different amount, same response

Had a retry bug where a client resent a payment request with a corrected amount but reused the idempotency key from the failed first attempt. Gateway matched on the key, found a cached response, and returned it — original amount, original transaction ID, 200 OK. No error about the mismatch. No flag anywhere in the response body.

We assumed idempotency keys were scoped to "this exact request," so reusing one with changed parameters would either fail validation or create a new charge. Neither happened. The provider's docs did mention that keys are matched independently of payload, but that line was three paragraphs below the retry example, and nobody reads past the retry example.

Took us a day to find because the symptom wasn't a failed payment, it was a correct-looking payment for the wrong amount. Our reconciliation job flagged a mismatch between order total and charge total, and from there it was logs-first debugging to figure out why a request with amount=4200 returned a response for amount=3800.

Fix was boring: generate a new key whenever any field in the request changes, not just on network-level retries. But the bigger question is why idempotency scoping isn't more consistently payload-aware across providers — some hash the body into the key check, some don't.

Anyone built a wrapper that enforces key-to-payload binding client-side instead of trusting the gateway's matching logic?

Top comments (5)

Collapse
 
muhammad_turnergane_7ddc profile image
Muhammad Turner Gane •

stripe is one of the ones that actually compares. reuse a key with different params and you get an idempotency error instead of the cached response. so yeah it varies by gateway - good reason to do the check on your side either way.

simplest version i've seen: derive the key from the request (hash of order id + amount + currency). corrected amount → new key. real network retry → same key. just be sure the first attempt actually failed before you send the corrected one, or you've built a clean double-charge path.

Collapse
 
payneteasy profile image
Payneteasy •

Agreed, and Stripe’s behavior is the right default: same key with different params should be an error, not a cached response. Since not every gateway checks this, we check it on our side regardless.

One catch with deriving the key from order id + amount + currency: the key changes whenever the params change. Your idempotency layer can then no longer see “same operation, different amount”, and the corrected request looks completely new. That’s exactly the double-charge path you describe, so “make sure the first attempt failed” can’t depend on the caller remembering to check. It has to be enforced:

One open or successful charge per order. A lock at the order level, separate from the idempotency key. A new attempt for the same order is refused while the previous one is pending or complete.
A correction is an explicit operation. Either void/refund the first charge, or confirm it failed via a status inquiry to the processor. Only then is the order unlocked for a new amount.
The idempotency key then protects against network retries only, which is all it’s good at anyway.

So it’s two layers: the key handles “did I already send this?”, and the order lock handles “should this order be charged again at all?”. Mixing the two is how the 3800 vs 4200 case got through.

Collapse
 
muhammad_turnergane_7ddc profile image
Muhammad Turner Gane •

yeah the order lock is the real guard. the key only says "same request again"; the lock says this order already has a charge in flight or done. without that, a corrected amount is just a new key and you're back to trusting the caller.

Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more