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.

Server logs
StripeSignatureVerificationError: No signatures found matching the expected signature for payload. Are you passing the raw request body you received from Stripe? If a webhook request is being forwarded by a third-party tool, ensure that the exact request body, including JSON formatting and new line style, is preserved. Learn more about webhook signing and explore webhook integration examples for various frameworks at https://docs.stripe.com/webhooks/signature

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 to constructEvent.
  • STRIPE_WEBHOOK_SECRET holds the secret from another endpoint, from stripe 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

  1. In the webhook route, read the body as raw text or a buffer and pass it unchanged to stripe.webhooks.constructEvent.
  2. In Express, put express.raw({ type: 'application/json' }) on the webhook route and register that route before app.use(express.json()).
  3. Open the endpoint in the Stripe Dashboard (Workbench → Webhooks), click Reveal secret, and set that exact value as STRIPE_WEBHOOK_SECRET for the matching environment. Redeploy.
  4. For local testing, use the whsec_ secret that stripe listen --forward-to localhost:3000/api/webhooks prints.
  5. 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 });
}
All errors
StripeAuthenticationError: Invalid API Key provided: sk_test_********************4242

Invalid 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.

StripeInvalidRequestError: No such price: 'price_1Q2w3E4r5T6y7U8i9O0pAbCd'

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.

POST https://my-app.com/api/checkout 500 (Internal Server Error)

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.