Common errors · Next.js, Express, Supabase Edge Functions
Stripe webhook: No signatures found matching the expected signature for payload
Cause The body was parsed as JSON before verification, or the whsec_ secret belongs to another endpoint or mode.
Fix Pass the raw request text to constructEvent and use the signing secret of this exact endpoint and mode.
Have it fixed for you, 290 EURSee the fix step by step
Payments succeed, but the webhook returns 400 and the order never unlocks. The signature is computed on the exact raw bytes Stripe sent; anything that parses the body first breaks it.
Where it breaks
Which case is yours?
- The framework parsed the body as JSON and you pass the re-serialised object to constructEvent.
- The signing secret is wrong: test vs live mode, or the secret of the Stripe CLI (stripe listen) used in production. Each endpoint has its own whsec_ value.
- The endpoint was never registered in the Stripe dashboard for this mode, so the events you see come from another endpoint.
The fix, step by step
- Next.js App Router: read the raw text, never req.json().
export async function POST(req: Request) { const body = await req.text(); const sig = req.headers.get("stripe-signature")!; const event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!); // handle event.type ... return new Response(null, { status: 200 }); } - Express: mount express.raw on the webhook route before any express.json().
app.post("/webhook", express.raw({ type: "application/json" }), handler); - Supabase Edge Function (Deno): use the async variant with the raw text.
const body = await req.text(); const event = await stripe.webhooks.constructEventAsync(body, sig, secret); - Copy the signing secret from Developers > Webhooks > your endpoint, in the mode you run (live or test), into the host's environment variables, then redeploy.
How to check it is fixed
In the Stripe dashboard, resend a checkout.session.completed event to the endpoint: it shows 200, and your database row for that order is updated.
Delivery example for this error
- const body = await req.json();+ const body = await req.text();+ constructEvent(body, sig, secret)- Root cause: The body was parsed as JSON before verification, or the whsec_ secret belongs to another endpoint or mode.
- Change made: Pass the raw request text to constructEvent and use the signing secret of this exact endpoint and mode.
- In the Stripe dashboard, resend a checkout.session.completed event to the endpoint: it shows 200, and your database row for that order is updated.
Illustrative: the real diff is written against your repository. Full sample report
Still stuck on your own code?
Send the repository and the exact error. Fix: 290 EUR all taxes included, delivered within 72 hours, refunded if not resolved.
Not sure it is this error? Ask for a free written diagnosis, no card needed.