wire webhooks into arizuko
Every SaaS with a “POST when X happens” knob can hand
events to an agent. Mint a hook token, paste the URL into the
source, the agent sees inbound messages at a hook:
JID. The primitive is route
tokens; this page covers the operational details for the
three sources you are most likely to wire up.
/hook/<token>. It accepts
only hook: tokens; presenting a web:
chat token there returns 404. issue_webhook always
returns a /hook/<token> URL.
use cases
GitHub push → agent. A repo's webhook
settings POST a push payload to /hook/<token>.
The agent sees one inbound per push at hook:<folder>/github
with the body verbatim and the X-GitHub-Event header
intact. A skill on the agent side parses the payload, decides
whether to reply on Slack, file a sub-task, or stay silent.
Linear comment → agent. Linear posts new
comments to a webhook URL. With a jid_suffix the
agent can split issues and comments into two streams —
hook:<folder>/linear/comments vs
hook:<folder>/linear/issues — and route
each independently.
Custom alert → agent. Grafana, Sentry, Healthchecks.io, or a cron job that POSTs JSON: anything that speaks HTTP. The body is delivered to the agent verbatim and the headers come with it.
issuing a webhook
Three ways, one writer.
The agent asks for it (MCP). A turn in folder acme/eng calls:
issue_webhook(source_label="github")
The MCP response carries the URL once:
{
"token": "Yp3v...Q2",
"url": "https://<host>/hook/Yp3v...Q2",
"jid": "hook:acme/eng/github"
}
The agent typically prints the URL back into chat so a human can paste it into GitHub. The raw token is not stored anywhere recoverable — if it scrolls past, reissue.
An operator from the CLI.
arizuko token issue acme/eng hook github
# prints the URL once
The CLI hits POST /v1/route_tokens/hook
OAuth-gated through proxyd. Same row, same shape as the
MCP path.
A button in dashd. The folder page in
/dash/ lists existing tokens and offers an
“Issue webhook” button per folder.
worked example: GitHub repo
- Have the agent at
acme/engcallissue_webhook("github"). Copy the URL. - In GitHub: repo → Settings → Webhooks → Add webhook.
- Payload URL:
https://<host>/hook/<token> - Content type:
application/json. - SSL verification: enabled.
- Events: pick the ones you want the agent to hear about.
On the next push, webd hashes the token, looks up the row, and writes one inbound:
{
"jid": "hook:acme/eng/github",
"sender": "github",
"body": "{\"ref\":\"refs/heads/main\",\"commits\":[...]}",
"headers": {
"x-github-event": "push",
"x-github-delivery": "...",
"x-hub-signature-256": "sha256=..."
}
}
The body is verbatim — whatever GitHub sent — up
to the 1 MiB cap. Headers come through lowercase-keyed. The
agent reads them via the inbound message shape; signature
validation (X-Hub-Signature-256) belongs to a
skill, not the platform.
webd returns a plain response to GitHub. The agent acknowledges (if at all) on whichever channel makes sense — webhooks rarely care about an in-band reply.
multiple sources under one folder
Pass a jid_suffix to partition. One folder can
receive several webhook streams at different JIDs without
collision:
issue_webhook("linear", jid_suffix="comments")
→ hook:acme/eng/linear/comments
issue_webhook("linear", jid_suffix="issues")
→ hook:acme/eng/linear/issues
Each suffix gets its own URL, its own row, and its own JID.
Route rules can fire only on one (jid=hook:acme/eng/linear/comments)
and ignore the other, or observe everything from
hook:acme/eng/linear/* without firing a turn
(see #observe).
revocation
One call from MCP or REST:
revoke_route_token(jid="hook:acme/eng/github")
# or
arizuko token revoke hook:acme/eng/github
The next request to that URL returns 401. No grace period.
An agent in a different folder cannot revoke a token whose
owner_folder is not its own — the ACL
check at revoke_route_token requires admin on
the issuing folder.
security model
The bearer model is intentional and has three corollaries.
- Rotate by reissue. If the URL leaks, mint a new token, swap it at the source, revoke the old row. There is no in-place rotation.
-
One token per source. A leaked GitHub
token can POST into
hook:acme/eng/github; it cannot POST intohook:acme/eng/linear/*or any other JID. Keep sources separate. -
Body signing is a skill. If the upstream
ships an HMAC header (GitHub
X-Hub-Signature-256, StripeStripe-Signature), validate it in a skill on the agent side using theheadersmap on the inbound. The platform never touches it.
Per-token rate limits live in webd: a higher bucket for
hook: than for web:, sized for
machine bursts. Excess returns 429. Body cap is 1 MiB.
checking it works
sudo journalctl -u arizuko_<instance> --since "30s ago" -f \
| grep -iE 'hook:|/hook/'
A 401 means the token is unknown or revoked. A 429 is the rate-limit bucket. A 413 is body over 1 MiB.
go deeper
- concepts/route tokens — the primitive both this page and chat link use.
- how-to: chat link — the
web:-prefix sibling, for browser chat URLs. - reference/tokens — payload shapes, env vars, full MCP and REST surface.
- routing — how a
hook:JID reaches an agent and how#observeworks.