Skip to main content
What happened to a message is pushed to you as an event, so webhooks listen is how you observe delivery from a terminal.
The listener binds 127.0.0.1:4318 and serves / by default. Change any of that with --host, --port and --path.

Getting events to your machine

Registering a local endpoint runs into three constraints. Endpoints must be public https URLs, which requires a tunnel. The CLI does not bundle or download one. Run whichever you like, then pass its address with --public-url to tell the listener what it is reachable at. Registration probes the URL with a challenge, which means the listener has to already be running when you save the endpoint in the dashboard. Start the listener, then go and register it. Event routing is exclusive. Subscribing a URL to an event type on a domain moves that event off whatever endpoint had it, and there is no redelivery for anything that was in flight. Point this at a test domain.
Registering your laptop for an event type on a production domain silently takes those events away from your production endpoint for as long as you are subscribed. Use a domain you can afford to disturb.

Watching events arrive

By default each delivery is rendered for a human. --print picks how much: --filter narrows to the event types you care about, and --max-field shortens anything wider than 512 characters so a long body does not swamp the terminal.
See Webhooks for the event catalog and the payload of each type.

Forwarding into your application

--forward re-posts every accepted delivery to a local URL. Your real handler receives real events while the listener keeps printing them:
The forward target has to be on this machine unless you pass --force. A forward gets 10 seconds by default, adjustable with --forward-timeout. Two flags change the semantics. --forward-sync waits for your handler before acknowledging the delivery to mailkube, which turns a handler failure into a retry. --forward-duplicates also forwards redeliveries, which is how you prove your deduplication on the event id works.

Recording a session

--record appends every accepted delivery to a .jsonl file, one JSON object per line, headers included:
That file is what makes the rest of this replayable. You can feed it back through your handler later, or verify a signature from it long after the timestamp has gone stale.

Using a listener as a test

--exit-after stops once that many matching events have arrived, and --exit-timeout gives up with exit 124 if they have not. Together they turn a listener into an assertion:
Exit 0 means the event arrived. Exit 124 means it did not arrive in time.

Replaying synthetic events

Real sends cost quota and move your reputation. webhooks simulate posts a synthetic, correctly signed event to a local endpoint:
Run it without --event to see the list of types it can produce. --file sends a payload of your own, from @file or @-. A line out of a --record capture replays straight back through your handler. The target must be loopback unless you pass --force. Pair it with --local, which runs a listener with no public URL at all, when you want to exercise a rendering or forwarding path without involving the platform.

Verifying a signature offline

webhooks verify performs the same computation a receiver does, against a payload you already have. Nothing is sent and nothing is contacted:
--body is never re-encoded, because a signature covers the exact bytes that were sent and reserializing the JSON would change them. The --id, --ts and --sig values are the X-Webhook-Id, X-Webhook-Ts and X-Webhook-Sig headers from the delivery. --tolerance is the accepted age of the timestamp, 5 minutes by default. Pass --tolerance 0 to check the signature alone. That is the setting for a capture from yesterday, where the timestamp is legitimately old.

Secrets

Every one of these commands reads the endpoint signing secret from --secret or from MAILKUBE_WEBHOOK_SECRET. It is never stored in the config file: a signing secret is tied to a single endpoint, and the config file stores per-profile settings. --skip-verify accepts unverified deliveries. It exists for development against a payload you constructed yourself, and using it against real traffic means accepting anything that reaches the port.
Inside a container, the default loopback bind is unreachable from a published port. Run mailkube webhooks listen --host 0.0.0.0 with -p 4318:4318.

Next steps

Webhook events

Every event type, its payload, and the signature scheme.

Scripting

Turning a recorded session into an assertion.