§ 05 The traps
What goes wrong only on your laptop
01
You registered http://localhost:3000
The provider's delivery log: connection refused · timed out · invalid URL
The provider resolves localhost on its server, so it calls itself. Some providers reject such URLs outright. Register the tunnel's public URL, or use a CLI that needs no URL. Why localhost works this way: whatislocalhost.com.
02
The signature fails, but only here
No signatures found matching the expected signature for payload · 401 · 400 Bad signature
Three usual causes, in order of likelihood:
- The body was parsed before you checked it. Middleware like
express.json() turns the bytes into an object; writing it back out doesn't reliably give the same bytes. Give the webhook route the raw body.
- The wrong secret. Events forwarded by
stripe listen are signed with the secret it prints, not the dashboard endpoint's. Sandbox and live secrets differ too.
- A relay re-encoded the body. See smee.io above. Capture the same event in the tester and check the signature there: if it passes there and fails in your app, the bytes changed on the way.
03
Every event arrives twice (or five times)
The same event ID in your logs, again and again, while you debug
A breakpoint holds the request open, and the provider gives up after its time limit: 3 seconds for Slack, 5 for Shopify, 10 for GitHub. It marks the delivery failed and, except for GitHub, sends it again. Answer 200 before you do any slow work, and record which event IDs you've handled so a retry does nothing. Dedupe keys for each provider →
04
It worked yesterday
Deliveries fail with 404 or DNS errors after a restart
Your tunnel's URL changed. Cloudflare Quick Tunnels pick a new hostname every run, and so do tunnels on plans without a fixed domain. Re-register the new URL, use a tunnel with a stable domain, or deploy the receiver so the URL stops depending on your laptop.
05
Your app redirects the webhook
301 · 302 · 308 in the delivery log
A redirect from http to https, or from /webhook to /webhook/, is a failure to the provider: Stripe counts redirects as failed deliveries. Register the exact final URL. More on redirects →
06
The framework rejects the POST
403 Forbidden · CSRF verification failed · InvalidAuthenticityToken
Rails, Django, Laravel and others protect POST routes with CSRF tokens that a provider can't have. Exempt the webhook route, and rely on the signature check instead. CSRF, logins and firewalls →
07
Forgetting the tunnel is public
Requests in your logs that no provider sent
Anyone who finds the URL can call your laptop. Verify signatures in development too, don't serve admin pages on the same port, and stop the tunnel when you're done.