arizuko › reference › Route tokens
route tokens
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.
Schema
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);
- token_hash
- sha256 of the raw token. The raw token is returned exactly once at issuance.
- jid
- Destination.
web:<folder>[/<suffix>]for chat tokens,hook:<folder>/<source>[/<suffix>]for webhooks.<folder>is the destination folder, distinct fromowner_folder(the issuer). - owner_folder
- Folder of the issuing principal. Bounds revocation:
revoke_route_tokenrequires admin on this folder, not on the JID folder. - created_at
- RFC3339 timestamp.
Token format
- 32 random bytes, base64url-encoded (~43 chars, 256 bits of entropy).
- Stored as sha256; raw value returned exactly once.
- No expiry. Revocation = delete the row.
Mint surface
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} |
REST request body
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. |
Mint response
{
"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.
/chat/<token>/ and /hook/<token> — each URL bound to its JID prefix kind
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.
Rate limits
| 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.
CLI
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.
Related
- concepts/route tokens — primitive + bearer model.
- components/route tokens — how it fits in the daemon graph.
- how-to: chat link, how-to: webhooks.
- reference/jid —
web:andhook:JID shapes. - reference/env § webd — rate-limit and body-cap env vars.
- Spec:
specs/5/W-webhook-routes.md.