The HTTP API#
Prepcook's server is the board's own backend; most of its routes exist for the board's pages and change without notice. The endpoints on this page are the ones meant for scripts and other services — a support agent that files cards, an incident tool, a CI job — and they are kept stable.
All of them are on the local server (http://localhost:$PORT, where PORT is
PREPCOOK_PORT, 4100 by default) under /api, take and return JSON, and
answer in one envelope:
{ "success": true, "data": { … }, "error_data": null, "message": null }A failure has "success": false and says why in message.
Authentication#
The card, approval and evidence endpoints accept two callers:
- A service, sending
Authorization: Bearer <token>, where the token is thePREPCOOK_API_TOKENsecret (prepcook secret set PREPCOOK_API_TOKEN). A request with a bearer token is judged on the token alone, compared in constant time; with no token set, every bearer request is refused. - The board owner: a request with no
Authorizationheader, accepted while the server holds a board secret. This is how the board's own pages call them.
Filing a card (POST /api/cards) takes the token only. A refused request is a
401 with the reason in message. See approvals.md.
The script and app-run endpoints at the end of this page are the local server's own, for the Settings page and for agents running on the same machine; they take no token.
Cards#
File a card#
POST /api/cardsPuts a card on a board for the orchestrator to work, assigned to the board's user. The body:
| Field | Required | Meaning |
|---|---|---|
project_id |
no | The board. Defaults to orchestrator.default_board, else the first board under Settings → Orchestrator. |
title |
yes | The card's title, up to 255 characters. |
description |
no | Markdown. |
work_class |
yes | investigation, bug or story. |
origin |
yes | { "kind": "tammy" | "incident" | "api", "ref": string, "reporter"?: string } — who filed it, and the filer's own reference (a thread URL, an incident id). Stored on the card and shown on it: "Filed by Tammy from thread". Cards PagerDuty incidents become in incidents mode carry incident and the incident's page (incidents.md). |
attachments |
no | [{ "name": string, "url": string }], each an http(s) URL. Listed as links in the description, where the planning agent sees them. |
metadata |
no | An object kept on the card under external; it never stands in for a workflow key. |
start |
no | true places the card in Plan, where the orchestrator starts on it within a poll; false (the default) leaves it in To Do. |
curl -sS -X POST "http://localhost:$PORT/api/cards" \
-H "Authorization: Bearer $PREPCOOK_API_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"title": "CSV export drops the last row",
"description": "Acme says every export over 500 rows is one short.",
"work_class": "investigation",
"origin": {
"kind": "tammy",
"ref": "https://acme.slack.com/archives/C0123456/p1760000000000100",
"reporter": "Grace Hopper"
},
"attachments": [{ "name": "export.csv", "url": "https://files.example.com/export.csv" }],
"metadata": { "customer": "acme" },
"start": true
}'201 Created:
{
"success": true,
"data": {
"id": "3f1e9b52-0000-4000-8000-000000000002",
"url": "http://localhost:4100/projects/7d0c6a0e-0000-4000-8000-000000000001/issues/3f1e9b52-0000-4000-8000-000000000002",
"key": "PC-42"
},
"error_data": null,
"message": null
}| Status | When |
|---|---|
| 201 | Filed. |
| 400 | The body is not valid: a blank or overlong title, a class other than the three, a blank origin.ref, an attachment without a name or an http(s) URL, metadata that is not an object — or no project_id and no board configured. Nothing was written. |
| 401 | No bearer token, or the wrong one. |
| 404 | No such board. |
| 409 | The board has no Plan column (start: true) or no To Do column. |
| 503 | The board could not be reached. |
Read a card#
GET /api/cards/{id}{
"success": true,
"data": {
"id": "3f1e9b52-0000-4000-8000-000000000002",
"key": "PC-42",
"title": "CSV export drops the last row",
"url": "http://localhost:4100/projects/7d0c6a0e-…/issues/3f1e9b52-…",
"project_id": "7d0c6a0e-0000-4000-8000-000000000001",
"status": "triaged",
"column": "Plan",
"work_class": "investigation",
"report": {
"what_happens": "Exports over 500 rows stop one row short.",
"where": "Reports → Export, CSV only; XLSX is complete.",
"why": "The exporter pages by 500 and stops when a page comes back full, so the last partial page is never fetched.",
"scope": "Every CSV export over 500 rows, every account. A defect, not configuration: the harness fails on a fresh seed.",
"workaround": "Export as XLSX.",
"confidence": "High — reproduced on main, and the candidate change makes the harness pass.",
"markdown": "TRIAGE REPORT\n\n## What happens\n…"
},
"confidence": "high",
"harness": { "path": ".repro/PC-42/repro.mjs", "outcome": "reproduced" },
"comments": 6,
"rca_id": "9c1d2e3f-0000-4000-8000-000000000003",
"origin": {
"kind": "tammy",
"ref": "https://acme.slack.com/archives/C0123456/p1760000000000100",
"reporter": "Grace Hopper"
}
},
"error_data": null,
"message": null
}status is where the card is in the workflow — backlog, planning,
triaged, executing, reviewing, pushed_mr, closed, cleanup, or
other for a column Prepcook does not know — and column is the column's name
as the board shows it. Triage and the merge are states of the card, so
status reads them from the card as well as its column: a triaged
investigation is triaged in Plan (or in a Triaged column a board added), and
a card in Shipped is pushed_mr while its pull request is open and closed once
it merged (method.md). report, confidence and harness are null until an
investigation has posted its report; rca_id is null until an RCA is filed
for the card. 404 for no such card.
Rather than polling, a filer can take the investigation_completed event
(notifications.md), which carries
the same report and the card's origin as source.
Convert a card#
POST /api/cards/{id}/convertTurns an investigation card into a bug — or, less often, a story. The body:
{ "work_class": "bug", "by": "Ada Lovelace" }by is optional (it defaults to "an API caller", or "the board owner" for a
token-less request) and is recorded as given. The card's class tag is swapped
and everything else stays: its comments, attachments, RCA, harness and triage
report. Then:
- To a bug, with a plan. When the investigation's RCA verdict is at high
confidence and its harness is still on disk, they are the plan: the
conversion is recorded as the plan's approval (
approvals,source: api) and the card moves to Execute, wherebug:executingstarts in the same worktree. Planning does not run again. - Otherwise — a bug without that, or any story — the card moves to Plan and is planned from scratch as its new class.
curl -sS -X POST "http://localhost:$PORT/api/cards/$CARD_ID/convert" \
-H "Authorization: Bearer $PREPCOOK_API_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"work_class": "bug", "by": "Ada Lovelace"}'{
"success": true,
"data": {
"id": "3f1e9b52-0000-4000-8000-000000000002",
"from": "investigation",
"to": "bug",
"column": "Execute",
"plan_carried_over": true
},
"error_data": null,
"message": null
}The card gets a convert: comment, and a card_converted event goes to the
board's notification targets. On the board, the same action is Convert to
bug in the menu of the card's class label.
| Status | When |
|---|---|
| 200 | Converted. |
| 400 | work_class is not bug or story. |
| 401 | A wrong bearer token, or no token while the server has no board secret. |
| 404 | No such card. |
| 409 | The card is not an investigation, or the board lacks the column it would move to. |
| 503 | The board could not be reached. |
Plan decisions#
POST /api/approvals/{issue_id}
GET /api/approvals/{issue_id}Approve a plan, ask for changes, or re-plan, from outside the board; GET
lists every decision recorded on the card. The body is
{ "decision": "approve" | "request_changes" | "replan", "comment"?: string, "by"?: string }
and the answer the ApprovalRecord written. An investigation card takes only
replan (it has no gate; convert it instead). The full contract, with every
status, is in approvals.md.
Evidence#
POST /api/evidence/search searches a board's evidence sources — the global
ones it keeps, its own, and its repository's [[profile.evidence]] — and
merges what they found (evidence.md). Agents reach it through
the evidence_search tool.
{
"pattern": "SeedMissing",
"from": "2026-10-18T08:00:00Z",
"to": "2026-10-18T12:00:00Z",
"source": "Pantry app log",
"limit": 50,
"area": "Sharing",
"project_id": "<board id>",
"worktree": "/path/to/the/worktree"
}Only pattern is required. to defaults to now and from to a day before
to; source names one source by label or kind; limit is 1 to 200 (50);
area skips sources that cover other product areas; project_id defaults to
the default board (orchestrator.default_board, else the first board);
worktree, when it is inside a workspace or the board's repository, is where
log_files paths resolve and whose manifest is read. The answer is
{ hits, truncated, sources }, described in
evidence.md. An unknown source or a bad window
is a 400, a board with no source a 404.
Scripts and app runs#
These drive a repository's .prepcook/scripts.toml (scripts.md), on
the reproduction target the repository's board uses (local or remote).
| Endpoint | What it does |
|---|---|
GET /api/repos/{repo_id}/scripts |
The repository's parsed manifest, or the error that stopped it, plus required_secrets_missing, the board's target, and target_problem when that target cannot run the manifest. |
POST /api/repos/{repo_id}/scripts/run |
Runs one step in the repository's checkout: { "step": "setup" | "build" | "start" | "ready" | "seed" | "test" | "stop" | "stand-up" }. Returns { "run_id": … } at once. start and ready tear the app down once it is ready; on a remote target stand-up deploys, seeds and destroys. |
GET /api/script-runs/{run_id} |
That run's state; report is set once finished_at is. |
GET /api/script-runs/{run_id}/raw-logs/ws |
A WebSocket streaming the run's output. |
POST /api/app-runs |
{ "worktree": "<path>" }: stands the app up in a workspace's worktree for the agent running there, which owns it until its process ends. Refused when an app already runs for that worktree, the path is not in a workspace, or no agent is running there. Returns the run's status (URL, target_kind, port for a local app, seed variable names) and the stand-up report. |
GET /api/app-runs/current?worktree=<path> |
The app running for that worktree, with health from its target's ready check, or null. |
POST /api/app-runs/{run_id}/restart |
Stands a running app up again from the worktree's current files (a remote one is redeployed with [app.remote] restart, or stopped and deployed again); same answer as POST /api/app-runs. |
Agents reach the last three through the app_status and app_restart tools
rather than over HTTP.