
# Testing webhooks locally with keenauth webhooks listen

`keenauth webhooks listen` lets you develop a webhook receiver on your own machine. It reads the event API of a `development` or `staging` environment and posts each new event to a local URL, signed exactly like a real webhook delivery (Standard Webhooks). You need no public endpoint and no tunnel: the events come to the CLI through the API, and the CLI forwards them on your machine.

## Usage

```sh
keenauth webhooks listen --forward-to http://localhost:3000/webhooks
```

The command first prints a signing secret for this session on stderr:

```text
Signing secret for this session (shown once; not sent to KeenAuth, not stored): idp_whsec_…
Forwarding new events to http://localhost:3000/webhooks. Press Ctrl-C to stop.
```

Configure your receiver with that secret while the command runs, for example as `KEENAUTH_WEBHOOK_SECRET`, and verify each delivery with the webhook verifier of the SDK (see [Verify webhooks](/docs/sdk/go)). The secret is created on your machine for this session only: it is never sent to KeenAuth, never stored, and a new run creates a new one. It is not the secret of any webhook endpoint you registered.

| Flag | Description |
|---|---|
| `--forward-to` | Local URL that receives the events (required). It must be an `http` or `https` URL on `localhost` or a loopback address such as `127.0.0.1` or `[::1]`. |
| `--types` | Only these event types, comma separated or repeated. |
| `--org` | Only events of this organization (`org_…`). |
| `--payload` | `thin` (default) or `full`. `full` includes the resource and also needs the `users:read` permission. |
| `--after` | Where to start: `latest` (default) forwards only events that happen from now on; a cursor from `keenauth events tail` starts there. |
| `--interval` | Time between two checks when there are no new events, from `1s` to `60s`. Default: `5s`. |
| `--output`, `-o` | `table`, `json` or `yaml` for the delivery lines. Without the flag the CLI prints a table on a terminal and JSON otherwise. |

The command needs the `events:read` permission and works with an API key and with a `keenauth login` session.

## What your receiver gets

Each event is sent as `POST` with the event JSON as the body, exactly as the event API returned it, and these headers:

| Header | Value |
|---|---|
| `webhook-id` | The event ID (`evt_…`), the same as in the body. |
| `webhook-timestamp` | The time of the delivery, in Unix seconds. |
| `webhook-signature` | `v1,<base64 HMAC-SHA256>` of `<webhook-id>.<webhook-timestamp>.<body>` with the session secret. |
| `content-type` | `application/json` |

Each delivery is tried once. The command prints one line per delivery with the time, event type, event ID, the status code your receiver answered (or the connection error) and the duration. A receiver that fails or is not running does not stop the command. Redirects are not followed.

## Environments

The command works only with `development` and `staging` environments. With a production credential it stops before it sends any request and exits with code 3:

- an API key for a production environment (`idp_sk_live_…` or `idp_rk_live_…`);
- a `keenauth login` session whose current environment is a production environment.

If the server refuses the request because the environment is a production environment, the CLI prints its problem response, `code: test_feature_not_in_production`, and also exits with code 3.

## Exit codes

| Code | Meaning |
|---|---|
| `0` | Stopped with Ctrl-C. |
| `1` | A server error or a network failure towards KeenAuth, or the type of the current environment could not be determined. |
| `2` | Usage error: `--forward-to` is missing, not a local URL, or a filter or interval is invalid. No request was sent. |
| `3` | A production credential, a missing credential, or the server answered `401` or `403`. |
| `4` | The plan does not include the feature (`403` with `plan_feature_unavailable`). |
