Stripe webhooks
Stripe webhooks in Next.js and Supabase apps: the questions, answered
Most Stripe failures in an AI-built Next.js or Supabase app come from five places: a webhook handler that verifies the wrong bytes, a signing secret from the wrong endpoint or mode, access granted from the success page instead of a webhook, a handler that runs the same event twice, and an Edge Function that calls the synchronous verifier. Each question below has its own page with the fix, checked against the Stripe, Next.js and Supabase docs. Start with the error you see; if you see none yet, work through the checklist before your first real payment.
For the full pre-launch list, see the Stripe webhook checklist for AI-built apps.
Repository check
Check this in your repo
What the repository check covers for Stripe today
For AI-built Vercel + Supabase apps, run the command below from your app folder with Node.js 20 or newer. For Stripe it reports two findings. unverified_stripe_webhook is a webhook handler, in a Next.js route, an Express or Hono route or a Supabase Edge Function, that never calls constructEvent, reads no stripe-signature header, reads no raw body, or passes JSON.stringify(...) to constructEvent. missing_stripe_webhook is the stripe package in package.json, or Stripe used in your code, with no webhook handler found. It also reports env_var_drift when your code reads a variable such as STRIPE_WEBHOOK_SECRET that .env.example does not list; it cannot see which variables are set in Vercel.
It is advice, not a gate, and a repository check, not a live database or payment test: it does not call Stripe or take a payment, and it cannot see the secrets stored in Vercel or Supabase, the endpoints registered in Stripe, the events they receive, delivery attempts, or whether a handler is safe to run twice. Those stay with the checklist below.
npx -y viberaven@1.6.12 checkChecklist
Before your app takes real payments
- 01Your webhook route verifies every event with constructEvent (constructEventAsync on Deno) over the raw request body, the Stripe-Signature header and the endpoint secret, and returns 400 when verification throws.
- 02The secret in each environment belongs to the endpoint and mode that sends events there: the stripe listen secret locally, the sandbox endpoint secret on a test deployment, the live endpoint secret in production.
- 03A live mode endpoint is registered with your production URL, listens only for the event types you handle, and is not behind a redirect or a deployment protection page.
- 04Access is granted from checkout.session.completed (and checkout.session.async_payment_succeeded for delayed payment methods), not only from the success page.
- 05Subscription status stays current from customer.subscription.updated and customer.subscription.deleted, and access is revoked when a subscription is canceled or unpaid.
- 06The handler records each event ID it processes and skips repeats, and does not depend on events arriving in order.
- 07The handler returns a 2xx quickly. After a fix, resend failed events from the Dashboard and confirm 200 in the Event deliveries tab.
Questions
Stripe webhook questions, one page each
Why does Stripe say "No signatures found matching the expected signature for payload"?
One of the three values passed to constructEvent is wrong: the body, the header or the secret. Stripe names the wrong secret as the most common cause.
Go deeper: Why does Stripe say "No signatures found matching the expected signature for payload"?
How do I read the raw body for a Stripe webhook in the Next.js App Router?
Call await req.text() and pass the string to constructEvent unchanged. App Router route handlers need no bodyParser setting.
Go deeper: How do I read the raw body for a Stripe webhook in the Next.js App Router?
Is the Stripe webhook secret different in test and live mode?
Yes. Each endpoint has its own secret, a different one per mode, and stripe listen prints a third.
Go deeper: Is the Stripe webhook secret different in test and live mode?
Do I need a Stripe webhook if I use Stripe Checkout?
Yes, if access depends on the payment. Stripe says the success page alone is not reliable, and subscriptions need webhooks.
Go deeper: Do I need a Stripe webhook if I use Stripe Checkout?
How do I stop a Stripe webhook from processing the same event twice?
Record each event ID in a table with a primary key, in the same transaction as the grant, and skip IDs you have seen.
Go deeper: How do I stop a Stripe webhook from processing the same event twice?
Why does Stripe say "SubtleCryptoProvider cannot be used in a synchronous context" in a Supabase Edge Function?
Use await req.text() and constructEventAsync with the SubtleCrypto provider, and turn off JWT verification for that function only.
My Stripe webhook works locally but fails after deploy
Start from the Event deliveries tab: the status code tells you whether it is the secret, the body or a URL that never reaches your handler.
Go deeper: Stripe webhook works locally but fails after deploy: how to diagnose and fix it
Checking Stripe, env vars and Supabase policies together
For an app a coding agent wrote end to end: webhook signatures, env vars per Vercel environment, and RLS tested as real roles.
Go deeper: How do I check the Stripe webhooks, env vars and Supabase policies in an app Claude Code wrote?
Go deeper: Stripe Webhook Checklist for AI-Built Apps
Related
Related guides
Sources
Official docs behind this checklist
Browse all launch guides · Read resources · How to make and check your next change
FAQ
FAQ for builders before launch
Which Stripe webhook events does a subscription app need?
At least checkout.session.completed to link the customer and subscription to your user, customer.subscription.updated and customer.subscription.deleted to keep status current, and invoice.payment_failed to tell the customer. Stripe suggests subscribing only to the events you handle.
Can I test Stripe webhooks without deploying?
Yes. Run stripe listen --forward-to localhost:3000/api/webhooks/stripe, set the whsec_ secret it prints as your local webhook secret, and send events with stripe trigger checkout.session.completed.