If your checkout can be retried, refreshed, replayed by a flaky network, or re-fired by payment providers (it can), then double-charge risk is an engineering problem — not a “customer support” problem. The fix is a combined pattern: idempotent command processing + webhook reconciliation with strict state transitions.
Why Double-Charges Happen in Real Systems
Payments are distributed workflows: client, backend, gateway, fraud checks, async webhooks, queues. Failures are normal — and retries are guaranteed. If you treat a retry as “new purchase”, you create duplicates.
- Client retry on spinner / poor network (especially mobile).
- Backend retry on transient gateway errors or timeouts.
- Webhook replay (providers retry until acknowledged).
- Race conditions between redirect success and webhook confirmation.
Idempotency Keys: The Non-Negotiable Primitive
Every “Confirm Checkout” command should carry an idempotency key. The server persists it with a unique constraint and returns the stored response on duplicates.
// Pseudocode: idempotent confirm
BEGIN TRANSACTION
existing = SELECT * FROM idempotency WHERE key=? FOR UPDATE
IF existing: RETURN existing.response
intent = INSERT INTO payment_intents(status='PENDING', ...)
INSERT INTO idempotency(key, responseRef=intent.id)
COMMIT
provider = Gateway.CreateOrConfirm(intent)
UPDATE payment_intents SET status=provider.status WHERE id=intent.id
UPDATE idempotency SET response=provider.summary WHERE key=?
Webhook Reconciliation: Make the Truth Deterministic
Webhooks arrive late, duplicated, and out of order. Your handler must: verify signature, store raw event (unique by event_id), map to internal intent/order, then apply a strict state machine.
- Verify signature + enforce replay protection when supported.
- Store raw event (audit, debugging), unique on provider event id.
- State machine: only forward transitions (PENDING → AUTHORIZED → CAPTURED).
- Compensate: detect duplicates and void/cancel the extra authorization/capture.
Stop Using “paid=true”
A boolean can’t represent authorization, capture, partial refunds, chargebacks, or disputes. Model explicit states with timestamps and provider references.
PENDING → AUTHORIZED → CAPTURED
FAILED / CANCELED
REFUNDED / CHARGEBACK
Security Controls Most Teams Miss
- Rate-limit checkout confirm per user/device/session.
- Bind idempotency key to cart hash to prevent key reuse across baskets.
- Harden webhook endpoint with signature validation and allowlists where possible.
- Correlation IDs across client ↔ backend ↔ provider for forensics.
Why This Ranks & Converts
“double charged”, “duplicate payment”, “webhook retries”, “idempotency key” are high-intent keywords. This topic shows your company builds stable payment systems — a direct revenue protection promise.