afterbuild/ops
ERR-498/Stripe · webhook
ERR-498
No signatures found matching the expected signature

appears when:After the app is deployed to Vercel and a live charge fails to update the database

Stripe webhook not firing

Dashboard shows no delivery or a red 'failed' badge. Cause is almost always the raw-body parse, a test-mode secret in production, or an endpoint URL still pointing at localhost.

Last updated 17 April 2026 · 6 min read · By Hyder Shah
Direct answer
Stripe webhook not firing is almost always one of three causes: endpoint URL wrong in the Stripe dashboard, STRIPE_WEBHOOK_SECRET missing in production env, or the Next.js route parsed JSON before signature verification. Use await request.text(), pass the raw string to stripe.webhooks.constructEvent, and confirm the live-mode endpoint is registered in the Stripe dashboard. Return 200 in under a second.

Quick fix for Stripe webhook not firing

app/api/stripe/webhook/route.ts
typescript
01// app/api/stripe/webhook/route.ts02import Stripe from "stripe";03import { NextRequest } from "next/server";04 05const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);06const secret = process.env.STRIPE_WEBHOOK_SECRET!;07 08export async function POST(req: NextRequest) {09  const body = await req.text(); // raw string, NOT .json()10  const sig = req.headers.get("stripe-signature")!;11 12  let event: Stripe.Event;13  try {14    event = stripe.webhooks.constructEvent(body, sig, secret);15  } catch (err) {16    return new Response(`Webhook Error: ${(err as Error).message}`, { status: 400 });17  }18 19  // Acknowledge fast, process async20  queueEvent(event); // push to Inngest / QStash / your queue21  return new Response("ok", { status: 200 });22}
Paste this into your webhook route — await req.text() preserves the raw body for signature verification

Deeper fixes when the quick fix fails

01 · Verify the endpoint URL is registered in live mode

Stripe dashboards are toggled between test and live. A webhook created while the toggle was on Test will not receive live events. Switch the toggle to Live mode → Developers → Webhooks → Add endpoint. Use the exact production URL. Copy the new whsec_ secret into Vercel and redeploy.

02 · Drop edge runtime from the webhook route

app/api/stripe/webhook/route.ts
typescript
01// ❌ DO NOT USE edge runtime for Stripe webhooks02// export const runtime = "edge";03 04// Stay on the default Node.js runtime so the Stripe SDK works05// (no directive needed)
Edge runtime strips body parsing and the Node crypto API — required by Stripe

03 · Add idempotency keyed on event.id

Stripe replays events on retry. Without an idempotency check your database sees the same subscription twice. Store event.id in a Postgres table with a unique constraint; short-circuit if the row already exists.

app/api/stripe/webhook/route.ts
typescript
01const { error } = await supabase02  .from("stripe_events")03  .insert({ id: event.id })04  .select()05  .single();06 07if (error?.code === "23505") {08  // already processed09  return new Response("duplicate", { status: 200 });10}

Why AI-built apps hit Stripe webhook not firing

AI builders treat Stripe as a scaffolding checklist — one route handler, one test charge, one success toast. The shape works in preview because every preview hits the test-mode webhook that was registered during the onboarding wizard. The bug is silent because test-mode traffic never reveals the live-mode gap.

Vercel compounds the issue. Environment variables are scoped per environment, and NEXT_PUBLIC_ values bake into the bundle at build time. A founder who updates Preview but not Production ships a half-configured integration. Stripe Node SDK requires the Node runtime, so an inherited export const runtime = "edge" on the webhook route also breaks signature verification.

The fix pattern is always the same: register the live endpoint, scope the secret to Production, read the body raw, acknowledge in under a second, and enqueue the real work. Deploy once with that shape and Stripe webhook not firing stops recurring.

Stripe webhook not firing by AI builder

How often each AI builder ships this error and the pattern that produces it.

AI builder × Stripe webhook not firing
BuilderFrequencyPattern
LovableEvery Stripe scaffoldCalls request.json() before verify
Bolt.newEvery Stripe scaffoldEdge runtime default breaks Node crypto
v0CommonShips test-mode secret, never creates live endpoint
CursorSometimesGenerates Express-style middleware that strips body
Replit AgentCommonOmits STRIPE_WEBHOOK_SECRET from deploy checklist
Base44RareSwallows verification error with generic 500

Related errors we fix

Stop Stripe webhook not firing recurring in AI-built apps

Still stuck with Stripe webhook not firing?

Emergency triage · $299 · 48h turnaround
We restore service and write the root-cause report.
start the triage →

Stripe webhook not firing questions

Why is my Stripe webhook not firing in production?+
Three common causes: a wrong endpoint URL registered in the Stripe dashboard, a missing STRIPE_WEBHOOK_SECRET in production env, or a webhook created in test mode while the app runs live. Check Stripe → Developers → Webhooks first. A red 'failed' badge confirms delivery is rejected; no events at all means the URL is wrong or the endpoint never runs.
How do I know if Stripe is even trying to send the webhook?+
Stripe Dashboard → Developers → Webhooks → your endpoint → 'Recent deliveries'. Each attempt shows the HTTP status your server returned. 404 = URL wrong. 400 = signature verification failed. 500 = handler crashed. No entries means Stripe never fired an event — wrong event type or test-mode/live-mode mismatch.
What does 'No signatures found matching the expected signature' mean?+
stripe.webhooks.constructEvent throws this when the raw request body was altered before verification. Next.js App Router, Express body-parser, and most frameworks parse JSON by default — that breaks the signature. In Next.js 16 App Route Handlers use await request.text(), never await request.json().
Do I need separate Stripe webhook secrets for test and live?+
Yes. Each endpoint in the Stripe dashboard has its own signing secret, and test-mode and live-mode endpoints are separate. Create one endpoint in test with a whsec_test secret, and another in live with a whsec_live secret. Store them in different Vercel scopes so preview hits test and production hits live.
Can a Vercel cold start cause the webhook to fail?+
Cold starts rarely cause outright failure but can trip Stripe's 10-second timeout on a first invocation. If the Stripe dashboard shows 'failed — timeout', move slow work out of the handler. Acknowledge with 200 immediately then enqueue processing via Inngest, QStash, or a DB queue. Never run a 5-second API call synchronously inside a Stripe webhook handler.
Next step

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.

About the author

Hyder Shah leads Afterbuild Labs, shipping production rescues for apps built in Lovable, Bolt.new, Cursor, Replit, v0, and Base44. our rescue methodology.

Sources