5 stripe webhook bugs i shipped so you dont have to
idempotency, ordering, timeouts and the wrong secret. all in production, all on a tiny SaaS with 30 customers
Contents
my app takes payments through stripe billing, and stripe tells my app what happened (paid, failed, cancelled) through webhooks. webhooks look simple. a POST request arrives, you update a row. i have now shipped five different bugs in roughly 80 lines of code, so here they are, in the order they hurt me.
1. parsing the body before verifying the signature
stripe signs every webhook and you should check that signature, otherwise anyone who finds your endpoint can POST an "invoice.paid" and get a free account. the check needs the raw request body, byte for byte. my first version did await req.json() and later passed JSON.stringify(body) to the verify function. different whitespace, different bytes, every single signature failed, and i "fixed" it by... removing the check. please dont be me. in a next.js route handler read the text and verify that:
export async function POST(req: Request) {
const body = await req.text()
const signature = req.headers.get("stripe-signature") ?? ""
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!)
} catch {
return new Response("invalid signature", { status: 400 })
}
// only now act on event
}
2. assuming each event arrives once
stripe can deliver the same event more than once. retries, network blips, whatever. my handler sent a "welcome, you're subscribed!" email on checkout.session.completed and one groomer got it three times and asked if she'd been charged three times. (she hadnt.) fix: store every event id with a unique constraint and skip the ones youve already handled.
create table stripe_events (
id text primary key,
processed_at timestamptz not null default now()
);
3. assuming events arrive in order
they dont, and the docs say so. i had customer.subscription.updated arrive before customer.subscription.created, my code tried to update a row that didnt exist yet and threw. even worse, an old "past_due" update arrived after a newer "active" one and briefly locked a paying customer out.
what fixed it for good was treating the webhook as a nudge, not as the truth. when any subscription event arrives i take the subscription id, fetch the current subscription from the API, and save that. one extra API call, zero ordering bugs since.
4. doing slow work inside the handler
stripe wants a 2xx back quickly. my handler was generating a PDF receipt and sending an email before responding. the day my email provider was slow, requests timed out, stripe marked them failed and retried, and with bug 2 still half fixed at the time you can guess what happened. now the handler verifies, records the event, puts a job on a queue and returns 200. the slow stuff happens after.
5. the wrong signing secret in production
every webhook endpoint has its own signing secret, and the stripe CLI's stripe listen gives you a different one again for local testing. i deployed with the CLI secret in my production env vars. every live webhook failed signature for two days and i only found out when stripe emailed me that my endpoint was failing. two days of payments my app didnt know about. i now have a tiny health check that alerts me if no webhook has been processed in 24 hours, which for a 30 customer business is a decent smoke alarm.
bonus: test the boring future
renewals, failed cards and trial endings happen weeks after signup, which is exactly when you've forgotten how your code works. stripe test clocks let you create a customer in test mode and move time forward, so you can watch a trial end and a renewal fail in 5 minutes instead of 30 days. i found a bug in my failed payment emails this way before any customer did, which made a nice change.
none of this is advanced. the happy path works on day one and the rest only shows up when real money is moving. if youre about to wire up payments for your side project, go through these five before your first customer does it for you.
Written by
Chloe Barnes
bootstrapping a tiny scheduling SaaS for dog groomers from manchester. ex agency dev. sharing the revenue numbers, the mistakes and the occasional win. tea > coffee
Get the best of #ledger_lines weekly
One email each Monday with the most useful posts. No spam; unsubscribe in one click.
Comments
Sign in to join the conversation.
Sign in to comment