Stripe webhook: "No signatures found matching the expected signature"
5 min read · Updated 27 September 2026
Stripe signs every webhook it sends, and stripe.webhooks.constructEvent() checks that signature against the exact bytes Stripe sent. If anything changes the body, or you use the wrong secret, you get "No signatures found matching the expected signature for payload".
Cause 1: your framework parsed the body first
The signature is calculated over the raw text. If your code turns it into JSON first (and back again), even one space's difference breaks the check. Always pass the raw body.
// 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 text, NOT req.json()
const signature = req.headers.get("stripe-signature") ?? "";
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
} catch {
return new Response("Bad signature", { status: 400 });
}
// handle event.type here, then:
return new Response("ok");
}// Express: the webhook route needs express.raw, and must come BEFORE app.use(express.json())
app.post("/api/stripe/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.sendStatus(200);
});Cause 2: the wrong signing secret
- Every webhook endpoint has its own secret (whsec_...). Copy it from Stripe Dashboard → Developers → Webhooks → your endpoint → Signing secret.
- Test mode and live mode are separate. A test-mode secret never verifies live events, and the reverse is also true.
- The Stripe CLI (stripe listen) prints its own temporary secret. Use that one for local testing only.
- Check the environment variable for stray quotes or spaces, and redeploy after changing it.
Test it locally
stripe listen --forward-to localhost:3000/api/stripe/webhook
stripe trigger checkout.session.completedOnly subscribe your endpoint to the events your code actually handles, such as checkout.session.completed and customer.subscription.updated. Selecting every event just creates noise and failed deliveries.
Still stuck?
Share your screen and we'll fix it together, live. A$99 / hour, agreed price, no surprises.
