No signatures found matching the expected signature
Your webhook code couldn’t verify that a Stripe event is genuine, usually because the body was parsed before the check or the secret is wrong.
Also appears as
- StripeSignatureVerificationError: Webhook payload must be provided as a string or a Buffer (https://nodejs.org/api/buffer.html) instance representing the _raw_ request body.Payload was provided as a parsed JavaScript object instead. Signature verification is impossible without access to the original signed material. Learn more about webhook signing and explore webhook integration examples for various frameworks at https://docs.stripe.com/webhooks/signature
- Webhook signature verification failed. Err: No signatures found matching the expected signature for payload.
Prompt to fix it
My Stripe webhook fails with "No signatures found matching the expected signature for payload". Find the webhook handler and make it verify the signature with the raw request body: read it as text or a buffer before any JSON parsing, and pass it unchanged to stripe.webhooks.constructEvent (or constructEventAsync on edge runtimes). Check that the signing secret comes from STRIPE_WEBHOOK_SECRET and isn’t the API key. Tell me which whsec_ secret to use for local testing with the Stripe CLI and which for the deployed endpoint. Keep signature verification on.
Paste into Layerwise or any AI coding tool.
What it means
Stripe signs every webhook it sends. Your server recomputes that signature from three things: the exact request body, the Stripe-Signature header, and your endpoint’s signing secret (whsec_...). This error means the result didn’t match, so the Stripe library refused to trust the event.
The body is the usual culprit. The signature covers the exact bytes Stripe sent, and many frameworks parse JSON before your code runs. Turning it back into a string with JSON.stringify loses the original spacing and line breaks, which breaks the match. In a Next.js route handler, read await request.text(); in Express, use express.raw({ type: 'application/json' }) on the webhook route.
The secret is the other half. Each endpoint has its own: the one stripe listen prints works only with the Stripe CLI, and test and live endpoints have different secrets. With the wrong secret, every event fails no matter how good the code is.
Common causes
- The handler parses the body with
request.json(),express.json(), or a body parser before passing it toconstructEvent. STRIPE_WEBHOOK_SECRETholds the secret from another endpoint, fromstripe listen, or from the other mode.- The secret picked up spaces, quotes, or a line break when it was pasted into the host’s settings.
- A proxy, middleware, or forwarding tool changes the body before it reaches your handler.
How to fix it
- In the webhook route, read the body as raw text or a buffer and pass it unchanged to
stripe.webhooks.constructEvent. - In Express, put
express.raw({ type: 'application/json' })on the webhook route and register that route beforeapp.use(express.json()). - Open the endpoint in the Stripe Dashboard (Workbench → Webhooks), click Reveal secret, and set that exact value as
STRIPE_WEBHOOK_SECRETfor the matching environment. Redeploy. - For local testing, use the
whsec_secret thatstripe listen --forward-to localhost:3000/api/webhooksprints. - On Cloudflare Workers or another edge runtime, call
await stripe.webhooks.constructEventAsync(...).
Example fix
// app/api/webhooks/stripe/route.ts
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
// The signature covers the exact bytes, so read the raw text.
const body = await request.text();
const signature = request.headers.get('stripe-signature') ?? '';
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
console.error(err);
return new Response('Invalid signature', { status: 400 });
}
// Handle event.type here.
return Response.json({ received: true });
}Related errors
All errorsInvalid API Key provided
Stripe rejected the request because the API key it received doesn’t match any active key, often a placeholder, a typo, or a deleted key.
No such price
Stripe couldn’t find the price ID your code sent, because it belongs to the other mode, another account or sandbox, or isn’t a price ID at all.
500 Internal Server Error
The server’s code threw an error or crashed while handling the request, and the real error message is waiting in the server logs.
Build with an agent that checks its work
The Layerwise agent clicks through the live preview and reads the console, so it catches errors before you do.