examples/storefront Public
id: payment-webhook
type: concept
owner: justin
component: stripe-webhook
describes:
- workers/stripe-webhook/src/**
reviewed: 2026-10-09
The payment webhook
An order is paid when Stripe says so, never when the browser says so. Stripe sends events to https://<stripe-webhook host>/stripe/webhook, and the storefront-stripe-webhook Worker turns them into order changes.
What it does
- Verifies the
stripe-signatureheader againstSTRIPE_WEBHOOK_SECRETwith Web Crypto. A bad or missing signature gets a 400 and changes nothing. - For
payment_intent.succeeded, callsOrdersService.markPaid(order, intent)on storefront-api. The order id comes from the intent's metadata, which checkout set ([[checkout]]). - For
payment_intent.canceled, callsOrdersService.cancel(order), which puts the stock back. - Answers 200 to everything else, including events for intents without an
order_id, so Stripe stops retrying them.
The Worker has no database binding. It reaches orders only through the service binding ORDERS ([[0006-one-writer-for-the-database]]).
Retries and repeats
Stripe delivers each event at least once and retries anything that does not get a 2xx. Both calls are safe to repeat:
markPaidonly moves an order frompending_paymenttopaid, and only when the intent id matches the one stored at checkout.cancelputs stock back in the same batch that cancels the order, and only while the order is pending.
If OrdersService throws, the webhook answers 500 and Stripe tries again later.
A payment for a cancelled order
If an order was cancelled and its payment still succeeds, markPaid leaves the order cancelled and the Worker logs payment for a cancelled order, refund it as an error. Expiry cancels the intent before the order ([[checkout]]), so this needs an order cancelled some other way; the log line is there to search for if it happens.
Local testing
stripe listen --forward-to localhost:8788/stripe/webhook prints a webhook secret for .dev.vars and forwards test-mode events. Run the Worker with npx wrangler dev -c workers/stripe-webhook/wrangler.jsonc --port 8788 next to storefront-api.