Your Stripe webhook works on your laptop but fails in production
Short answer: On your laptop, stripe listen gives you its own secret and sends events straight to your code. In production, Stripe calls your public address and uses a different secret. Most failures come from that secret, or from something that stops Stripe before your code runs.
Step 1: Read what Stripe saw
In the Stripe Dashboard, open Developers → Webhooks, pick your endpoint and open Event deliveries. The status code tells you where to look:
| Stripe shows | What it means | Go to |
|---|---|---|
| 400 with a signature error | Your code ran, but the check failed | Step 2 |
| 401 or 403 | Something blocked Stripe before your code | Step 3 |
| 301, 302, 307 or 308 | Your address redirects. Stripe counts that as a failure | Step 4 |
| 404 or 405 | Wrong path, or the route doesn't accept POST | Step 4 |
| 500 | Your code crashed | Read your server logs |
| Timed out | Your code is too slow to reply | Step 5 |
| TLS error | Problem with your HTTPS certificate | Step 4 |
Step 2: Use the production secret, then deploy again
- The secret from
stripe listenonly works withstripe listen. In production, use the secret of the endpoint you added in the Dashboard. Click Reveal secret on that endpoint. - Test mode and live mode have different endpoints and secrets. A live endpoint needs the live secret.
- Put it in your host's Production environment variables, not only Preview or Development.
- Deploy again. On Vercel, changes to environment variables only apply to new deployments.
- Also make sure the route reads the raw body. See our "signature verification failed" guide.
Step 3: Look for a login wall
Some hosts protect your app from outside visitors. Stripe is an outside visitor.
- Vercel: Deployment Protection can block preview URLs (and sometimes more). Point Stripe at your production domain. If you must use a protected URL, Vercel's docs show adding a bypass secret to the webhook URL as a query parameter:
https://your-app.vercel.app/api/webhook?x-vercel-protection-bypass=YOUR_SECRET - Supabase Edge Functions: the default JWT check returns 401 to Stripe. See our Supabase 401 guide.
- Cloudflare or other firewalls: a bot check or challenge page can block Stripe. Allow your webhook path through.
- Your own middleware: if it sends logged-out users to a login page, let the webhook route skip it.
Step 4: Use the exact final address
Stripe does not follow redirects. Copy the exact address that answers without one. Watch for:
httpvshttps(live mode needs HTTPS)www.vs nowww.- a slash at the end, or none
Test with:
curl -i -X POST https://yourapp.com/api/webhook
Look at the first line. A 3xx with a location: header means a redirect. Use the address in location: instead. A 400 from your own code (because the test has no signature) is a good sign. It means Stripe can reach your code.
Stripe also needs a valid certificate and TLS 1.2 or 1.3.
Step 5: Reply fast
Return a 2xx as soon as you've checked the signature and saved what you need. Do slow work after. Stripe's docs say to defer complex logic and reply right away.
After the fix
- Stripe retries failed live events for up to three days. In a sandbox it retries three times over a few hours.
- You can also click Resend on an event in the Dashboard (works for up to 15 days).
- Make your handler safe to run twice for the same event. Retries can send it again.
Check it with a free scan
Paste your production webhook address into the free Tillping scan: https://tillping.online/#scan. No account is needed. Only scan an address you own or are allowed to test.
It sends a few safe test requests and looks at the answers. It can flag:
- a redirect (3xx), and where the test ended up
- a Vercel login wall, a Supabase JWT block, or a Cloudflare bot challenge
- 404, 405 and 5xx errors
- no answer at all (bad host name, refused, or no reply in about 8 seconds)
- a slow reply (3 seconds or more)
- a bad certificate, or one that ends within 14 days
- an endpoint that says OK to a fake signature
It never asks for your secret, so it can't check Step 2 for you.
Sources
- Stripe, Webhooks (status code table, retries, HTTPS and TLS): https://docs.stripe.com/webhooks
- Stripe, Signature errors: https://docs.stripe.com/webhooks/signature
- Vercel, Protection Bypass for Automation: https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection/protection-bypass-automation
- Vercel, Environment variables: https://vercel.com/docs/environment-variables
Run the free check
Paste your webhook address and see what your endpoint answers. No account. No keys.
Check my webhook