Skip to main content

Stripe webhook signature verification failed: verify the raw body with the right secret

Why Stripe rejects your webhook, how to find the cause, and the Supabase Edge Function code that fixes it.

Updated 4 min readMarkdown
Stripe webhook signature verification failed: verify the raw body with the right secret

Payments work with the Stripe CLI. Then real events start hitting your endpoint and every delivery fails with "No signatures found matching the expected signature for payload", "Webhook signature verification failed", or, in Deno, "SubtleCryptoProvider cannot be used in a synchronous context".

What's going on: Stripe signs the exact bytes it sends. If your code parses the JSON first, or checks the signature with the wrong secret, the bytes or the key don't match and verification fails.

Four reasons it fails

  • The body was parsed first. req.json() followed by re-stringifying changes whitespace and key order, so the signature no longer matches.
  • The secret is the wrong one. Every endpoint has its own whsec_ secret, test and live mode differ, and the Stripe CLI uses yet another one for local forwarding.
  • Verification is synchronous in Deno. Edge Functions run on Deno and need constructEventAsync with the SubtleCrypto provider. The synchronous method throws.
  • Something rewrote the request. A proxy or platform layer dropped the Stripe-Signature header or changed the body.

Pin down which one it is

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

The fix: raw body, async verification, the endpoint's own secret

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

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 })
})

Stripe's docs say it plainly: verification needs the raw request body, and any change to it makes verification fail. That's why the body is read as text and never parsed before the check.

Deploy with JWT verification turned off. Stripe authenticates with its signature, not a Supabase token; Supabase's example uses supabase functions deploy --no-verify-jwt stripe-webhooks.

Secrets per environment

Production gets the live endpoint's secret, staging gets the test endpoint's secret. Stripe generates a unique secret for each endpoint, and test and live are different even for the same URL. When you forward events with the Stripe CLI, use the secret stripe listen prints.

How to confirm it's fixed

  • Send a test event from the Stripe dashboard: the delivery returns 200.
  • Change one character of the secret: the delivery now fails with 400, so you know the check is real.
  • Run a test-card payment end to end and confirm the order or subscription updates in your database.

Keep it from coming back

  • Stripe retries, so make each event idempotent: store the event id and skip ones you've already handled.
  • Reconcile with the Stripe API daily, so a missed webhook can't leave an order unpaid in your system.

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

Why did it work locally? The Stripe CLI has 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.


The original fix is on our site: Fix "No signatures found matching the expected signature" from Stripe. If you're getting ready to launch, the free Launch Check looks at a live app in about a minute.

I wrote this with AI assistance and reviewed it myself.

About the author

  • Founder and CEO of StacksMesh (stacksmesh.com). We fix, launch and migrate apps built with Base44, Lovable, Bolt and Replit, with a fixed price in writing before any work. I write about making AI-built apps safe and production-ready.

No comments yet. Be the first to comment.