Skip to content
ttorta docs

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 prompt when prompt is empty.
  • 409 with existed: true when the pane is open but would not take the prompt, or existed: false with 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" }.