{"slug": "stripe-webhook-signature-verification-failed-3-things-to-check", "title": "Stripe webhook signature verification failed: 3 things to check", "summary": "A developer-authored guide explains that Stripe webhook signature verification failures almost always stem from three causes: using the wrong signing secret, passing a parsed JSON body instead of the raw request bytes, or a proxy altering the payload in transit. The guide provides code for Next.js App and Pages Router, Express, and Supabase Edge Functions, noting that Deno requires the async constructEventAsync with Stripe's SubtleCryptoProvider and that Supabase blocks Stripe webhooks with a 401 until verify_jwt is disabled.", "body_md": "*This article was created with the help of AI. Each step was checked against the Stripe docs and the stripe-node library listed at the end.*\n\nHere's why this error matters. A customer pays, Stripe sends your app a webhook, and your app turns it away because the signature check fails. The customer paid and gets nothing. Stripe keeps retrying for a while, but you may not notice until someone writes in, upset. The good news: the cause is almost always one of three small things.\n\n**The error:**\n\n```\nWebhook signature verification failed.\nNo signatures found matching the expected signature for payload.\n```\n\n**Short answer:** Stripe's check uses three things: the raw request body, the `Stripe-Signature` header and your endpoint's signing secret. If any one of them is wrong, the check fails. Stripe says the most common cause is the **wrong secret**.\n\n`whsec_`. It is `sk_...`) and `we_...`).` stripe listen` on your laptop is Stripe signs the exact bytes it sends. If your app turns the body into JSON and back, the bytes change and the check fails. Get the **raw** body.\n\n**Next.js (App Router):**\n\n``` python\n// app/api/webhooks/stripe/route.ts\nimport Stripe from 'stripe'\n\nconst stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)\n\nexport async function POST(req: Request) {\n  const body = await req.text() // raw text. Do not use req.json()\n  const sig = req.headers.get('stripe-signature')\n  if (!sig) return new Response('Missing signature', { status: 400 })\n\n  let event: Stripe.Event\n  try {\n    event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!)\n  } catch (err) {\n    return new Response(`Webhook Error: ${(err as Error).message}`, { status: 400 })\n  }\n\n  // handle event.type here\n  return Response.json({ received: true })\n}\n```\n\n**Express:** put the raw parser on the webhook route, and add `express.json()` **after** it.\n\n``` js\napp.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {\n  let event\n  try {\n    event = stripe.webhooks.constructEvent(req.body, req.headers['stripe-signature'], endpointSecret)\n  } catch (err) {\n    return res.status(400).send(`Webhook Error: ${err.message}`)\n  }\n  // handle event.type here\n  res.json({ received: true })\n})\n\napp.use(express.json()) // other routes only\n```\n\n**Next.js (Pages Router):** turn off the body parser for this route and read the raw bytes.\n\n``` python\n// pages/api/webhook.ts\nimport type { NextApiRequest, NextApiResponse } from 'next'\nimport Stripe from 'stripe'\n\nexport const config = { api: { bodyParser: false } }\nconst stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)\n\nasync function rawBody(req: NextApiRequest) {\n  const chunks: Buffer[] = []\n  for await (const chunk of req) chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk)\n  return Buffer.concat(chunks)\n}\n\nexport default async function handler(req: NextApiRequest, res: NextApiResponse) {\n  if (req.method !== 'POST') return res.status(405).end()\n  const sig = req.headers['stripe-signature'] as string\n  try {\n    const event = stripe.webhooks.constructEvent(await rawBody(req), sig, process.env.STRIPE_WEBHOOK_SECRET!)\n    // handle event.type here\n    return res.json({ received: true })\n  } catch (err) {\n    return res.status(400).send(`Webhook Error: ${(err as Error).message}`)\n  }\n}\n```\n\n**Supabase Edge Functions (Deno):** use `await req.text()`, and use the async version with Stripe's crypto provider:\n\n``` js\nconst cryptoProvider = Stripe.createSubtleCryptoProvider()\nconst event = await stripe.webhooks.constructEventAsync(body, signature, secret, undefined, cryptoProvider)\n```\n\nThe plain `constructEvent` fails there with \"SubtleCryptoProvider cannot be used in a synchronous context\". Supabase also blocks Stripe with a 401 by default until you set `verify_jwt = false` for the webhook function.\n\nSomething in front of your app can also change the body, such as a proxy or a forwarding tool. Even a small change, like extra spaces in the JSON, breaks the check.\n\nPrint the `Stripe-Signature` header once. It should look like `t=...,v1=...`. If it is empty, your code is reading the wrong header, or something in between removed it.\n\nStripe's libraries also reject events whose timestamp is more than 5 minutes off from your server's clock (the default). If you replay a saved request later, it will fail. Send a fresh test event instead, and make sure your server's clock is correct.\n\nOnce real events pass, check the other side: your endpoint must say no to a bad signature. Try it on your own endpoint:\n\n```\ncurl -i -X POST https://yourapp.com/api/webhooks/stripe \\\n  -H 'Content-Type: application/json' \\\n  -H 'Stripe-Signature: t=1,v1=bad' \\\n  -d '{}'\n```\n\nYou want a 4xx, such as 400. If you get a 2xx, your route is not checking the signature.\n\n`constructEvent`, `constructEventAsync`): More plain-English webhook fix guides: [https://tillping.online/fix?ref=dev](https://tillping.online/fix?ref=dev)\n\nTillping is a small tool that checks the payment-webhook path of your app, so a paying customer isn't left with nothing while you hear nothing. It isn't open yet. If you'd like a heads-up when it is, join the early-bird list: [https://tillping.online/?ref=dev](https://tillping.online/?ref=dev)", "url": "https://wpnews.pro/news/stripe-webhook-signature-verification-failed-3-things-to-check", "canonical_source": "https://dev.to/tillping-hq/stripe-webhook-signature-verification-failed-3-things-to-check-174o", "published_at": "2026-11-02 13:16:39+00:00", "updated_at": "2026-11-02 13:23:48.966776+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Stripe", "Next.js", "Express", "Supabase", "Deno"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/stripe-webhook-signature-verification-failed-3-things-to-check", "markdown": "https://wpnews.pro/news/stripe-webhook-signature-verification-failed-3-things-to-check.md", "text": "https://wpnews.pro/news/stripe-webhook-signature-verification-failed-3-things-to-check.txt", "jsonld": "https://wpnews.pro/news/stripe-webhook-signature-verification-failed-3-things-to-check.jsonld"}}