DEV Community

Launchlane
Launchlane

Posted on

The 4 Stripe webhook mistakes that silently break paid signups (and how to fix them in Next.js)

Most "Stripe checkout works locally but customers don't get access" bugs come down to the webhook handler. Here are the four I keep seeing:

1. Parsing the body before verifying the signature.
stripe.webhooks.constructEvent needs the raw body. In the Next.js App Router, call await req.text() and don't req.json() first:

const body = await req.text();
const sig = req.headers.get("stripe-signature")!;
const event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
Enter fullscreen mode Exit fullscreen mode

2. Granting access on the redirect instead of the webhook. Users close tabs and success URLs get replayed. Grant access in checkout.session.completed and treat the success page as display only.

3. No idempotency. Stripe retries events. Store event.id and skip it if you've seen it, or a retry can provision twice.

4. Ignoring the subscription lifecycle. Handle customer.subscription.updated / deleted and invoice.payment_failed, or cancelled users keep access forever.

Test it locally: stripe listen --forward-to localhost:3000/api/webhooks/stripe, then stripe trigger checkout.session.completed.

Disclosure: Launchlane made this kit. I packaged this pattern (signature check, idempotency table, lifecycle handlers) into a small starter, the Stripe Checkout + Webhook Starter ($29): https://payhip.com/b/3TPQq. Everything above is enough to fix your own handler without it.

Top comments (0)