# https://tunnels.io/learn/how-to-test-webhooks-locally

[Home](https://tunnels.io/) / [Learn](https://tunnels.io/learn) / How to test webhooks locally

Learn

# How do you test webhooks locally?

Run your app on a local port, start a tunnel that gives that port a public HTTPS URL, then register that URL as the webhook endpoint at the provider. Deliveries travel over the internet, down the tunnel, and into your local handler, so you can set a breakpoint on a real payload without deploying anything.

## Why can a webhook not reach localhost?

Because `localhost` is not an address anyone else can route to. It resolves to `127.0.0.1`, which means *this machine*, so a provider that tried it would be calling itself. Your laptop also sits behind NAT on a private address such as `192.168.1.24`, which is not routable on the public internet, and your router will not accept an inbound connection that nothing asked for.

Nothing is blocking the provider. It simply has no address to send the request to. A tunnel fixes that by giving the outside world a public name, while the connection that carries traffic is opened outward from your machine.

## How do you actually set it up?

Three steps. Start your app on a local port, put a public HTTPS URL in front of that port, then paste that URL plus your handler's path into the provider's webhook settings. From that moment the provider's real events arrive at your local process.

```
# 1. your app is already listening locally
$ npm run dev
listening on http://localhost:3000
# 2. give that port a public HTTPS URL
$ tunnels http 3000
Active Tunnels:
▸ https://k7m2p9x4qw8n3vzt.tunnels.host → http://localhost:3000  [https]
# 3. register the full endpoint at the provider, path included
#    https://k7m2p9x4qw8n3vzt.tunnels.host/webhooks/stripe
```

Some providers ship a CLI that forwards events to a local port over their own outbound connection. That is a good tool for that one provider and does nothing for the other four you are integrating. A tunnel is provider-agnostic: anything that can reach the internet can reach your handler, including a colleague, a phone on cellular data, or a curl from a build server.

## Do you need a stable URL?

For a debugging session, no. A random hostname is fine while you are sitting there watching requests land. For anything you register once and leave registered, yes, because a random name is released when the client disconnects and the next session gets a different one.

The failure is quiet. The provider keeps delivering to a hostname that is no longer yours, and you notice through missing events rather than an error on your screen. The rule of thumb: reserve a name as soon as something other than a human is remembering your URL. Our [guide to reserved subdomains](https://tunnels.io/learn/what-is-a-reserved-subdomain) covers what that does and does not buy you.

## How do you verify a webhook signature locally?

Exactly as you would in production, using the signing secret the provider issued for that specific endpoint. The signature is computed over the raw request body, so your handler has to read the bytes before any JSON middleware parses and re-serializes them; a re-serialized body is a different byte string and the check fails even though the payload looks identical.

If verification fails only when the request arrives through a tunnel, the two usual causes are that middleware, and using a signing secret that belongs to a different endpoint. Most providers issue one secret per endpoint, so a new tunnel URL registered as a new endpoint means a new secret. Many also sign a timestamp and reject anything outside a tolerance window, which is worth knowing before you replay yesterday's event.

## How do you replay a webhook you already received?

Use the provider's own redelivery. Providers keep a delivery log for each endpoint with a resend or redeliver control, and that path sends the identical payload with a valid signature, which is the only replay that exercises your verification code honestly.

Saving the raw body and posting it back at yourself with curl is still useful for handler logic, ordering and idempotency bugs, but a hand-made request will not carry a signature your verifier accepts unless you sign it yourself. Keep that trick for parsing work and use provider redelivery for end-to-end tests.

## Does the endpoint have to be HTTPS?

In practice, yes. Most providers now refuse to save an `http://` endpoint at all, and the ones that still allow it warn you, because a webhook body is data your application is about to trust.

A tunnel gives you HTTPS at the public edge with a certificate the provider already trusts, and forwards to your local server over plain HTTP on loopback. You do not generate a certificate for your dev machine and you do not persuade anyone to trust a self-signed one. [HTTP vs HTTPS tunnels](https://tunnels.io/learn/http-vs-https-tunnels) explains where TLS is terminated and what your handler sees as a result.

## Why do deliveries fail when the tunnel is clearly up?

Because the request arrived and your application rejected it. The provider marks a delivery failed on any non-2xx response, so the interesting number is the status your app returned, not whether the connection worked. Check the path first, then the status, then how long the handler took.

404 on the path

The tunnel forwards the full path through untouched. A 404 means your router never matched the route, usually a missing prefix or a trailing slash the provider stored.

400 or 403 from middleware

Allowed-host lists, CSRF checks and strict-host middleware reject an unfamiliar hostname. Add the tunnel host to your development config and exempt the webhook route from CSRF.

Timeouts

A breakpoint held open past the provider's timeout is a failed delivery. Acknowledge with a 200 first, then do the slow work on a queue, which is what you want in production anyway.

## Related reading

[Webhook development The provider-by-provider view: what Stripe, GitHub, Shopify and Twilio each demand of an endpoint.](https://tunnels.io/use-cases/webhooks) [What is a reserved subdomain? Why a URL that changes on every restart quietly breaks the endpoint you registered last week, and when it is worth fixing.](https://tunnels.io/learn/what-is-a-reserved-subdomain) [How to expose localhost to the internet The four ways to do it, tunnels, SSH forwarding, port forwarding and deploying, with the honest trade-offs of each.](https://tunnels.io/learn/how-to-expose-localhost) [HTTP vs HTTPS tunnels Where TLS is terminated on a tunnel URL, and why webhook providers increasingly refuse anything but HTTPS.](https://tunnels.io/learn/http-vs-https-tunnels)

More of these in [Learn](https://tunnels.io/learn).

## How do you test webhooks with tunnels.io?

Run `tunnels http 3000` and your local port has a public HTTPS URL on `tunnels.host` with a valid certificate. There is no interstitial or warning page on any plan, including the free one, which matters more here than anywhere else: a webhook caller is not a browser and will never click through a splash screen. The client also runs a request inspector on `localhost:4040` showing every delivery with its method, path, status, duration and time, which is usually enough to find a failure without adding a log line.

The free Basic plan is $0, HTTP only, with 4 GB a month, one tunnel and a random URL, which is all a debugging session needs. When the endpoint has to stay registered between sessions, Pro at $4 a month or $40 a year adds up to three reserved subdomains, five tunnels, 128 GB and TCP tunnels. Limits are a sustained request rate rather than a monthly quota, 1,024 requests per second on Basic and 8,192 on Pro, so a burst of retries after you come back online is not going to cost you anything.

[Get started free →](https://tunnels.io/get-started) [See plans and limits](https://tunnels.io/pricing)

## Questions

### Can I test Stripe or GitHub webhooks without deploying anything?

Yes. Point the provider at a public HTTPS tunnel URL that forwards to your local port, then trigger an event from their dashboard or CLI. The delivery travels over the internet to the tunnel edge and down to your handler, so you can breakpoint the real payload and verify the real signature. Nothing is deployed and nothing is mocked.

### Do webhook signatures still verify through a tunnel?

Yes, as long as your handler verifies against the raw request body. The tunnel forwards the body through unchanged. Signature failures during local testing almost always come from JSON middleware parsing and re-serializing the payload before verification, or from using a signing secret that belongs to a different registered endpoint.

### Do I need a paid plan to test webhooks locally?

No. A random HTTPS URL on the free Basic plan is enough for a debugging session, because you paste it into the provider, work, and remove it afterwards. You need a reserved subdomain, which starts on Pro at $4 a month, when the endpoint stays registered between sessions and you do not want to update the provider dashboard every morning.

### What happens to deliveries while my tunnel is offline?

They fail while nothing is connected, and most providers retry on a backoff for hours or days before giving up. Closing your laptop at the end of the day therefore does not usually mean losing events. When you reconnect, check the provider's delivery log and redeliver anything that exhausted its retries.

### Why does my framework return 400 or 403 only through the tunnel?

Because the request now arrives with the tunnel hostname in the Host header instead of localhost. Allowed-host lists, CSRF origin checks and strict-host middleware reject the unfamiliar name before your handler runs. Add the tunnel hostname to the allowed hosts in your development config, and exempt the webhook route from CSRF, since a webhook caller has no session or token to present.
