Idempotency is not optional in payments
A double-tap should never become a double-charge. How Collect makes payment endpoints safe to retry — by design, not by luck.
The scariest bug in a payments system isn't a crash. It's a double charge — the one that happens when a client taps "Pay" twice, or a network blip triggers a retry, and your server dutifully runs the charge a second time. Crashes get noticed. Double charges get disputed.
The contract
Every payment-creating endpoint in Collect requires an Idempotency-Key. The rules are spelled out, not improvised:
- Same key + same request + completed → return the stored response, no second charge.
- Same key + same request + still in flight → 425 Too Early; the client waits, the user sees nothing.
- Same key + a different request body → 409 Conflict; that's real misuse, surfaced to logs.
The keys are scoped per business, so two merchants can never collide.
Why first-writer-wins matters
Two concurrent requests with the same key race to insert a row. A unique index on (businessId, key) means exactly one wins; the loser reads the in-flight response and converges instead of charging again.
const existing = await idempotency.claim({ businessId, key, requestHash })
if (existing.status === "completed") return existing.response
if (existing.status === "in_flight") throw new TooEarly()
// we won the race — safe to call the processor exactly onceThe boring guarantee is the product
None of this is glamorous. Nobody buys software because the idempotency contract is airtight. But the absence of it is the kind of thing that ends trust in a payments product overnight.
That's why it's built in from the first commit — typed, validated, and tested — not bolted on after the first incident.