Stripe Webhooks Not Firing After Deploy? Here's How to Fix It
Payments succeed but orders never complete? The 5 reasons Stripe webhooks stop firing after you deploy — and how to fix each one.
Your checkout works. Stripe shows the payment as succeeded. But the order never completes — no confirmation email, no subscription activated, no database record. Locally, everything worked.
This is the single most common failure I see in AI-built (Lovable, Bolt, v0, Cursor) apps that just went live. The good news: it's almost always one of five causes.
1. The webhook endpoint points at the wrong URL
Stripe doesn't know you deployed. If you created the webhook endpoint during development, it's probably still pointing at a localhost tunnel or a preview URL.
Fix: In the Stripe Dashboard → Developers → Webhooks, check the endpoint URL. It must be your production domain, e.g. https://yourapp.com/api/stripe/webhook — not localhost, not an *.vercel.app preview URL that changes on every deploy.
2. You're using the wrong webhook signing secret
Each webhook endpoint has its own signing secret (whsec_...). The secret from your local Stripe CLI (stripe listen) is different from the one for your production endpoint. If your deployed app still has the CLI secret, every event fails signature verification with a 400.
Fix: Copy the signing secret from the production endpoint in the Stripe Dashboard and set it as STRIPE_WEBHOOK_SECRET in your hosting provider's environment variables (e.g. Vercel → Project → Settings → Environment Variables). Then redeploy — env var changes don't apply to already-built deployments.
3. Test mode vs. live mode mismatch
Test-mode webhooks only receive test-mode events, and live-mode webhooks only receive live-mode events. AI-generated code frequently ships with the test key (sk_test_...) in production.
Fix: Make sure all three match the same mode: your secret key (sk_live_...), your publishable key, and the webhook endpoint (created while the dashboard was in live mode).
4. The request body is being parsed before verification
Stripe signs the raw request body. If your framework parses the body into JSON before the signature check, verification fails every time. This bites Next.js, Express (express.json()), and most AI-generated handlers.
Fix (Next.js App Router): read the raw text, then verify:
export async function POST(req: Request) {
const body = await req.text(); // raw body — do NOT use req.json()
const sig = req.headers.get("stripe-signature")!;
const event = stripe.webhooks.constructEvent(
body,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
// handle event…
return new Response(null, { status: 200 });
}
5. The handler returns an error, so Stripe retries — then gives up
If your handler throws (missing env var, database not reachable from production, RLS blocking the insert), Stripe records failed deliveries and eventually disables the endpoint.
Fix: Open the endpoint in the Stripe Dashboard and look at the recent deliveries log — it shows the exact HTTP status and response body your server returned. That error message is usually the real root cause (often a Supabase insert failing because row-level security is enabled but no policy allows the service role — see my Supabase RLS guide).
Still stuck?
Webhook failures are rarely just webhook failures — they're usually a symptom of environment variables, database policies, or deploy configuration being broken at the same time. That's exactly what my fixed-price triage finds: a full audit of your app with a video walkthrough and prioritized fix list, in 48 hours, for $299.
Stuck? I'll fix it for a fixed price.
I'm a senior full-stack engineer who audits, fixes, hardens, and deploys broken Lovable, Bolt, Cursor, v0, and Replit apps. Fixed price — diagnose before I touch a line of code, so you stop burning credits on prompts that make it worse.
Vibe-Code Rescue — from $299