Skip to content

Events and webhooks

vxnsin edited this page Sep 4, 2026 · 1 revision

Events and webhooks

warden history answers afterwards, and only if somebody thinks to look. The interesting moments are the ones nobody is watching for: a port taken while a deploy runs, a lease that quietly expired overnight, a holder that died and left its port booked.

Every one of those already lands in warden's own book. This carries it further.

Watching it happen

$ warden events
09:41:02  registered   shop-api  127.0.0.1:8600
09:41:11  renewed      shop-api  127.0.0.1:8600
09:41:44  released     shop-api  127.0.0.1:8600

warden events --json writes one event per line and flushes each as it arrives, so it pipes into anything:

warden events --json | jq -r 'select(.action == "expired") | .name'

GET /v1/events is the same stream as server-sent events, behind the same token as every other read. See HTTP API.

There are five actions:

Action When
registered A name got a port it did not have
renewed A heartbeat, or a re-register on the same port
moved A name came back on a different port
released Given back, by the holder or by warden release
expired A lease ran out and warden took the port back

Posting it somewhere

warden setup asks for all of this and posts a test event before saving anything. One setting at a time works too:

warden settings set webhook https://discord.com/api/webhooks/...
warden settings set webhook_format discord
warden settings set webhook_events registered,released
warden webhook --test

Restart the warden afterwards. It reads its settings once, at startup.

Where the address comes from

Discord Channel → Edit → Integrations → Webhooks → New Webhook → copy the URL
Slack api.slack.com/apps → your app → Incoming Webhooks → Add New Webhook
Teams Channel → ⋯ → Workflows → "Post to a channel when a webhook request is received"
Your own Anything that accepts a POST

The Teams route is a Power Automate flow because the old Office 365 connector cards are being retired; warden posts an adaptive card in a message, which is what that flow accepts.

The four shapes

webhook_format What arrives
json The event as it is, signed. Build anything custom on this
discord An embed, coloured by action
slack Blocks, plus a text fallback for phone notifications
teams An adaptive card in a message attachment

Checking it really came from you

With json and a webhook_secret, every post carries a signature:

X-Warden-Signature: sha256=b1646dcf...

That is an HMAC over exactly the bytes that were sent, keyed with the secret. warden serialises the body itself rather than leaving it to the HTTP client, because a signature over a body somebody else re-serialises signs something else.

expected = hmac.new(secret.encode(), request.body, hashlib.sha256).hexdigest()
hmac.compare_digest(request.headers["X-Warden-Signature"], f"sha256={expected}")

Every post also carries X-Warden-Node and X-Warden-Event, whatever the shape.

Two things that are not negotiable

Handing out a port never waits on a webhook. Delivery happens after the change is committed and off the request path, retried three times and then given up on. A chat server having a bad afternoon cannot make a port take longer to hand out. A reader that falls behind loses its oldest event, never its newest, and never the write.

A webhook that fails is not allowed to fail quietly. From the inside, one failing all day looks exactly like a quiet day. warden webhook and warden doctor both report the target, the shape and the last failure:

$ warden doctor
...
warn  events to https://discord.com/... are not arriving - Server error '502 Bad Gateway'

The address is always cut back to its host. A webhook URL is a credential, and its path is the half worth stealing.

In a fleet

Every warden reports its own events, so every node you want to hear from needs the setting. A hub does not relay its nodes' events — a node that cannot reach the hub still hands out ports, and the same independence applies here.

Clone this wiki locally