API Reference
HTTP routes
Every path the board server answers, with its method, request body, response shape and status codes.
All paths are relative to the board’s address, which is http://127.0.0.1:5599
unless you moved it with --port.
Routes that write are matched on POST. Read routes are matched on the path
alone and answer whatever method you send; use GET.
The read routes
GET /api/state
Everything the board draws about a workspace, from directory listings and
torta.json. Always 200: when the config cannot be read, error carries
the message and the rest is empty.
{
"workspace": "/Users/you/projects/app",
"project": "app",
"agents": [
{
"name": "frontend",
"home": "/Users/you/projects/app/.agents/frontend",
"access": { "reads": [], "writes": [], "shadowed": 0, "toolDenies": [],
"counts": { "allow": 0, "deny": 0 } },
"counts": { "todo": 2, "wip": 1, "done": 9 },
"todoStatus": { "APPROVED": 2 },
"tickets": { "todo": [], "wip": [], "done": [] }
}
],
"statuses": [],
"roles": {},
"statusesSeen": ["APPROVED", "WIP"],
"unknownStatuses": [],
"pending": [],
"runtime": { "state": null, "sessions": [], "killSwitch": false, "engine": null },
"error": null,
"generatedAt": 1755600000000
}
One ticket inside tickets carries id, file, queue, type, slug,
status, title, repo, children, assignee, author, date, redo,
ticket, mtime, trail, hops, arrived, move and movedBy.
A pending row carries the whole decision plus id, which is
<agent>/<queue>/<file>, title, approveTo, rejectTo, note, and
context, the last three budget_breach or lease_reclaim events about that
ticket.
unknownStatuses lists statuses present on disk that the workflow legend does
not name.
GET /api/ticket
| Parameter | Meaning |
|---|---|
id |
<agent>/<queue>/<file>.md |
200 with the parsed frontmatter and the body with the frontmatter stripped:
{ "meta": { "status": "APPROVED", "title": "Add the empty state" }, "body": "## User story\n..." }
404 with { "error": "not found" } when the id has the wrong number of
parts, names a queue that is not todo, wip or done, is not a ticket
filename, contains a path segment that would escape the workspace, or does not
exist.
GET /api/search
| Parameter | Meaning |
|---|---|
q |
the query |
limit |
clamped to 1 up to 200, default 60 |
Search over every ticket. Always 200:
{
"query": "empty state",
"terms": ["empty", "state"],
"hits": [],
"scanned": 42,
"total": 3,
"truncated": false
}
Every term has to match for a ticket to be a hit. A hit is the ticket row plus
agent, score, where, description and snippet. An empty query returns
no hits.
GET /api/panes
Every open pane, plus what this machine supports. Always 200:
{
"ok": true,
"instance": "board-1755600000000",
"agents": ["archi", "product", "frontend"],
"panes": [
{
"agent": "frontend",
"pid": 51234,
"backend": "python",
"started": 1755600000000,
"attached": 1,
"ticket": "feature.add-empty-state.WIP.md",
"idleMs": 1200,
"bytes": 48210,
"exit": null
}
],
"support": {}
}
backend is node-pty or python. instance changes when the board
restarts.
GET /api/settings
One row per account this project needs, with each step checked on this machine.
200:
{
"ok": true,
"project": {},
"platforms": {},
"rows": [
{
"id": "github",
"title": "GitHub",
"why": "every repo in this project, and every PR an agent opens",
"login": "gh auth login",
"loginIsCommand": true,
"url": "https://github.com/signup",
"agents": ["infra"],
"dormant": false,
"ships": null,
"field": null,
"value": null,
"required": true,
"steps": [],
"state": "missing",
"detail": ""
}
],
"blockers": ["github"],
"domainRule": { "pattern": "...", "help": "just the hostname" }
}
state is have, missing or unknown. blockers lists every row that is
not ready and not dormant. A step carries its own state, self, and
failed, which names the checks that did not pass.
409 with { "ok": false, "error": "there is no workspace here yet" } in
wizard mode. 500 if the workspace cannot be read.
GET /api/claude
The Claude Code probe on its own. Always 200:
{ "ok": true, "claude": { "state": "ready", "summary": "Claude Code 1.2.3, signed in.", "fix": [] } }
state is missing, signed-out, unknown or ready. fix is the list of
things to do, in order.
GET /api/wizard
What the questions offer, and which mode this server is in.
200:
{
"ok": true,
"mode": "wizard",
"instance": "board-1755600000000",
"claude": { "state": "ready", "summary": "", "fix": [] },
"parent": "~/Documents/projects",
"roles": [],
"presets": {},
"build": [],
"defaults": { "role": "", "preset": "solo", "locationParent": "~/Documents/projects" },
"rules": {},
"featureLines": [],
"why": "torta runs its agents on Claude Code, signed in as you..."
}
In board mode the answer is ok, mode, instance, claude and parent
only. 500 if the options cannot be built.
GET /api/wizard/preview
The workspace the current answers would produce.
| Parameter | Meaning |
|---|---|
name |
the project name |
location |
where it should live |
build |
comma separated card ids |
preset |
solo or full-team |
role |
what you are |
| Status | Answer |
|---|---|
| 200 | { "ok": true, "preview": { ... } } |
| 400 | { "ok": false, "field": "name", "error": "..." } when validation refuses |
| 409 | the server is no longer a wizard |
| 500 | the preview throws |
GET /api/events
The board’s live update stream. See server-sent events.
Static files
| Path | Content type |
|---|---|
/ and /index.html |
text/html; charset=utf-8, the board or the wizard by mode |
/client.js, /dock.js, /markup.js, /preserve.js, /legend.js, /views.js, /pane-policy.js, /wizard.js |
text/javascript; charset=utf-8 |
/vendor/xterm.js, /vendor/xterm-fit.js |
text/javascript; charset=utf-8 |
/vendor/xterm.css |
text/css; charset=utf-8 |
/vendor/LICENSE |
text/plain; charset=utf-8 |
Nothing outside this allowlist is served. A file that cannot be read answers
500 as text/plain. Anything unmatched answers 404 as text/plain
with the body not found.
The write routes
POST /api/decision
The same call torta approve makes.
{ "ticket": "feature.add-empty-state.REVIEW.md", "decision": "approve", "reason": null }
decision is one of approve, reject, interrupt.
| Status | Answer |
|---|---|
| 200 | the resolver’s answer, which carries ok: true and moved |
| 400 | the body is not JSON, or ticket or decision is missing. The message names the three decisions |
| 400 | the resolver’s own refusal, when the transition is not takeable |
| 500 | torta.json cannot be read |
A successful decision broadcasts to every connected board.
POST /api/settings
Writes one key into torta.json. Exactly one of:
{ "domain": "example.com" }
{ "confirm": "<step id>" }
{ "unconfirm": "<step id>" }
| Status | Answer |
|---|---|
| 200 | { "ok": true } merged into the whole settings payload |
| 400 | pass exactly one of domain, confirm, unconfirm, when the count is wrong |
| 400 | this endpoint writes domain, confirm, unconfirm and nothing else, naming the refused keys |
| 400 | field: "domain" and the help text, when the hostname does not match: just the hostname, example.com, not https://example.com/ |
| 400 | field: "confirm", when the step id is not one this workspace can self-report |
| 409 | wizard mode |
| 500 | the write throws |
Clearing a domain writes null, not an empty string.
POST /api/engine
The pause control.
{ "paused": true }
| Status | Answer |
|---|---|
| 200 | { "ok": true, "paused": true } merged into the runtime strip: state, sessions, killSwitch and engine |
| 400 | pass { paused: true } or { paused: false }, when the field is not a boolean |
| 409 | wizard mode |
| 500 | the file cannot be written or removed |
Pausing writes .torta/DISPATCH_OFF; resuming removes it, and already gone
counts as already resumed. Sessions already running are left alone.
POST /api/wizard
Creates the workspace.
{
"name": "coffee shop site",
"location": "~/Documents/projects/coffee-shop-site",
"preset": "solo",
"role": "founder",
"owner": "you",
"feature": ["what it should do", "who it is for", "how you will know"],
"build": ["landing"]
}
| Status | Answer |
|---|---|
| 200 | { "ok": true, "workspace": "...", "project": "...", "published": null, "warnings": [] } |
| 400 | errors.claude, when Claude Code is missing or signed out |
| 400 | errors.platforms, when a build card id is not something torta builds |
| 400 | errors.<field> from validation, or errors._ when the body is not JSON |
| 409 | this directory already has a workspace |
| 500 | errors._ when provisioning throws. A refusal leaves nothing on disk |
After a 200 the server flips to board mode on the same port, starts watching, and broadcasts that a workspace was created.
POST /api/wizard/pick
Opens this machine’s native folder dialog.
{ "name": "coffee-shop-site" }
200 for all three outcomes: a chosen path, a cancel, or no picker on this machine. When a name is given it is appended to the chosen folder. 409 when the server is no longer a wizard.
POST /api/pane/close
Closes a pane from the board. It does not need the token.
{ "agent": "frontend" }
200 with { "ok": true } when a pane was closed, { "ok": false } when
there was none. 400 when the body is not JSON.
POST /api/pane/status
Token required. 200:
{ "ok": true, "pane": { "agent": "frontend", "pid": 51234, "backend": "python",
"started": 0, "attached": 1, "ticket": null, "idleMs": 900, "bytes": 12, "exit": null } }
pane is null when no pane is open for that agent.
POST /api/pane/keys
Token required. Raw bytes, unwrapped and unsubmitted, so Esc stays a
keystroke.
{ "agent": "frontend", "keys": "ls\r" }
200 when the pane took them, 409 with
no pane is open for this agent when there is none.
POST /api/pane/inject
Token required. Text, delivered as a bracketed paste.
{ "agent": "frontend", "text": "please add the empty state", "submit": true }
200 with { "ok": true, "delivered": "paste" }, or 409 with
no pane is open for this agent.
POST /api/pane/deliver
Token required. One turn, whichever way the session can be reached.
{ "agent": "frontend", "prompt": "...", "model": null, "ticket": "feature.x.WIP.md" }
- 200 with
{ "ok": true, "existed": true, "delivered": "paste", "pid": 51234, "backend": "python" }when a pane was already open and took the paste. - 200 with
{ "ok": true, "existed": false, "delivered": "argv", "pid": 51234, "backend": "python" }when a fresh session was opened, which takes the prompt as a command line argument. - 400 with
deliver needs a promptwhenpromptis empty. - 409 with
existed: truewhen the pane is open but would not take the prompt, orexisted: falsewith the reason a pane could not be opened.
A successful delivery claims the ticket for that pane and nudges every board.
Any other /api/pane/ path
404 with { "ok": false, "error": "no such pane route" }.