Payment webhooks that never double-credit: signatures, idempotency and reconciliation
By Daniel Chen · · 4 min read
Most payment bugs I have fixed were not on the checkout page. They were in the callback: the webhook that tells your system a payment succeeded. These are the rules I now apply to every integration, whether it is Checkout.com, Stripe or a local gateway.
Verify before you trust
Check the provider's signature on the raw request body before reading anything else. Reject anything that fails, and log it. A webhook endpoint is a public URL, so treat every request as untrusted until proven otherwise.
Process each event exactly once
Providers retry, and networks time out after your code has already committed, so the same event will arrive twice. Store the provider's event or payment id with a unique constraint and make the handler do nothing when it has seen that id before. A database constraint is a stronger guarantee than an if statement.
Model payments as a state machine
Pending, authorised, captured, refunded, failed. Allow only valid transitions and ignore events that would move a payment backwards. Events can arrive out of order, and a late 'pending' must never overwrite a 'captured'.
Reply fast, work later
Acknowledge the webhook quickly and do the slow work (emails, ledger entries, provisioning) after the payment state is safely stored. Long handlers cause timeouts, and timeouts cause retries.
Reconcile on a schedule
Even with all of the above, a callback can be lost. A reconciliation job that asks the provider about anything still pending turns 'we lost a payment' into 'it was fixed ten minutes later'. On a ticketing project, that job together with status polling is what kept missed callbacks from becoming lost revenue.
Test the whole loop locally
Run the full flow on your machine with a tunnel for webhooks, and write the steps down for your team. One trap worth knowing: a forwarded webhook returning 200 does not prove your handler processed it. Check the stored payment, not just the HTTP status.
Finally, keep card data with the provider through hosted fields or tokens. Your PCI scope stays small, and so does your risk.
- Payments
- Webhooks
- Checkout.com
- Stripe