DEV Community

Cover image for Stripe webhook "No signatures found matching the expected signature"? Here's the fix
Ahmed Hassan Yaseen
Ahmed Hassan Yaseen

Posted on Originally published at stacksmesh.com

Stripe webhook "No signatures found matching the expected signature"? Here's the fix

Stripe sends a webhook to your endpoint, and the delivery fails with one of these:

No signatures found matching the expected signature for payload
Webhook signature verification failed
SubtleCryptoProvider cannot be used in a synchronous context
Enter fullscreen mode Exit fullscreen mode

Stripe signs the exact bytes it sends. If your code parses the JSON before verifying, or uses the wrong signing secret, the signature no longer matches. The fix: read the raw body as text, verify it with constructEventAsync and the endpoint's own whsec_ secret, and only then parse the event.

I see this one a lot in apps built with Lovable, Bolt and Base44 on Supabase, usually when payments move from the Stripe CLI to a real endpoint.

Why it happens

  1. The body was parsed before verification. Calling req.json() and re-stringifying changes whitespace and key order, so the signature no longer matches.
  2. The wrong signing secret. Each endpoint has its own whsec_ secret, test and live mode differ, and the Stripe CLI uses another one for local forwarding.
  3. Synchronous verification in Deno. Edge Functions run on Deno and need constructEventAsync with the SubtleCrypto provider; the synchronous method throws.
  4. A proxy or platform rewrote the request. A layer in between dropped the Stripe-Signature header or changed the body.

Find the cause

  1. In Stripe, open the endpoint under Webhooks and read the failed delivery's response body.
  2. Confirm which mode (test or live) the event came from, and which secret the function reads.
  3. Log whether the Stripe-Signature header arrives at all.

Fix 1: verify the raw body in a Supabase Edge Function

Store the endpoint's signing secret as a function secret (for example STRIPE_WEBHOOK_SIGNING_SECRET). Read the body with req.text(), verify it with constructEventAsync and the SubtleCrypto provider, then handle the event.

import Stripe from 'npm:stripe@^22'

const stripe = new Stripe(Deno.env.get('STRIPE_API_KEY')!)
const cryptoProvider = Stripe.createSubtleCryptoProvider()

Deno.serve(async (req) => {
  const signature = req.headers.get('Stripe-Signature')
  const body = await req.text() // the exact bytes Stripe signed
  let event
  try {
    event = await stripe.webhooks.constructEventAsync(
      body, signature!, Deno.env.get('STRIPE_WEBHOOK_SIGNING_SECRET')!, undefined, cryptoProvider
    )
  } catch (err) {
    return new Response((err as Error).message, { status: 400 })
  }
  // handle event.type here, and make it safe to receive twice
  return Response.json({ received: true })
})
Enter fullscreen mode Exit fullscreen mode

The important line is await req.text(). Stripe's docs are blunt about it: verification needs the raw request body, and any manipulation of that body makes it fail.

Deploy the function with JWT verification off. Stripe proves itself with the signature, not with a Supabase token. Supabase's own example deploys it with supabase functions deploy --no-verify-jwt stripe-webhooks.

Fix 2: match secrets to environments

  • Use the live endpoint's secret in production and the test endpoint's secret in staging. Stripe generates a unique secret for each endpoint, and the test and live secrets are different.
  • When forwarding with the Stripe CLI, use the secret that stripe listen prints.

Check the fix worked

  • Send a test event from the Stripe dashboard: the delivery shows 200.
  • Change one character of the secret: the delivery fails with 400. That proves the check is real.
  • Pay with a test card end to end and confirm the order or subscription updates in your database.

Stop it coming back

  • Handle each event idempotently (store the event id), because Stripe retries.
  • Reconcile with the Stripe API every day, so a missed webhook never leaves an order unpaid in your system.

Quick answers

Can I skip signature verification? No. Without it, anyone can post a fake "payment succeeded" event to your endpoint and unlock paid features.

Why did it work locally? The Stripe CLI uses its own signing secret. Production needs the secret of the endpoint registered in the dashboard.

Sources: Stripe: Check the webhook signatures and Supabase example: Stripe webhooks in an Edge Function.


I first published this fix on our site: Fix "No signatures found matching the expected signature" from Stripe. To check your live app for exposed keys, missing security headers and broken basics in about a minute, run the free Launch Check.

I wrote this with AI assistance and reviewed it myself.

Top comments (0)