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
2xxand 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.