Fix Stripe webhook "No signatures found matching the expected signature for payload"
Updated · Applies to: Stripe, Next.js, Express, Supabase Edge Functions, Vercel, Cursor, Lovable
TL;DR fix: Stripe signs the exact raw request body. If your framework parsed it as JSON first, or you're using the wrong whsec_ secret (CLI vs Dashboard, test vs live), verification fails. In the Next.js App Router use await req.text(). In Express use express.raw({ type: "application/json" }) on the webhook route. Make sure STRIPE_WEBHOOK_SECRET is the secret for that endpoint. Never skip verification to "fix" it.
Steps
- Check you're using the right signing secret. Each endpoint has its own whsec_ secret. stripe listen prints a different secret than the Dashboard endpoint, and test and live mode differ. Print which one your code uses (not the value itself) and compare.
- Pass the raw body, unmodified. constructEvent needs the exact bytes Stripe sent. Don't JSON.parse and re-stringify, and don't let middleware parse the body before your handler.
- Use the Stripe-Signature header. Read the stripe-signature header from the request and pass it as the second argument.
- Fix it for your framework. Next.js App Router: await req.text(). Express: express.raw on the webhook route only, registered before express.json(). Supabase Edge Functions (Deno): await req.text() and constructEventAsync.
- Redeploy and resend a test event. Update env vars in your host (Vercel, Supabase secrets), redeploy, then resend the event from the Dashboard or trigger one with stripe trigger.
Next.js App Router (app/api/stripe/webhook/route.ts)
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(req: Request) {
const body = await req.text(); // raw body, not req.json()
const sig = req.headers.get("stripe-signature")!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
} catch (err) {
return new Response(`Webhook Error: ${(err as Error).message}`, { status: 400 });
}
// handle event.type ...
return Response.json({ received: true });
}
Express
// Register BEFORE app.use(express.json())
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const event = stripe.webhooks.constructEvent(
req.body, req.headers["stripe-signature"], process.env.STRIPE_WEBHOOK_SECRET);
res.json({ received: true });
});
Supabase Edge Function (Deno)
import Stripe from "npm:stripe";
const stripe = new Stripe(Deno.env.get("STRIPE_SECRET_KEY")!);
const cryptoProvider = Stripe.createSubtleCryptoProvider();
const body = await req.text();
const event = await stripe.webhooks.constructEventAsync(
body, req.headers.get("stripe-signature")!, Deno.env.get("STRIPE_WEBHOOK_SECRET")!,
undefined, cryptoProvider);
Stripe’s docs: “The request body must be the exact UTF-8 string Stripe sent, without any modifications.”
Prompt for your AI tool
Stripe webhook fails: "No signatures found matching the expected signature for payload".
Make the webhook route verify with the RAW body (req.text() / express.raw), use the stripe-signature
header, and read STRIPE_WEBHOOK_SECRET for this endpoint. Do not remove verification. Keep the secret
key server-side only. When to get help
Get help if webhooks drive fulfillment (orders, credits, subscriptions), if events are processed twice, or if your Stripe secret key may have been exposed. A leaked sk_live key is an emergency, so rotate it first.
Want us to look at your app?
VibeCPR Rescue Reports are launching soon: we scan your repo and live app, and a human verifies every critical finding. Join the waitlist for early access and founder pricing. You'll also get Patch Notes, our weekly fixes newsletter.
FAQ
What causes "No signatures found matching the expected signature for payload"?
Either the body passed to stripe.webhooks.constructEvent is not the exact raw body Stripe sent (usually because a framework parsed it as JSON first), or the webhook signing secret (whsec_) doesn't match the endpoint that sent the event, for example using the Stripe CLI secret in production.
How do I get the raw body in the Next.js App Router?
In a route handler, call const body = await req.text() and pass that string, the stripe-signature header, and your STRIPE_WEBHOOK_SECRET to stripe.webhooks.constructEvent. Don't call req.json() first.
Can I just skip signature verification?
No. Without verification anyone can POST fake events to your endpoint, for example a fake checkout.session.completed that unlocks paid features for free.
Sources
- Stripe docs: Resolve webhook signature verification errors
- Stripe docs: Receive Stripe events in your webhook endpoint
Related fixes
- Fix "Your Cloud Firestore database has insecure rules" (Firebase email)
- Fix Supabase Edge Function "blocked by CORS policy" (preflight)
- Fix "new row violates row-level security policy for table" (Supabase)
- Fix "404: NOT_FOUND" on refresh in a React/Vite app on Vercel
- Fix "Module not found: Can't resolve" on Vercel (works locally)