Stripe webhook failing: "No signatures found matching the expected signature"
Stripe shows failed webhook deliveries (400 or 401) to your Supabase Edge Function or API route. How to fix signing secrets, raw body parsing and JWT settings.
Why it happens
Stripe signs every webhook. Your code must check the signature using the exact raw request body and the endpoint's signing secret (whsec_...). If the body was parsed as JSON first, or the secret is from a different endpoint or from test mode, verification fails.
Supabase Edge Functions require a Supabase JWT by default. Stripe does not send one, so the request is rejected with 401 before your code runs, unless JWT verification is turned off for that function.
In Deno (Supabase Edge Functions), you must use the async constructEventAsync method with a crypto provider; the synchronous version throws.
How to fix it, step by step
- In Stripe → Developers → Webhooks (Workbench), open the endpoint and look at a failed event to see the response code and body.
- 401: deploy the function with JWT verification off (supabase functions deploy <name> --no-verify-jwt, or verify_jwt = false in config.toml). The Stripe signature check is your security.
- 400 signature error: copy the signing secret from that exact endpoint, in the same mode (test vs live), and save it as STRIPE_WEBHOOK_SECRET in your function secrets.
- Read the body as raw text (await req.text()) and pass it unchanged to stripe.webhooks.constructEventAsync(body, signature, secret, undefined, Stripe.createSubtleCryptoProvider()).
- Return a 200 quickly after handling the event, and make the handler safe to run twice (Stripe retries).
- Use "Resend" on a failed event in Stripe to test the fix.
Paste-ready prompt for Lovable
My Stripe webhook to a Supabase Edge Function fails. Stripe shows: <status code and error>. Please:
1. Make sure the function reads the raw body with await req.text() and verifies with stripe.webhooks.constructEventAsync(body, sig, Deno.env.get('STRIPE_WEBHOOK_SECRET'), undefined, Stripe.createSubtleCryptoProvider()).
2. Configure the function to deploy without JWT verification (verify_jwt = false) since Stripe signs the request.
3. Handle checkout.session.completed (and subscription events if I use subscriptions) idempotently, using the Stripe event or session id.
4. Return 200 on success and 400 only on signature failure.
5. Tell me which secrets I need to set and where to find them.FAQ
Is turning off JWT verification safe?
For a webhook, yes, as long as your code verifies the Stripe signature and rejects anything that fails.
My test webhooks work but live ones fail.
Test and live endpoints have different signing secrets. Create the live endpoint and use its secret in production.
Still broken?
Get a free diagnosis by email within 24 hours. Fixed or refunded.
Get a free diagnosisOr get the weekly fix digest:
Free: the Vibe-Code Fix Checklist.