> Part of [WHCC Developer Documentation](https://www.whcc.com/developer/llms.txt)

# Local Webhook Testing

WHCC delivers webhooks by making an HTTPS request to a URL you register. A handler running on `localhost` is not reachable from our network, so during development you need a public HTTPS URL that forwards to your machine. A tunnel does that.

Pair this with the [Webhook Sandbox](https://www.whcc.com/developer/docs/order-submit-api/webhook-sandbox/index.html.md) and you can develop the whole integration locally: trigger a real signed notification on demand, receive it on your laptop, and iterate on your handler without placing an order.

> ## Your tunnel URL must be stable
>
> This is the one decision that matters, and it is easy to get wrong.
>
> Each set of API credentials has **one** callback URI, and [re-registering resets you to unverified](https://www.whcc.com/developer/docs/order-submit-api/webhook-register/index.html.md). So a tunnel that hands you a new random URL every restart forces you to re-register and re-verify every single time &mdash; and each re-registration breaks the callback that was working a moment earlier.
>
> **Use a tunnel option that gives you a fixed hostname.** Both options below do, on their free tiers. Avoid the random-URL modes &mdash; specifically `trycloudflare.com` quick tunnels, which generate a new subdomain on every run.

## Option 1: ngrok

The lowest-friction option, and the one we suggest if you have no preference. It needs no domain of your own, and its request inspector is the fastest way to diagnose a signature failure.

Sign up at [ngrok.com](https://ngrok.com), install the agent, and authenticate with the token from your dashboard. On macOS:

```shell
brew install ngrok
ngrok config add-authtoken YOUR_AUTHTOKEN
```

The free plan includes one static domain, assigned to your account automatically and shown on your dashboard. The label is generated for you and cannot be chosen &mdash; it looks like `abc123xyz.ngrok-free.dev` (some accounts are issued on `ngrok-free.app` instead). Copy it from the dashboard rather than guessing it. Start the tunnel against that domain, pointing at whatever port your handler listens on:

```shell
ngrok http --url=abc123xyz.ngrok-free.dev 3000
```

Your callback URI is then `https://abc123xyz.ngrok-free.dev/` plus whatever path your handler serves. Because the domain is assigned to your account, it is the same every time you start the agent &mdash; register it once and it keeps working.

> **The browser warning page does not affect webhooks.** ngrok's free plan shows an interstitial to browsers, which alarms people setting this up. Per ngrok's documentation it applies only to HTML browser traffic and *&ldquo;does not impact users serving APIs or accessing ngrok endpoints programmatically.&rdquo;* Both our verification POST and our webhook deliveries are server-to-server, so neither sees it.

Free-plan quotas at the time of writing are 20,000 requests and 1&nbsp;GB per month, with up to 3 concurrent endpoints &mdash; far beyond what webhook development consumes. There is no session time limit; the agent can stay online indefinitely.

## Option 2: Cloudflare Tunnel

Choose this if you already have a domain on Cloudflare, want your own hostname, or would rather not have monthly request caps. It has no built-in request inspector, so plan to log requests yourself.

> A **named** tunnel is required. Cloudflare's `trycloudflare.com` quick tunnels (`cloudflared tunnel --url ...`) generate a random hostname per run and are documented as *&ldquo;intended for testing and development only&rdquo;* with no uptime guarantee. That random hostname is exactly what the warning at the top of this page is about.

Named tunnels require a domain already added to your Cloudflare account &mdash; the login step asks you to pick one. Install `cloudflared` (`brew install cloudflared` on macOS; packages and installers for Linux and Windows are on Cloudflare's downloads page), then:

```shell
cloudflared tunnel login
cloudflared tunnel create whcc-webhooks
cloudflared tunnel route dns whcc-webhooks webhooks-dev.example.com
```

`create` prints a tunnel UUID and writes a credentials file. `route dns` adds a CNAME pointing your hostname at the tunnel. Put the UUID into `~/.cloudflared/config.yml` along with the local port you want traffic forwarded to:

```yaml
url: http://localhost:3000
tunnel: <Tunnel-UUID>
credentials-file: /Users/you/.cloudflared/<Tunnel-UUID>.json
```

Then run it:

```shell
cloudflared tunnel run whcc-webhooks
```

Your callback URI is `https://webhooks-dev.example.com/` plus your handler's path. The hostname is yours, so it survives restarts and reboots.

## Register the tunnel URL

Start your handler and the tunnel *before* registering. Registration is not a reservation &mdash; we POST to the URL immediately and expect an answer:

```shell
curl https://sandbox.apps.whcc.com/api/callback/create \
	-H "Authorization: Bearer YOUR_SANDBOX_TOKEN" \
	-F callbackUri=https://abc123xyz.ngrok-free.dev/webhooks/whcc \
	-X POST
```

That POST carries a single `verifier` field as `application/x-www-form-urlencoded`. Your handler must return `2xx`. Read the verifier out of your logs (or out of the ngrok inspector) and submit it:

```shell
curl https://sandbox.apps.whcc.com/api/callback/verify \
	-H "Authorization: Bearer YOUR_SANDBOX_TOKEN" \
	-F verifier=a53ae191-00f3-44f4-810c-19d88a5b4c16 \
	-X POST
```

Full detail, including what [400.05](https://www.whcc.com/developer/docs/order-submit-api/webhook-register/index.html.md) means when verification fails, is on the Registration page. With the callback verified you can start firing events from the [Webhook Sandbox](https://www.whcc.com/developer/docs/order-submit-api/webhook-sandbox/index.html.md).

## Debugging signature failures

Signature mismatches are the most common problem when building a handler, and they are hard to debug from your own logs because the usual cause is a body that was parsed and re-serialized before hashing &mdash; something your framework may do before you ever see it.

ngrok's inspector helps here specifically. Open [`http://localhost:4040`](http://localhost:4040) while the agent runs and you get every request with its headers and **the raw bytes on the wire** &mdash; which is exactly what the [signature](https://www.whcc.com/developer/docs/order-submit-api/webhook-security/index.html.md) is computed over. Compare those bytes against what your handler hashed.

Port `4040` belongs to the first agent you start. A second concurrent agent binds `4041`, so check the port in the agent's own output if you are running more than one tunnel &mdash; it is easy to sit on the wrong one and wonder why no requests appear.

The inspector also has a **Replay** button. Once a real signed webhook has arrived, you can re-send it to your handler as many times as you like while you fix your verification code, without triggering a new event from the sandbox each time.

## Things that catch people out

| Symptom | Cause |
| --- | --- |
| Webhooks stopped after a restart | A random-URL tunnel handed you a new hostname. Use a fixed domain, then register once. |
| 400.05 on /api/callback/create | The tunnel or the handler was not running, the path was wrong, or the handler did not return 2xx to the verification POST. It is also intermittent &mdash; a healthy tunnel returned 400.05 on 2 of 7 attempts in our own testing, including one where the handler answered 200 in under 2&nbsp;ms. Retry once before you start debugging. Note that create reports this as HTTP 200 with {"ErrorNumber":"400.05"} in the body, so check the body rather than the status code. |
| 409 callback_not_verified from the sandbox trigger | You registered but never completed /api/callback/verify. |
| Signature never matches | The body was re-serialized before hashing, or the digest case was compared literally. See Security and Signatures. |
| 524 from a Cloudflare tunnel | Your handler held the connection past Cloudflare's Proxy Read Timeout &mdash; 125 seconds by default, adjustable only on Enterprise plans. Our own delivery timeout is 100 seconds, so WHCC gives up first and retries; you will see the retry before you ever see a 524. Acknowledge fast, work asynchronously. |

> A tunnel is a development tool. Register a real, deployed HTTPS endpoint before you go to production &mdash; and remember that re-registering to switch from your tunnel to production resets the callback to unverified, so complete the verify step again.

> ## What's Next
>
> With a verified callback in place, use the [Webhook Sandbox](https://www.whcc.com/developer/docs/order-submit-api/webhook-sandbox/index.html.md) to trigger `Shipped`, `Processed`, and `Order Cancelled` notifications on demand.