Stripe webhooks: Processing payments reliably
In short: Stripe reports payments, subscriptions and refunds by webhook to your server; you verify the signature, reply quickly, process idempotently and do not rely on the customer’s return page.
In more detail: After a payment the customer returns to your site, but the browser can be closed or the connection drop. Only the server message (webhook) from Stripe is reliable: an HTTP POST with an event (checkout.session.completed, invoice.paid, customer.subscription.deleted …).
In Depth
Flow
- Create an endpoint (
/api/stripe/webhook) and select events in Stripe (dashboard or CLI). - Verify the signature with the webhook secret and the raw request body:
const sig = req.headers.get("stripe-signature")!;
const body = await req.text(); // raw, not JSON-parsed!
const event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);Without verification anyone could report forged payments. 3. Process the event (mark order paid, set subscription status, send invoice). 4. Reply quickly with 2xx; put long work into a queue/background job.
Reliability
- Retries: Stripe resends if you do not reply 2xx (for several days). Your code must be idempotent: store the event ID (
event.id) and ignore duplicates; check state transitions. - Order not guaranteed: events can arrive swapped; if needed fetch the object anew from Stripe.
- Database outage: then reply 5xx so Stripe retries instead of losing events. With serverless databases that first wake up, allow enough timeout.
- Test mode/live mode have separate secrets and endpoints.
- Stripe CLI (
stripe listen --forward-to localhost:3000/api/stripe/webhook) for local testing.
Security and law
Keep the secret private, HTTPS only, only necessary events. Check amount and currency server-side, do not take them from the browser. Store personal data sparingly (Stripe stores card data, not you: PCI burden low with Checkout/Elements). Legal: withdrawal notice, invoices, terms, price information, cancellation button for subscriptions, minors and contracts (parents’ consent). This is not legal advice.
Typical mistakes
- Body parsed as JSON beforehand → signature check fails.
- Wrong secret (test vs. live, several endpoints).
- No idempotency: duplicate credits/emails.
- Too slow a reply → flood of retries.
- Trusting only the redirect.
- Events not logged: with an event log (
stripe_events) errors can be traced later. Monitor failed deliveries in the dashboard.
See also: Stripe, Webhooks, HMAC, Idempotence, Payment services for minors