Teitunnel

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 ones

4. 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 stripe

The 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 3

In 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:

  1. 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.
  2. 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 stripe

If 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.com yourself 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.com unless you turn it on there.
  • Signature errors: see step 4.
  • Slow handlers: providers time out after a few seconds. Answer 200 right 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

Edit on GitHub

Last updated on

On this page