Use cases
Test webhooks on localhost
Receive Stripe, GitHub, Slack, Shopify or Twilio webhooks on your computer through Cloudflare Tunnel, at a stable HTTPS URL, then inspect, check and replay every delivery.
Webhook providers need a public HTTPS URL, but your handler runs on localhost. In this
tutorial you give a local handler a URL that stays the same every day, point Stripe at it,
watch a delivery arrive, check its signature, and replay it while you fix the handler.
The same steps work for GitHub, Slack, Shopify, Twilio and others.
For one provider in detail, with its limits and retries: Stripe, GitHub, Shopify and Slack.
You need: Teitunnel, a Cloudflare account with a domain
(first route), and a webhook handler
you can run. The examples use a handler at http://localhost:4000/webhooks/stripe and the
hostname hooks.yourhost.com.
1. Get a stable URL
A random trycloudflare.com Quick Share works for a first try, but its URL changes every
time, so you'd update the provider each session. Share on a subdomain you keep for webhooks
instead:
teitunnel share 4000 --on hooks.yourhost.com --watch "/webhooks/*"In the app: Quick Share, type 4000, Address ▸ On yourhost.com, Subdomain
hooks, then Share.
https://hooks.yourhost.com now reaches localhost:4000 for as long as the share runs, and
it's the same address tomorrow. --watch prints a line whenever a request hits that path, so
you notice deliveries even with a busy terminal. Every request goes through Teitunnel's
inspector, which records it with its response, credentials
masked.
2. Point the provider at it
In the Stripe dashboard, in test mode, Developers ▸ Webhooks ▸ Add endpoint, with
https://hooks.yourhost.com/webhooks/stripe. Copy the endpoint's Signing secret
(whsec_…) into your app and verify signatures with it.
3. Watch a delivery arrive
Send a test event from the provider (GitHub sends a ping as soon as you add the webhook).
The terminal prints the request with its status and duration.
In the app, open Inspector: the request is at the top of the list, marked Webhook. Select it to see the headers, the body, your handler's answer and the timings. From a terminal:
teitunnel traffic ls --path /webhooks --last 5
teitunnel traffic get 7f3a # the end of an id, as ls shows it
teitunnel traffic get 7f3a --format curl # the same request as a curl command
teitunnel traffic watch --path /webhooks # follow new ones4. Check the signature
Save the provider's signing secret once, and the inspector checks every delivery's signature against it: Signature valid, Signature invalid, Signature expired (signed too long before it arrived), or Not checked.
In the app, select the request and choose Add Signing Secret… under Webhook. From a terminal (read from standard input, never from the command line):
teitunnel inbox secret hooks.yourhost.com stripeThe secret is kept in your system's keychain and never shown again. The inspector knows Stripe, GitHub, Slack, Shopify, Standard Webhooks (Svix, Clerk, Resend…), Twilio, Linear and Discord.
If the inspector says the signature is valid but your handler says it isn't, your framework is probably parsing the body before verification. Give the handler the raw request body.
5. Fix the handler and replay
You don't need the provider to send the event again. Change your code, then send the same request to your service:
teitunnel traffic replay 7f3a
teitunnel traffic replay 7f3a --set-header "X-Debug: 1" --times 3In the app: Replay, or Edit and Replay… to change the method, path, headers or body
first. Headers you leave alone keep their captured values, masked ones included, while the
app still has the request in memory. A request read back from the history (after a restart,
or teitunnel traffic replay in another terminal) is replayed without its masked headers
("From history: masked headers are left out of the replay."), so its signature header is
missing. Repeat until the handler answers 200.
Providers that sign a timestamp (Stripe, Standard Webhooks) usually accept a signature for only about 5 minutes, so a later replay fails your handler's check. In Edit and Replay…, turn on Re-sign: with the signing secret saved in step 4, each replay gets a fresh, valid signature. Or send a fresh test event from the provider.
6. Keep receiving when you're away
A share ends when you stop it. For a handler that should always receive:
- Stop the share (Ctrl-C), then make it a route:
teitunnel route add hooks.yourhost.com 4000, and turn on Keep running when Teitunnel quits in Tunnels so it's up whenever your computer is. - Add a webhook inbox: webhooks sent while your computer is off are kept on your Cloudflare account and delivered in order when it's back.
teitunnel inbox add hooks.yourhost.com /webhooks/ --verify stripeIf deliveries fail
- The provider says 502: your handler isn't running on that port. 530 or error
1033: the share stopped or the computer is off. Open
https://hooks.yourhost.comyourself to check. - Cloudflare blocks the requests: if your domain has Bot Fight Mode, I'm Under
Attack mode or strict WAF rules, Cloudflare may challenge webhook deliveries, which can't
solve a challenge. Add a WAF skip rule for the hostname, or use a subdomain on a domain
without them. Teitunnel's own edge protection is per hostname,
so it doesn't affect
hooks.yourhost.comunless you turn it on there. - Signature errors: see step 4.
- Slow handlers: providers time out after a few seconds. Answer
200right away and do the work afterwards.
Keep it private
Anyone who knows the URL can post to it, so always verify the provider's signature. Don't
put a login in front of a webhook route: providers can't sign
in. A webhook inbox with --verify keeps only correctly signed webhooks.
Where to go next
Last updated on
Share a Vite, Next.js, Django or Rails dev server
Fix "Blocked request. This host is not allowed", "Invalid Host header", broken hot reload and CSRF errors when sharing a dev server through Cloudflare Tunnel.
Stripe webhooks on localhost
Receive Stripe webhooks on your computer at a stable HTTPS URL, check the Stripe-Signature header, replay deliveries with a fresh signature, and keep events while you're offline.