Developer reference for the route_tokens primitive: schema, mint surface (MCP + REST), URL handlers at /chat/<token>/ (for web: JIDs) and /hook/<token> (for hook: JIDs) — each URL bound to its JID prefix kind, mechanics shared. SSE events, rate limits. Spec: specs/5/W-webhook-routes.md.
CREATE TABLE route_tokens (
token_hash BLOB PRIMARY KEY,
jid TEXT NOT NULL,
owner_folder TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE INDEX route_tokens_jid ON route_tokens(jid);
web:<folder>[/<suffix>] for chat tokens, hook:<folder>/<source>[/<suffix>] for webhooks. <folder> is the destination folder, distinct from owner_folder (the issuer).revoke_route_token requires admin on this folder, not on the JID folder.One internal writer (insertRouteToken) reached through two faces, per specs/5/5-uniform-mcp-rest.md.
| Action | MCP tool | REST endpoint |
|---|---|---|
| Issue chat link | issue_chat_link(jid_suffix?) | POST /v1/route_tokens/chat |
| Issue webhook | issue_webhook(source_label, jid_suffix?) | POST /v1/route_tokens/hook |
| List | list_route_tokens() | GET /v1/route_tokens |
| Revoke | revoke_route_token(jid) | DELETE /v1/route_tokens/{jid} |
Each POST body carries the same params as the matching MCP tool:
POST /v1/route_tokens/chat
{ "jid_suffix": "support" } // optional
POST /v1/route_tokens/hook
{ "source_label": "github", "jid_suffix": "main" } // jid_suffix optional
owner_folder is bound from session context (the agent's folder or the caller's grants) and is never a parameter. Tier scope (lower = wider reach):
| Tier | Mint scope |
|---|---|
| 0 | Any folder. |
| 1 | Self and descendants. Issued on behalf of a descendant: owner_folder stays at the issuer. |
| 2 | Self only. |
| 3+ | No mint. |
{
"token": "Yp3v...Q2",
"url": "https://<host>/hook/Yp3v...Q2",
"jid": "hook:acme/eng/github"
}
Token returned once. The <folder> segment of the JID is the destination folder (here acme/eng); for cross-folder mints the row's owner_folder may differ. url is built from WEB_HOST plus the URL bound to the JID prefix kind — /chat/<token>/ for web: JIDs, /hook/<token> for hook: JIDs. Each URL accepts only its bound prefix; the issuance verb picks the URL.
The two URLs share handler internals — same widget, same POST path, same SSE — but the binding is enforced at token lookup. A hook: token at /chat/<token>/ returns 404; a web: token at /hook/<token> returns 404.
| Method | Path | Token JID | Purpose |
|---|---|---|---|
| GET | /chat/{token}/ | web: | Chat widget (HTML). |
| POST | /chat/{token}/ | web: | Append body as one inbound at the row's JID. Returns turn_id (browsers open SSE). |
| GET | /chat/{token}/{turn_id}/sse | web: | Live SSE for the round. |
| GET, POST | /chat/{token}/mcp | web: | Per-token MCP surface: send_message, get_round, get_round_status. |
| GET | /hook/{token} | hook: | Same widget HTML as the /chat/ surface. |
| POST | /hook/{token} | hook: | Append body verbatim as one inbound at the row's JID. Returns turn_id. |
| GET | /hook/{token}/{turn_id}/sse | hook: | Live SSE for the round. |
The token row's JID prefix drives sender attribution and default output style. Body shape, snapshot/status URLs, SSE events, and reconnect via Last-Event-Id are the same round-handle protocol used by every web reply — see components/webd.
POST body:
{"content": "...", "topic": "..."} # JSON
content=...&topic=... # form
<any opaque bytes> # webhook payloads: body verbatim
Default JSON response (browser path):
{
"user": { "id": "msg_abc", "content": "...", "created_at": "..." },
"turn_id": "msg_abc",
"status": "pending"
}
For hook: tokens, the inbound message webd writes:
{
"jid": "hook:<folder>/<source>[/<suffix>]",
"sender": "<source>",
"body": "<raw POST body, verbatim>",
"headers": { "x-github-event": "push", ... }
}
sender is the <source> segment of the JID (the value the agent passed as source_label at mint time). headers carries the full request header set as a lowercase-keyed map for skill-side validation (e.g. X-Hub-Signature verification). Body is opaque to webd — whatever the source POSTed, verbatim, up to 1 MiB.
| JID prefix | Bucket scope | Default ceiling |
|---|---|---|
web: | per token, in-memory | human-typing rate |
hook: | per token, in-memory | machine-burst rate |
Excess returns 429. Body cap is 1 MiB, env-configurable.
arizuko token issue <folder> chat # mints web:<folder>
arizuko token issue <folder> chat --suffix support # mints web:<folder>/support
arizuko token issue <folder> hook github # mints hook:<folder>/github (destination folder)
arizuko token issue <folder> hook linear --suffix issues # mints hook:<folder>/linear/issues
arizuko token list
arizuko token revoke <jid>
All four commands hit the same REST endpoints listed under Mint surface; the CLI does no extra work beyond shaping the JSON and printing the URL.
web: and hook: JID shapes.specs/5/W-webhook-routes.md.