arizuko

arizuko › howto › Webhooks

webhooks

A service that can send an HTTP POST when an event occurs can give events to an agent. Make a hook token and put its URL into the source. The agent then sees each event as an inbound message at a hook: JID.

The mechanism is the route token. The setup below covers the three most common sources.

The webhook URL is /hook/<token>. Every valid route token works at this URL. The JID kind is information for the agent and does not limit the URL. issue_webhook always returns a /hook/<token> URL.

Three sources show the use

A GitHub push
The webhook settings of a repository post a push payload to /hook/<token>. The agent sees one inbound message for each push at hook:<folder>/github, with the body unchanged. A skill of the agent reads the payload and decides to reply on Slack, to file a task, or to do nothing.
A Linear comment
Linear posts new comments to a webhook URL. With a jid_suffix, issues and comments arrive at two JIDs, hook:<folder>/linear/comments and hook:<folder>/linear/issues. Each JID can have its own route.
An alert
Grafana, Sentry, Healthchecks.io or a cron job that posts JSON. Any HTTP client works. The agent gets the body unchanged.

The agent, the CLI or the REST API makes the token

All three ways write the same row.

The agent asks for it. A turn in the folder acme/eng calls:

issue_webhook(source_label="github")

The MCP answer shows the URL only once:

{
  "token": "Yp3v...Q2",
  "url":   "https://<host>/hook/Yp3v...Q2",
  "jid":   "hook:acme/eng/github"
}

The agent usually writes the URL into the chat, so that a person can paste it into GitHub. The store keeps only a hash of the token. If the URL is lost, issue a new token.

An operator uses the CLI, here on the instance demo:

arizuko token -i demo issue webhook acme/eng github
# jid:   hook:acme/eng/github
# token: Yp3v...Q2

The URL is https://<host>/hook/<token>. An external tool uses POST /v1/route_tokens/hook on routd. The dashboard lists the tokens of a folder at /dash/tokens/<folder>/ and can revoke them, but it does not make tokens.

Each way also takes an optional context. The context is the processing rule of the link. The agent gets it as a <link-context> block with each event:

issue_webhook(source_label="stripe", context="payment events; summarize daily, no replies")

A GitHub repository needs six settings

  1. Ask the agent at acme/eng to call issue_webhook(source_label="github"). Copy the URL.
  2. In GitHub, open the repository, then Settings, then Webhooks, then Add webhook.
  3. Payload URL: https://<host>/hook/<token>
  4. Content type: application/json.
  5. SSL verification: enabled.
  6. Events: select the events that the agent must see.

At the next push, webd hashes the token, finds the row and sends one inbound message to routd:

{
  "jid":     "hook:acme/eng/github",
  "sender":  "github",
  "content": "{\"ref\":\"refs/heads/main\",\"commits\":[...]}"
}

The body is unchanged, up to 1 MiB. webd does not forward the request headers. The token URL is the only authentication, as the security model explains.

webd answers GitHub with 204 No Content. The agent answers, if at all, on the channel that fits. A webhook source does not read a reply.

A suffix keeps several sources apart in one folder

Pass a jid_suffix to split the events. One folder can receive several webhook streams at different JIDs:

issue_webhook(source_label="linear", jid_suffix="comments")
  → hook:acme/eng/linear/comments

issue_webhook(source_label="linear", jid_suffix="issues")
  → hook:acme/eng/linear/issues

Each suffix gets its own URL, its own row and its own JID. A route can start turns for one JID only, with chat_jid=hook:acme/eng/linear/comments. Another route can store the events of the other JID with #observe and start no turn.

A revoke takes effect at once

Revoke through MCP or the CLI:

revoke_token(jid="hook:acme/eng/github")
# or
arizuko token -i demo revoke hook:acme/eng/github

The next request to that URL returns 404. No grace period exists. The caller of revoke_token must own the token: the owner_folder of the token must be the folder of the caller.

The URL is the only secret

Anyone with the URL can post to the JID. webd does not check a body signature.

The bearer model is a design decision, and it has three results:

Rotate by a new token
If the URL leaks, make a new token, put it into the source and revoke the old row. No rotation in place exists.
Use one token for each source
A leaked GitHub token can post into hook:acme/eng/github. It cannot post into hook:acme/eng/linear/* or into any other JID.
No body signature
webd drops the HMAC headers of the source, for example the GitHub X-Hub-Signature-256 and the Stripe Stripe-Signature, with all other request headers.

webd limits the rate for each token. The limit for hook: JIDs is higher than for web: JIDs, because machines send bursts. A request over the limit gets 429. The body limit is 1 MiB.

The journal shows each event

sudo journalctl -u arizuko_<instance> --since "30s ago" -f \
  | grep -iE 'hook inbound|hook rate limit|route token not found'

A 404 means that the token is unknown or revoked. A 429 means that the rate limit applies. A 413 means that the body is larger than 1 MiB.

Four pages give the details

route tokens
The mechanism of this page and of the chat link.
chat link
The web: token for browser chat URLs.
token reference
Payload shapes, env vars and the full MCP and REST surface.
routing
How a hook: JID reaches an agent, and how #observe works.