This error is about the second argument you pass to constructEvent / construct_event, not about your body or your secret. Stripe's library splits that value on commas and looks for t=<timestamp> and v1=<signature>. A real Stripe-Signature header looks like this:
t=1728480000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd,v0=6ffbb59b2300aae63f272406069a9788598b792a944a07aba816edb039989a39
If there's no parseable t= in the value (or the value isn't a string at all), you get this error. If there's a t= but no v1=, you get No signatures found with expected scheme instead.
constructEvent(rawBody, signatureHeader, endpointSecret). Passing the whsec_… secret second, and the header third, produces exactly this error.req.headers, request.headers) instead of the one header's value.Authorization, a header your own proxy adds, or a value you made up when testing with curl or Postman.No stripe-signature header value was provided; see that page for the missing-header causes (header-name casing, API gateways, proxies).stripe listen --forward-to localhost:3000/webhook plus stripe trigger payment_intent.succeeded, or Resend an event from the Dashboard.Log the type and the start of the value you pass. The signature header isn't secret, but never log your whsec_ secret.
console.log(typeof sig, String(sig).slice(0, 20)); // expect: string t=1728480000,v1=...
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['stripe-signature']; // one header's value; Node lower-cases header names
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET); // body, header, secret
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
res.sendStatus(200);
});
export async function POST(req) {
const body = await req.text();
const sig = req.headers.get('stripe-signature'); // a string, not the Headers object
try {
const event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET);
} catch (err) {
return new Response(`Webhook Error: ${err.message}`, { status: 400 });
}
return new Response(null, { status: 200 });
}
# FastAPI: sig = request.headers.get("stripe-signature")
# Django: sig = request.headers.get("Stripe-Signature") # or request.META.get("HTTP_STRIPE_SIGNATURE")
# Flask: sig = request.headers.get("Stripe-Signature")
event = stripe.Webhook.construct_event(payload, sig, endpoint_secret) # payload, header, secret
Full Python handlers are on stripe.SignatureVerificationError in Python.
Once the header parses, the next error you may see is “No signatures found matching the expected signature for payload”. That one is about the raw body or the secret.
Header fixed, body still failing? The guide covers every common Stripe webhook failure with copy-paste fixes. The kit is a self-hosted Cloudflare Worker relay that verifies Stripe signatures, then retries, replays or dead-letters each event instead of dropping it.
Fix Your Stripe Webhooks guide · Self-Hosted Webhook Relay Kit. Code LAUNCH40 = 40% off until Sun Oct 11, 6:40 AM MT.
Free download: Webhook debugging cheat sheet.
Still failing? Paste your payload, header and secret into the free Stripe signature checker. It runs in your browser and tells you which cause it is.
All five common causes with copy-paste fixes: Fix Your Stripe Webhooks guide ($9). Retries, replay and a dead-letter list in front of your endpoint: Self-Hosted Webhook Relay Kit ($19).