Stripe's library threw before checking anything else: the header argument you passed to constructEvent / construct_event was undefined, None or an empty string. Stripe always sends a Stripe-Signature header on webhook deliveries, so either your code read it wrong or something between Stripe and your code didn't pass it on. (Older stripe-node versions, up to v11.0, report the same problem as “Unable to extract timestamp and signatures from header”.)
req.headers['Stripe-Signature'] is undefined. Use req.headers['stripe-signature'] or req.get('Stripe-Signature'). In Django, request.META['Stripe-Signature'] doesn't exist; the key is HTTP_STRIPE_SIGNATURE.event.headers['Stripe-Signature'] is missing. With a REST API non-proxy integration, headers only reach the function if your mapping template passes them; Stripe's docs give a template that passes rawBody and headers.Object.keys(req.headers)) on one delivery and check that stripe-signature is there.stripe listen --forward-to … and stripe trigger …, or Resend a real event from the Dashboard.app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['stripe-signature']; // lower-case key (or req.get('Stripe-Signature'))
if (!sig) return res.status(400).send('Missing Stripe-Signature header');
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_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'); // Headers.get() is case-insensitive
if (!sig) return new Response('Missing Stripe-Signature header', { status: 400 });
// stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET) ...
}
// Find the header whatever the casing; Stripe sends "Stripe-Signature"
const headers = Object.fromEntries(Object.entries(event.headers || {}).map(([k, v]) => [k.toLowerCase(), v]));
const sig = headers['stripe-signature'];
const body = event.isBase64Encoded ? Buffer.from(event.body, 'base64') : event.body;
const stripeEvent = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET);
@app.post("/webhooks/stripe")
async def stripe_webhook(request: Request):
payload = await request.body()
sig_header = request.headers.get("stripe-signature") # case-insensitive
if not sig_header:
raise HTTPException(status_code=400, detail="Missing Stripe-Signature header")
event = stripe.Webhook.construct_event(payload, sig_header, endpoint_secret)
A FastAPI parameter stripe_signature: str | None = Header(None) also works, because FastAPI converts underscores to hyphens.
sig_header = request.headers.get("Stripe-Signature") # or request.META.get("HTTP_STRIPE_SIGNATURE")
event = stripe.Webhook.construct_event(request.body, sig_header, settings.STRIPE_WEBHOOK_SECRET)
Once the header arrives, signature mismatches are usually a raw-body problem: see “No signatures found matching the expected signature for payload” and Express raw body. Python handlers in full: stripe.SignatureVerificationError in Python.
Header found, 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).