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": {},
  "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 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" }.