Tillping
Menu

Fix guides

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 listen only works with stripe 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:

  • http vs https (live mode needs HTTPS)
  • www. vs no www.
  • 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

Run the free check

Paste your webhook address and see what your endpoint answers. No account. No keys.

Check my webhook