Idempotency in payments: why a double click must not charge twice
Working on the payments team I learned a lesson that sounds small but means everything: a payment operation must be repeatable without charging twice. We call that idempotency.
The problem
The user taps "Pay", the network is slow, they see no response and tap again. Or your own system retries a half-failed request. With no protection, you just charged twice. In payments that's not a bug, it's an incident.
The idea: the idempotency key
Every payment attempt carries a unique idempotency key generated by the client. The server stores the result against that key:
- If the key is new → process the payment and store the result.
- If the key already exists → return the stored result, without charging again.
Gateways like Stripe or Adyen support this out of the box with an Idempotency-Key header. But your own backend must be idempotent end to end too.
How I implement it
- Generate the key before showing the pay button, not after.
- Persist
(key → state, result)atomically: aUNIQUEdatabase constraint is your best friend. - Treat webhooks as idempotent as well: the same event can arrive several times and out of order.
- Distinguish "in progress" from "completed" so two concurrent requests with the same key aren't both processed.
What I take away
Idempotency isn't just for payments: retries, message queues, jobs… anything that can run "more than once" needs it. In payments, it simply isn't negotiable.
Building a gateway or a billing system? Reach out from the home page.