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": {},
"intake": "product",
"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.
intake is the agent that takes requests: the front door if the roster has
one, otherwise whoever leads it. With no tickets anywhere the board names it in
its empty state, because that conversation is the only thing left to do.
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": {},
"adopt": {},
"why": "torta runs its agents on Claude Code, signed in as you..."
}
adopt is the engine’s adoption policy, narrowed to what a page draws: the
owners a project may be given and the line describing each, the two answers that
name no owner and the words for them, the line shown while the reading runs, the
two sentences every adoption owes the reader, and the name of the report it
writes. The browser hardcodes none of it.
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, or the folder being adopted |
build |
comma separated card ids |
preset |
solo or full-team |
role |
what you are |
mode |
adopt for the adoption preview; anything else previews a new workspace |
assignments |
adoption only: JSON, one path: owner per project found |
| 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 |
With mode=adopt the answer is the same one torta adopt --dry-run prints,
from the same function, so the summary page and the terminal cannot describe
different moves.
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",
"build": ["landing"]
}
Send "mode": "adopt" with folder instead of location, plus assignments,
to adopt rather than create. Adoption runs its own code path and never the
create path, because the folder is full of somebody’s work and the delete that
guards a failed generation there would destroy it.
The queues are created empty either way. intake in the answer names the agent
the board should open a pane on, which is the agent that authors feature
tickets. An adoption also answers with adopted: true and report, the path of
the ADOPTED.md it wrote.
| Status | Answer |
|---|---|
| 200 | { "ok": true, "workspace": "...", "project": "...", "intake": "product", "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.
Creating, the chosen folder is the parent and the name is appended. Adopting, the chosen folder is the project itself, so nothing is appended: a name there would point torta at a path that is not on the disk.
POST /api/wizard/scan
Reads a folder somebody wants adopted: local probes and two git questions,
and then one read-only Claude Code session that says who owns each project it
found. Nothing is written and nothing moves.
{ "folder": "~/Documents/projects/my-app" }
200 with verdict (ready, empty or truncated), the refusal that
belongs to that verdict in the schema’s own words, the name the folder
implies, every projects row found (each with what it is, the owner the
reading placed it with, the why it gave, and the to it would move into with
the because for that repository), the default assign and build, note
when the reading answered nothing usable, and android when a folder this
workspace will not build is coming along. A row the reading would not place
carries owner, why, to and because as null, and the project is left
where it is. A folder that cannot be adopted at all answers 200 with
ok: false and field: "location". 409 when the server is no longer a
wizard.
The session is not asked for: the wizard already refuses to run without Claude Code installed and signed in. Its start is logged, and what it said is kept on the server, so the owners an adoption acts on are the ones a session gave rather than ones a browser could invent.
POST /api/wizard/place
Where each project would go, for the owners the assign page is showing now.
{
"folder": "~/Documents/projects/my-app",
"assignments": { "app": "frontend", "api": "backend" }
}
200 with destinations, one entry per project, each carrying the to it
would move into and the because for that repository. Cheap and pure: no
session is started, nothing is written, and the folder is not walked again while
the scan that read it is still held. It is what makes correcting an owner move
the destination shown beside it. A folder that cannot be adopted answers 200
with ok: false and field: "location"; 409 when the server is no longer a
wizard.
POST /api/wizard/undo
Puts an adopted folder back the way it was.
{ "folder": "~/Documents/projects/my-app" }
Not gated on mode, because undoing is asked for from the board the adoption
became. 200 with what was undone; 400 with refusals when it will
not remove something of torta’s you have edited since. partial and left
name anything still standing, so a caller reading only ok is not misled.
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" }.