Skip to main content
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.

Three things this is not

It is worth being blunt about these, because each one is a way a tunnel behaves differently.
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.
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.
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.
Metadata travels in headers so the body stays exactly what a webhook handler expects: A handler that ignores all of them still works.

Narrow what you get

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. 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

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

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

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.
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.
If a destination starts pushing back with 429, the run stops there rather than piling on.

Open the console where you are looking

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:
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.