# Test webhooks locally

Test webhooks locally without a tunnel — the whk CLI streams events from your source and replays them against localhost while you build the handler.

Source: https://docs.webhooker.eu/tutorials/local-development/

Destinations must be publicly reachable, so `http://localhost:3000` cannot be
one. Instead the `whk` CLI holds an authenticated stream open and replays each
webhook against your local URL — nothing on your machine is exposed, and no
tunnel is involved. If you are weighing this against ngrok and the other
forwarding options, see
[ngrok alternatives for webhooks](https://webhooker.eu/blog/ngrok-alternatives-for-webhooks).

## 1. Install the CLI

Download the `whk` binary for your platform from the
[releases page](https://github.com/webhooker-eu/webhooker/releases), make it
executable and put it on your `PATH`:

```bash
chmod +x whk
sudo mv whk /usr/local/bin/
whk --version
```

Builds are published for Linux, macOS (Intel and Apple Silicon) and Windows.

## 2. Log in

Create an [API key](/account/api-keys/) in the dashboard, then:

```bash
whk login --server https://app.webhooker.eu
```

The key is prompted for without echoing and saved to your user config
directory, together with the server. `whk whoami` prints the workspace it
resolved, which is the quickest check that the key is good.

The key can also come from the environment, which is what CI should do:

```bash
export WEBHOOKER_SERVER=https://app.webhooker.eu
export WEBHOOKER_API_KEY=whk_…
```

Both have `--server` and `--api-key` flags on every command if you would rather
pass them inline. `whk logout` deletes the saved credentials again.

## 3. Watch what arrives

```bash
whk tail stripe-prod
```

A source can be named by its name, its id or its ingest token. The stream prints
one line per event as it arrives — method, size, verification result. Add
`--json` for one JSON object per line, which pipes into `jq`.

This is metadata only: cheap, and enough to confirm a provider is really sending.

## 4. Forward to your handler

```bash
whk listen stripe-prod --forward http://localhost:3000/hooks
```

Each webhook is replayed against that URL with its original method and body. The
command keeps running until you stop it.

Useful flags:

| Flag | Effect |
| --- | --- |
| `--forward <url>` | Where to replay each webhook. Required. |
| `--header "Name: Value"` | Extra header on the local request. Repeatable. |
| `--skip-verify` | Also forward events whose inbound verification failed. |
| `--json` | One JSON object per line instead of human-readable output. |

```bash
whk listen stripe-prod \
  --forward http://localhost:3000/hooks \
  --header "X-Env: local" \
  --json
```

## 5. Keep production untouched

Two patterns work well:

- **A separate source.** `stripe-test` with the provider's test-mode endpoint
  pointed at it. Production keeps its own source, gateways and history.
- **One source, two gateways.** Your deployed endpoint stays a gateway; you
  listen on the same source while you develop. Both see the same events.

Because events are stored, you do not need the provider to keep sending: trigger
one event, then resend it from the event log as often as you like while you
iterate on the handler. To fire synthetic events with a valid signature instead
of waiting on the provider, the
[webhook mock sender](https://webhooker.eu/tools/webhook-mock-sender) sends
Stripe, GitHub and Shopify payloads from your own machine.

## Troubleshooting

| Symptom | Cause |
| --- | --- |
| `whk: command not found` | The binary is not on your `PATH`. |
| Login fails | Wrong key, or the wrong `--server`. Check with `whk whoami`. |
| `tail` connects but prints nothing | No events are arriving. Send a test event from the provider, or `curl` the ingest URL. |
| Too many open streams | Your plan caps concurrent live streams. Close other `tail`/`listen` sessions and dashboard tabs. |
| Events with `failed` verification never appear | They are withheld by default. Add `--skip-verify` while you debug a secret. |
| `listen` forwards, your app sees nothing | Check the local URL and port, and that the handler accepts the method the provider used. |
