auditmyapp

Notes / Stripe errors

Stripe says "No signatures found matching the expected signature for payload"

The hint in the error message is usually right. Here are the four causes, and why switching the check off is the worst available fix.

You set up a Stripe webhook, send a test event, and your handler throws this:

No signatures found matching the expected signature for payload.
Are you passing the raw request body you received from Stripe?

What the check is doing

Stripe signs every webhook with a secret that only you and Stripe know. Your handler recomputes the signature from the request body and compares the two. If a single byte of the body differs from what Stripe sent, or you are using a different secret, they don't match and you get this error.

Cause 1: the body was parsed before you checked it

This is the usual culprit. The signature covers the body exactly as Stripe sent it. If something parses it as JSON first and the parsed version is turned back into text, the spacing and key order change, and the check fails.

In a Supabase Edge Function, read the body once as text and pass that text straight in:

const body = await req.text();   // not req.json()
const signature = req.headers.get('stripe-signature') ?? '';

const event = await stripe.webhooks.constructEventAsync(
  body, signature, Deno.env.get('STRIPE_WEBHOOK_SECRET')!,
  undefined, Stripe.createSubtleCryptoProvider(),
);

In a Next.js App Router route, the same idea:

export async function POST(req: Request) {
  const body = await req.text();
  const signature = req.headers.get('stripe-signature') ?? '';
  const event = stripe.webhooks.constructEvent(
    body, signature, process.env.STRIPE_WEBHOOK_SECRET!,
  );
  // handle the event
}

In Express, use express.raw({ type: 'application/json' }) on the webhook route, and make sure a global express.json() isn't running on that route first.

Cause 2: the wrong signing secret

Every webhook endpoint has its own signing secret, starting with whsec_. It is not your Stripe API key. Three mix-ups cover nearly every case:

A stray space or line break pasted into the environment variable has the same effect, so check that too.

Cause 3: something changed the body on the way in

Frameworks, proxies and middleware sometimes decode, re-encode or trim request bodies. If your own code looks right and the check still fails, look for anything sitting between the request and your handler, such as a global JSON parser or a proxy that rewrites requests.

Cause 4: the body was already read

A request body can only be read once. If an earlier line has already read it, for example calling req.json() to log it, you will either get an error saying the body has already been used, or pass an empty body into the check. Read it once, as text, at the top, and parse your own copy afterwards if you need it.

Related errors you may see instead

Don't switch the check off

After an hour on this error, commenting out the verification is very tempting, and the handler starts working immediately. It also means anyone who finds your webhook URL can send a fake "payment succeeded" event and receive whatever you sell, for free. Your webhook URL is not a secret. The signature is the only thing proving the request came from Stripe.

Once verification works, the next thing to get right is Stripe sending you the same event twice, which it sometimes will. That is covered here.