All guidesStripe and webhooks
On this page

Stripe webhooks

SupabaseStripe

Stripe webhooks in Next.js and Supabase apps: the questions, answered

By Ohad Krispin, founder of VibeRaven · LinkedIn

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.

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 check

Checklist

Before your app takes real payments

  1. 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.
  2. 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.
  3. 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.
  4. 04Access is granted from checkout.session.completed (and checkout.session.async_payment_succeeded for delayed payment methods), not only from the success page.
  5. 05Subscription status stays current from customer.subscription.updated and customer.subscription.deleted, and access is revoked when a subscription is canceled or unpaid.
  6. 06The handler records each event ID it processes and skips repeats, and does not depend on events arriving in order.
  7. 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.

Go deeper: Why does Stripe say "SubtleCryptoProvider cannot be used in a synchronous context" in a Supabase Edge Function?

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

Sources

Official docs behind this checklist

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.