Home › Guides › No signatures found matching
Stripe webhook error: "No signatures found matching the expected signature for payload"
Matching pack: Webhooks Done Right (for Stripe) (US$19), an Agent Skill for Claude Code and Cursor with reference endpoints for Express/TypeScript and Flask/Python that verify against the raw body and process each event once. Read its test report (36 automated tests (18 Node, 18 Python), all passing, plus a clean TypeScript typecheck; locally signed fixtures, not live Stripe deliveries). Free: Stripe webhook signature checker, which runs in your browser.
Your endpoint receives the event, calls constructEvent(), and gets this error back. Stripe's docs say it means at least one of the three things you passed in is wrong: the request body, the Stripe-Signature header, or the endpoint secret.
The short answer
- Wrong secret (Stripe calls this the most common cause). A Dashboard endpoint and a
stripe listensession have different secrets, both starting withwhsec_. It is not yoursk_API key. - Parsed body. The signature is over the exact bytes Stripe sent. If your framework turned the body into JSON first, re-serialising it won't give the same bytes, so the check fails.
- Bad header. Pass the whole
Stripe-Signatureheader value, which looks liket=...,v1=....
Source: Stripe docs, "Resolve signature verification errors" (docs.stripe.com/webhooks/signature). Checked 28 Oct 2026.
1. Check the endpoint secret
- Dashboard endpoint: open the endpoint in Workbench and click Reveal secret. Test and live endpoints each have their own.
- Local testing with the Stripe CLI: use the secret that
stripe listenprints in the terminal, not the Dashboard one. - Print the secret your code actually loads (or its first few characters) and compare. A stale environment variable or the wrong
.envfile is a common way to end up with the other one.
2. Pass the raw body, not parsed JSON
Stripe's docs list the usual culprits: frameworks that reorder keys, strip whitespace, convert to JSON or change the encoding before your code runs. With Express, app.use(express.json()) must come after the webhook route, because middleware runs in order. Give the webhook route its own raw parser:
// The webhook route gets the raw bytes. Register it BEFORE express.json().
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
let event;
try {
event = stripe.webhooks.constructEvent(
req.body, // a Buffer, untouched
req.headers['stripe-signature'],
process.env.STRIPE_WEBHOOK_SECRET // whsec_... for THIS endpoint
);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
res.sendStatus(200); // reply fast, then do the slow work
});
// JSON parsing for every other route
app.use(express.json());
In a Next.js App Router route handler, read the body with await req.text(), as Stripe's own example does. For the Pages Router, Stripe's docs say to turn off bodyParser and read the raw buffer.
// app/api/webhooks/route.ts (App Router)
export async function POST(req: Request) {
const body = await req.text(); // not req.json()
const sig = req.headers.get('stripe-signature');
const event = stripe.webhooks.constructEvent(body, sig!, process.env.STRIPE_WEBHOOK_SECRET!);
// ...
}
Behind AWS API Gateway with Lambda, Stripe's docs describe a body mapping template that passes a rawBody field through to the function. Any proxy or platform that rewrites JSON bodies needs the same kind of fix.
Sources: Stripe docs, "Resolve signature verification errors" (docs.stripe.com/webhooks/signature); stripe-node example, examples/webhook-signing/nextjs/app/api/webhooks/route.ts (github.com/stripe/stripe-node). Checked 28 Oct 2026.
3. Check the header (and the clock)
Log the header value you pass. It should look like t=xxx,v1=yyy, possibly with more entries. If it is empty, your code is reading the wrong header name or your proxy drops it.
A related error is a timestamp outside the tolerance zone. stripe-node rejects a signature whose timestamp is more than 300 seconds (5 minutes) from your server's clock by default, which protects against replayed requests. Keep the server clock synced rather than raising the tolerance.
Source: stripe-node source, src/Webhooks.ts, DEFAULT_TOLERANCE 300 seconds (github.com/stripe/stripe-node). Checked 28 Oct 2026.
Quick checklist
- The secret starts with
whsec_and belongs to this endpoint, in this mode (or to thisstripe listensession). - The body reaching
constructEvent()is a string or Buffer of the original bytes, not an object. - No JSON middleware runs before the webhook route.
- The header is the full
Stripe-Signaturevalue. - The server clock is correct.
Want to check one delivery by hand? Paste the raw body, header and secret into the free Stripe webhook signature checker. It runs in your browser and sends nothing anywhere. Use a test-mode secret.
Let your agent build it the safe way
Webhooks Done Right (for Stripe) (US$19) is an Agent Skill for Claude Code and Cursor. This skill teaches Claude Code or Cursor to avoid them. It includes reference endpoints for Express/TypeScript and Flask/Python, atomic idempotency stores (including Postgres), a Postgres schema tested on a real database, and signed test fixtures. Its tests use locally signed fixtures checked with the real Stripe libraries; live deliveries from Stripe were not used.