fix guide
Stripe webhook not working in production
Harjot · · updated
Payments work in test, and webhooks reached your app through the Stripe CLI. On the live site nothing happens after a payment: no order, no upgraded account. Or your logs show Webhook signature verification failed.
Start in Stripe. In Workbench, open the Webhooks tab, select your endpoint, then open the Event deliveries tab. Each event shows as Delivered, Pending or Failed, and the HTTP status code of the attempt. That code narrows down which of these five checks to start with.
1. The secret is from the Stripe CLI
Stripe's docs say the most common cause of a failed signature check is the wrong endpoint secret. The secret for a CLI-forwarded endpoint is different from the secret for a Dashboard endpoint. Both start with whsec_, so they look alike.
The CLI secret is the one printed when you run stripe listen. The live site needs the one from your Dashboard endpoint. In Workbench, open the Webhooks tab, select the endpoint and click Reveal secret. Put that value in your live STRIPE_WEBHOOK_SECRET.
This is the error Stripe's docs show for a mismatch:
Webhook signature verification failed. Err: No signatures found matching the expected signature for payload.signs Event deliveries shows failed deliveries, and your logs show the error above.
2. Test mode and live mode use different secrets and keys
Stripe's docs say that if you use the same endpoint for both test and live API keys, the secret is different for each one. Objects in one mode also aren't accessible to the other.
Check which mode you were in when you copied the secret. Switch Stripe to live mode, open the endpoint for your live site URL, reveal its secret and put that in your live STRIPE_WEBHOOK_SECRET. If live mode has no endpoint yet, create one there.
The live site also needs live API keys. Stripe's docs say to keep them in a secrets vault, or in environment variables if you have no vault.
signs Test payments reach your app and live payments don't. Live mode shows no endpoint, or a different one.
3. The variable isn't set for Production
The live site only has the variables you gave it on the host. Open the project in Vercel, go to Settings, then Environment Variables. Check that STRIPE_WEBHOOK_SECRET and your Stripe secret key are ticked for Production.
Vercel's docs say changes to environment variables apply only to new deployments, not to previous ones. Push a commit to the production branch or run vercel --prod after you edit them.
For the general version of this problem, see Environment variable undefined in production on Vercel.
signs The secret shows in your local .env file, and the variable is missing from Vercel's Production list or was added after the last deploy.
4. The body was parsed before verification
Stripe's docs say it needs the raw request body to check the signature, and that the body must be the exact string Stripe sent. They warn that some frameworks parse or change the body first, and any change makes the check fail.
Next.js App Router: read the body with request.text() in the route handler. Stripe's App Router example does this. Next.js's route handler docs say you don't need a bodyParser config, unlike Pages Router. Pass that string to constructEvent. Do not call request.json() first.
const body = await request.text();
const signature = request.headers.get("stripe-signature") as string;
const event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET as string);Next.js Pages Router: Stripe's docs say to disable bodyParser and read the body with buffer(request).
Express: Stripe's docs say app.use(express.json()) must come after your webhook route, because Express runs middleware in order.
signs The secret is right and the signature check still fails. The code reads the body as JSON before it calls constructEvent.
5. The endpoint URL redirects or doesn't match your app
Stripe counts a redirect as a failed delivery. Its docs say a 3xx status means the destination server tried to redirect the request, and the fix is to set the endpoint to the URL the redirect resolves to.
Compare the URL in Stripe with your live site, character by character. A www address that redirects to the bare domain is one cause. So is a trailing slash: Next.js's docs say that by default it redirects URLs with a trailing slash to the version without one, so /api/webhook/ redirects to /api/webhook.
Stripe also requires an HTTPS URL for live mode, and the route has to accept POST requests and be public. Stripe's docs list a 404 for an address that doesn't exist and 401, 403 or 405 for access limits.
signs Event deliveries shows a 3xx status, or a 404, on the attempts.
Test it like a new user
- Fix one thing, redeploy, then open the failed event in Workbench and click Resend. Stripe's docs say this works for up to 15 days after the event.
- Check the delivery now shows Delivered with a 200 status code.
- Make one real purchase on the live site with a small amount, or one that you refund.
- Check your app did what it should: order saved, account upgraded, email sent.
- Open Event deliveries and confirm no event is stuck on Pending or Failed.
Common questions
Why does Stripe say no signatures found matching the expected signature?
Usually the endpoint secret is wrong. The secret printed by stripe listen differs from the one on your Dashboard endpoint, and test mode and live mode each have their own. Reveal the secret on the live endpoint and put it in your production environment variable.
Why do Stripe webhooks work locally but not on Vercel?
Usually the live webhook secret is wrong or missing. Also check that the variable is set for Production and redeployed, that the code reads the raw body before verifying, and that the endpoint URL doesn't redirect. Event deliveries in Stripe Workbench shows the status code for each attempt.
How do I resend a failed Stripe webhook after fixing it?
Open the failed event in Workbench, under Event deliveries for your endpoint, and click Resend. Stripe's docs say this works for up to 15 days after the event. Check the delivery then shows Delivered with a 200 status code.
Still stuck?
Not sure this is the cause? Paste your app's link into the free scan. It lists what a visitor's browser can see, like leaked keys and exposed files, worst first. The score is free; every fix as a prompt for your builder is $9, one-time.
Want a person to look inside? See the diagnosis