June 13, 2026 · 3 min read

Idempotent payment webhooks: never double-charge on a retry

  • payments
  • stripe
  • webhooks
  • idempotency
  • reconciliation

Anything that touches money is unforgiving. A dropped event and an order never ships; a duplicated one and a customer is charged twice. Both turn into support tickets, refunds, and lost trust — fast. And the part most likely to bite you is the one that looks simplest: the webhook endpoint.

Here's how I build payment webhooks that handle money correctly under real conditions.

Webhooks are at-least-once, not exactly-once

The mental model that fixes most payment bugs: a webhook can arrive more than once, and the order isn't guaranteed. Stripe retries until it gets a 2xx. A network hiccup can duplicate a delivery. A retry can fire for an event that actually succeeded the first time. If your handler assumes "this runs exactly once," it's already wrong.

So design for duplicates from the start, rather than hoping they don't happen.

Verify the signature first

Before anything else, verify the provider's signature on the raw request body. An unverified payment webhook is an open door — anyone who finds the URL could forge "payment succeeded." Reject anything that doesn't validate, and only then start processing.

Idempotency: process each event once

Every Stripe event has a unique id. Use it:

  • On receipt, try to record that event id as "seen."
  • If it's already recorded, you've processed it — acknowledge with a 2xx and stop.
  • If it's new, do the work and persist the id in the same transaction, so a crash can't mark it done without doing it (or do it without marking it).

Now a retried or duplicated event is a no-op. This is exactly the discipline behind the payment, commerce, and accounting integrations on Trove Gifting — Stripe alongside Shopify and Xero — where a double-processed event would mean a double charge or books that don't add up.

Acknowledge fast, do slow work in the background

Return 2xx quickly so the provider stops retrying, then hand any slow follow-up (emails, syncing to other systems) to a background job. A webhook handler that does heavy work inline eventually times out — and a timeout looks like failure, so the provider retries, and now you're fighting yourself.

Reconcile, because events still go missing

Even done well, webhooks aren't a complete source of truth — an event can be missed entirely. So the last piece is reconciliation: periodically compare your records against the provider's API and flag anything that disagrees. It's the safety net that catches the gap between "we think we got paid" and "we actually got paid," before it reaches your accounting.

The payoff

Verify the signature, make every handler idempotent, acknowledge fast and defer the slow work, and reconcile on a schedule. Do that and money moves correctly even when the network doesn't cooperate — no double charges, no silent drift.

That's the standard I hold payment and revenue integrations to, because anything less shows up in someone's bank statement.

Wiring up Stripe, a store, and your books? Start a project brief and I'll help you make the money flow reliable.

FAQ

Why do payment webhooks arrive more than once?
By design. Providers like Stripe retry until they get a 2xx response, and networks can duplicate a delivery. Your endpoint has to assume every event may arrive multiple times.
How do you make a webhook handler idempotent?
Record each event's id the first time you see it, in the same transaction as the work it triggers. If you've already processed that id, acknowledge and stop — so a duplicate has no extra effect.
What is reconciliation, and why does it matter for payments?
Reconciliation is confirming that your store, your payment provider, and your accounting all agree on what happened. Without it, a dropped or duplicated event drifts your numbers silently until it surfaces in support tickets or the books.

Written by

Arvin Kent Lazaga

AI-Native Adaptive Full-Stack Software Engineer · Remote from the Philippines. I build production web, mobile, and backend systems across different stacks using Claude Code, OpenAI Codex, and disciplined planning, review, and testing.

Need a production-grade backend, integration, or automation system?

Let's turn the workflow into reliable software.