Stripe webhook: five rules that stop paid orders from getting lost
A Stripe webhook is where a payment becomes an order, and where orders get lost. Five rules keep it safe: the server computes the price, the signature is verified on the raw body, the record is idempotent on the session id, the amount is re-checked inside the same transaction, and a second path reconciles payments whose webhook never arrived. Then you check the live site from the outside.
1. The server computes the price
The browser sends what the buyer wants (a product id, a slot), never the amount. The server computes the price from its own data and creates the Checkout Session. The buyer pays on Stripe's page, so no card data touches your site.
2. Verify the signature on the raw body
Stripe signs every event. Verify it with the endpoint's signing secret on the raw request body, before any JSON parsing (Stripe docs). The two most common mistakes: the wrong secret (the CLI's instead of the dashboard endpoint's) and a body already parsed by the framework. A webhook that accepts unsigned calls lets anyone mark an order as paid.
3. Record once, keyed on the session id
Stripe may deliver the same event more than once. Store the payment with a unique key on the session id: the second delivery finds the row and does nothing. No duplicates, no double delivery.
4. Re-check inside the transaction
If the price can change (an auction, limited stock), re-validate the paid amount against the current price inside the same database transaction, with a lock on the item. A late webhook must not overwrite a higher payment that arrived earlier.
5. Reconcile what the webhook missed
Webhooks fail: a wrong secret, a deploy at the wrong moment, a timeout. Without a second path, a customer pays and gets nothing, and nobody notices. Add one: the success page re-reads the session from Stripe's API and records it if missing, or a periodic job lists the last day's paid sessions and fills the gaps. In a real review of a live payments site, the first four rules were solid and the fifth was missing: it was written down as an open gap, not hidden.
What does a Stripe integration cost?
According to projectcostestimator (June 2026): $500-1,500 for a basic Checkout (1-2 weeks), $2,500-8,000 for embedded payments in a SaaS, $7,000-15,000 for subscriptions with renewals and dunning.
What a Stripe integration costs
Then check from the outside
The code says what should happen; the live site says what happens. A POST to your webhook without a signature must be refused (400), and the site should send basic security headers. On 8 October I checked my three sites: the payments webhook refused unsigned calls as it should, but all three were missing X-Content-Type-Options and Referrer-Policy, and on the payments site those headers had been written in the code a month earlier and never deployed.
| Site | What the outside check found |
|---|---|
| my personal site | HTTPS and HSTS in place, share tags complete with a reachable image, robots and sitemap; two security headers missing; pages in 0.7-1.1 s |
| a second site | the same two headers missing; a share card generated on the fly that answers 200 as an image |
| the payments site | the same two headers missing, no robots or sitemap, home page in 3.5 s; the webhook refuses an unsigned POST (400) |
Frequently asked questions
- Why does my Stripe webhook signature fail?
- Usually the wrong signing secret (the Stripe CLI's instead of the dashboard endpoint's) or a body already parsed as JSON: the signature must be verified on the raw body.
- Can Stripe send the same webhook twice?
- Yes. Record payments with a unique key on the Checkout Session id, so a second delivery changes nothing.
- What if a webhook never arrives?
- Add reconciliation: the success page re-reads the session from Stripe's API, or a periodic job records paid sessions that are missing.
- How do I test it before real money?
- In test mode end to end: pay with Stripe's test card, resend the same event, send an unsigned POST, tamper the amount in the browser, block the webhook once and check reconciliation recovers it.
- How do I check my site from the outside?
- Request the live pages as a visitor would: HTTPS, security headers, share tags and their image, robots and sitemap, load time, and an unsigned POST to the webhook, which must be refused.
Where to start
The free guide, the kit for your AI agent and the service are on the Hard Parts of a Website page.

And a real question: which part of your site are you most afraid to touch? Tell me.
Videos, automations and a second brain for people who work with Claude, built in public on lucasgrilli.com and GitHub.