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 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.
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. So a tunnel that hands you a new random URL every restart forces you to re-register and re-verify every single time — 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 — specifically trycloudflare.com quick tunnels, which generate a new subdomain on every run.
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, install the agent, and authenticate with the token from your dashboard. On macOS:
1
2
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 — 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:
1
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 — 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 “does not impact users serving APIs or accessing ngrok endpoints programmatically.” 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 GB per month, with up to 3 concurrent endpoints — far beyond what webhook development consumes. There is no session time limit; the agent can stay online indefinitely.
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 “intended for testing and development only” 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 — 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:
1
2
3
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:
1
2
3
url: http://localhost:3000
tunnel: <Tunnel-UUID>
credentials-file: /Users/you/.cloudflared/<Tunnel-UUID>.json
Then run it:
1
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.
Start your handler and the tunnel before registering. Registration is not a reservation — we POST to the URL immediately and expect an answer:
1
2
3
4
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:
1
2
3
4
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 means when verification fails, is on the Registration page. With the callback verified you can start firing events from the Webhook Sandbox.
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 — something your framework may do before you ever see it.
ngrok's inspector helps here specifically. Open http://localhost:4040 while the agent runs and you get every request with its headers and the raw bytes on the wire — which is exactly what the signature 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 — 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.
| 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 — 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 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 — 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 — and remember that re-registering to switch from your tunnel to production resets the callback to unverified, so complete the verify step again.
With a verified callback in place, use the Webhook Sandbox to trigger Shipped, Processed, and Order Cancelled notifications on demand.