arizukohow-to › webhooks

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.

The webhook URL is /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

  1. Have the agent at acme/eng call issue_webhook("github"). Copy the URL.
  2. In GitHub: repo → Settings → Webhooks → Add webhook.
  3. Payload URL: https://<host>/hook/<token>
  4. Content type: application/json.
  5. SSL verification: enabled.
  6. 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 URL is the secret. Anyone who holds it can POST to the JID. webd does not verify any body signature.

The bearer model is intentional and has three corollaries.

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