> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hookie.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Local development

> Forward live events to a port on your laptop, watch the stream, send test events and replay a bad window — without a tunnel.

The loop this replaces: expose a port through a tunnel, paste the temporary URL into a provider's webhook field, and change it again tomorrow when the tunnel restarts. Instead, your endpoint URL stays the one you already configured, and the CLI carries events the last hop to your laptop.

```bash theme={null}
hookie listen --forward http://localhost:3000/webhook
```

```
Listening on https://app.hookie.ai (project p_7fa2…) over ws.
The live stream is best-effort: a quiet tail does not prove nothing arrived.
Resume only works inside the server's buffer (1000 events / 5 minutes).
Forwarding to http://localhost:3000/webhook
Body: the routed record's data, NOT the original request body. Use --envelope for the full record.
200 ok  31ms  seq 4181  orders
200 ok  27ms  seq 4182  orders
```

## Three things this is not

It is worth being blunt about these, because each one is a way a tunnel behaves differently.

<Warning>
  **The body is the routed record, not the original HTTP request.** What arrives has already been through your endpoint's mapping rules, so it is the record Hookie stored rather than the bytes the sender posted. Pass `--envelope` to forward the whole stream record (`seq`, `id`, `dataset`, `received_at`, `data`) instead of just its `data`.
</Warning>

<Warning>
  **The live stream is best-effort by contract.** A quiet tail is not proof that nothing arrived — deliveries and records are the durable account. When events are evicted before you see them, the CLI prints an explicit `GAP:` line naming the range and pointing at where the record actually is. Silence would be the lie.
</Warning>

<Warning>
  **Your plan caps concurrent stream connections** — one, on Free — counted across the CLI and any console **Observability** tab you left open. A second attach is reported as that sentence rather than a bare `429`.
</Warning>

Metadata travels in headers so the body stays exactly what a webhook handler expects:

| Header                                    | Value                                                                                   |
| ----------------------------------------- | --------------------------------------------------------------------------------------- |
| `Hookie-Event-Id`                         | The record id.                                                                          |
| `Hookie-Dataset`                          | The dataset it was routed into.                                                         |
| `Hookie-Seq`                              | The stream cursor. Gaps in it are normal when you filter — the cursor is per workspace. |
| `Hookie-Received-At`                      | When Hookie received it.                                                                |
| `Hookie-Project-Id` · `Hookie-Webhook-Id` | Where it came from, when known.                                                         |

A handler that ignores all of them still works.

## Narrow what you get

```bash theme={null}
hookie listen --forward http://localhost:3000/webhook \
  --project growth --dataset orders
```

Both filters are applied by the **server**, not after the fact. That matters more than it looks: the plan caps connections, so a client that attached to everything and filtered locally would spend its whole allowance to watch one project.

| Flag                                         |                                                        |
| -------------------------------------------- | ------------------------------------------------------ |
| `--forward <url>`                            | Where to POST each event. Required.                    |
| `--dataset <name>` · `--project <slug>`      | Server-side filters.                                   |
| `--envelope`                                 | Forward the whole stream record instead of its `data`. |
| `--method <verb>` · `--header "Name: value"` | Change the request. `--header` repeats.                |
| `--max <n>` · `--timeout <30s>`              | Stop after this many events, or this long.             |
| `--from-seq <n>`                             | Resume from a cursor, inside the buffer.               |
| `--transport ws\|sse`                        | WebSocket by default.                                  |

A local server that is not running yet does not end the session — that is the normal state while you are still starting your app. Each failure is reported and the CLI keeps listening; the exit code tells you afterwards that something was missed.

## Watch without forwarding

```bash theme={null}
hookie tail --dataset orders
hookie tail --json | jq 'select(.data.total_cents > 10000)'
```

`tail` prints events instead of forwarding them. With `--json` it prints **one line per event**, because a tail is a stream and has to be consumable a line at a time rather than as one document.

## Send one

```bash theme={null}
hookie send --endpoint checkout --data @event.json
hookie send --endpoint checkout --data '{"type":"order.created"}'
jq -n '{type:"order.created"}' | hookie send --endpoint checkout --data @-
```

`send` posts the **public ingest plane** — the same URL a provider posts to — because there is no admin "send". `--endpoint` takes an endpoint's readable base slug, its id, its name, or a full `workspace/project/slug` path. Add `--dataset raw` to append a dataset segment, which stores the payload whole.

Run `hookie listen` in one terminal and `hookie send` in another and you have the whole round trip in front of you.

## Replay a bad window

```bash theme={null}
hookie replay dlv_01H8…                  # one delivery
hookie replay --since 1h --failed        # every failure in the last hour
hookie replay --since 2026-09-26T09:00:00Z --until 2026-09-26T11:00:00Z --failed
```

The bulk form searches deliveries and replays each match, because the server replays one at a time and has no bulk route — this is the CLI doing the loop rather than pretending an endpoint exists. It asks first, since every replay sends a real request to a real destination.

<Note>
  **A replay cannot itself be replayed.** The source row's event id already carries a `#replay:` marker, so a second one would be rejected. The CLI detects those, skips them, and says how many it skipped — rather than emitting deliveries that are guaranteed to fail.
</Note>

If a destination starts pushing back with `429`, the run stops there rather than piling on.

## Open the console where you are looking

```bash theme={null}
hookie open --project growth --tab deliveries
```

Prints the URL and opens it. The print is what makes it work over SSH and on a headless box.

## A local Hookie

Everything above works against a local `wrangler dev` too, which is the default target:

```bash theme={null}
npm run dev --workspace apps/api      # http://localhost:8787
hookie tail                            # no --url needed
```

No connection is needed for a local origin: set `HOOKIE_TOKEN`, or just point at it. The remote gate only applies to origins that are not localhost.
