Webhook signature verification failed
appears when:When the request body is parsed or mutated before the signature check, or the wrong webhook secret is configured for the environment
Webhook signature verification failed
Stripe signs the raw bytes. If your framework parses JSON first, the signature will never match — even if the payload is byte-identical to what Stripe sent.
await req.text() first, pass that string to constructEvent(body, signature, secret). Do not call req.json(), do not rely on body-parser middleware, and verify the STRIPE_WEBHOOK_SECRET matches the mode (test vs live) of the endpoint in the Stripe dashboard.Quick fix for webhook signature verification failed
01// app/api/webhooks/stripe/route.ts — App Router raw body pattern02import { NextRequest, NextResponse } from "next/server";03import Stripe from "stripe";04 05const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);06 07export async function POST(req: NextRequest) {08 const body = await req.text(); // raw bytes as string — never req.json()09 const signature = req.headers.get("stripe-signature");10 11 if (!signature) {12 return NextResponse.json({ error: "missing signature" }, { status: 400 });13 }14 15 try {16 const event = stripe.webhooks.constructEvent(17 body,18 signature,19 process.env.STRIPE_WEBHOOK_SECRET!20 );21 // handle event.type here22 return NextResponse.json({ received: true });23 } catch (err) {24 console.error("webhook signature verification failed", err);25 return NextResponse.json({ error: "invalid signature" }, { status: 400 });26 }27}Deeper fixes when the quick fix fails
01 · Pages Router: disable body parsing for the webhook route
01// pages/api/webhooks/stripe.ts02export const config = {03 api: {04 bodyParser: false,05 },06};07 08import { buffer } from "micro";09import Stripe from "stripe";10 11const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);12 13export default async function handler(req, res) {14 if (req.method !== "POST") return res.status(405).end();15 16 const buf = await buffer(req);17 const signature = req.headers["stripe-signature"] as string;18 19 try {20 const event = stripe.webhooks.constructEvent(21 buf,22 signature,23 process.env.STRIPE_WEBHOOK_SECRET!24 );25 res.json({ received: true });26 } catch (err) {27 res.status(400).send(`webhook error: ${(err as Error).message}`);28 }29}02 · Clerk / Svix webhook verification
01// app/api/webhooks/clerk/route.ts02import { Webhook } from "svix";03import { headers } from "next/headers";04 05export async function POST(req: Request) {06 const body = await req.text();07 const h = await headers();08 const svixHeaders = {09 "svix-id": h.get("svix-id")!,10 "svix-timestamp": h.get("svix-timestamp")!,11 "svix-signature": h.get("svix-signature")!,12 };13 14 const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!);15 try {16 const event = wh.verify(body, svixHeaders);17 return Response.json({ received: true });18 } catch {19 return Response.json({ error: "invalid signature" }, { status: 400 });20 }21}03 · Regression test with a signed fixture
01// tests/webhook.test.ts02import { POST } from "@/app/api/webhooks/stripe/route";03import Stripe from "stripe";04 05test("rejects tampered payload", async () => {06 const payload = JSON.stringify({ id: "evt_test", type: "ping" });07 const secret = "whsec_test";08 const signature = Stripe.webhooks.generateTestHeaderString({ payload, secret });09 10 const req = new Request("http://x", {11 method: "POST",12 headers: { "stripe-signature": signature },13 body: payload + "tampered",14 });15 16 const res = await POST(req as never);17 expect(res.status).toBe(400);18});Why AI-built apps hit webhook signature verification failed
Every major webhook provider — Stripe, Clerk via Svix, GitHub, Shopify — signs the raw bytes of the request body using HMAC-SHA256 with a shared secret. The signature lives in a header. Your server recomputes the HMAC over the body it received and compares. If the two hashes match, the event is authentic. If any byte differs, verification fails.
The failure mode that trips up AI-generated code is body handling. When a Cursor or Lovable scaffold writes a webhook handler, it often reaches for req.json() because that is the pattern used everywhere else in the app. That call reads the stream, parses the JSON, and discards the original bytes. The handler then serializes the parsed object back to a string to pass into constructEvent. The re-serialization reorders keys, changes whitespace, drops trailing newlines. The hash no longer matches what Stripe signed. Verification fails on every request, and the log message is identical to a real attack — so the developer assumes their secret is wrong and rotates it, which does not help.
The second failure mode is secret drift. Stripe shows you a whsec_ secret once when you create the endpoint. If you create a test endpoint and a live endpoint, those are two different secrets. If your production env is pointing at the test secret, or vice versa, the verification fails for real events. Every webhook audit we run finds at least one env variable pointing at the wrong mode's secret.
The third failure mode is middleware. In Pages Router, Next.js parses bodies by default. In App Router, middleware.ts can mutate requests. If anything upstream of your handler reads or modifies the body, the bytes reaching the signature check are no longer what Stripe signed. The fix is always the same: guarantee the raw bytes arrive untouched.
webhook signature verification failed by AI builder
How often each AI builder ships this error and the pattern that produces it.
| Builder | Frequency | Pattern |
|---|---|---|
| Lovable | Every Stripe integration | Uses req.json() then re-stringifies for verification |
| Bolt.new | Common | Hard-codes test whsec_ in production env |
| Cursor | Common | Adds body parsing middleware that mutates payload |
| Base44 | Sometimes | Skips verification entirely in development mode |
| Replit Agent | Rare | Confuses stripe-signature header case |
Related errors we fix
Stop webhook signature verification failed recurring in AI-built apps
- →Always use await req.text() in App Router webhook handlers — never req.json().
- →Store separate env variables per mode: STRIPE_WEBHOOK_SECRET_TEST and STRIPE_WEBHOOK_SECRET_LIVE.
- →Ship a signed-fixture regression test that runs in CI on every PR.
- →Never let middleware.ts match webhook paths — exclude /api/webhooks/* from the matcher.
- →Log the first 200 bytes of the body on verification failure so you can diff against Stripe's sent payload.
Still stuck with webhook signature verification failed?
webhook signature verification failed questions
Why does Stripe signature verification fail in Next.js App Router?+
My webhook worked in dev but fails in production — what changed?+
Does Clerk webhook signing work the same way?+
Why would body-parser break my webhook?+
How long does an Afterbuild Labs webhook audit take?+
Ship the fix. Keep the fix.
Emergency Triage restores service in 48 hours. Break the Fix Loop rebuilds CI so this error cannot ship again.
Hyder Shah leads Afterbuild Labs, shipping production rescues for apps built in Lovable, Bolt.new, Cursor, Replit, v0, and Base44. our rescue methodology.