Other endpoints
Images
Attachment: { "id": 4, "name": "error.png", "mimeType": "image/png", "sizeBytes": 48211 }.
Types: image/png|jpeg|gif|webp (sniffed from the bytes), max 10 MB each, max 8 per task or follow-up. Stored in the database. An upload is staged until a task create/update attaches it or a follow-up sends it; staged uploads older than 24h are deleted hourly.
POST /api/uploads
Multipart form, field file (one image). 201 Attachment (staged). 400 not an accepted image type, empty, over 10 MB.
GET /api/attachments/:id
200 raw image bytes (Content-Type = mimeType, immutable cache, nosniff). 404.
DELETE /api/attachments/:id
Deletes a staged upload, or an image of a backlog task. 204. 404. 409 when the task has left backlog (images are read-only after launch).
Snippets
Reusable prompt text, inserted into prompts, follow-ups and notes by typing / in the web app. projectId null = global (shown in every project); set = only in that project's tasks.
Snippet:
{
"id": 1,
"name": "plan-first",
"body": "Before editing, write a plan and wait for approval.",
"projectId": null,
"createdAt": "2026-11-08T10:00:00Z",
"updatedAt": "2026-11-08T10:00:00Z"
}GET /api/snippets→ 200{ "snippets": Snippet[] }: global first, then by project title, thenname.POST /api/snippetsbody{ "name": string, "body": string, "projectId"?: number|null }→ 201Snippet. 400 blank name/body or unknownprojectId. 409 duplicate name (names are unique across all scopes).GET /api/snippets/:id→ 200Snippet. 404.PATCH /api/snippets/:idpartial of the POST body → 200Snippet. 400, 404, 409 as above.DELETE /api/snippets/:id→ 204. 404.
Preferences
New-task model defaults and the global notification filter. A new task prefills from pinned, else lastUsed, else the built-in default (T3 default model, full-access, default).
ModelDefaults:
{ "providerInstanceId": "claude", "model": "claude-haiku-5-5", "modelOptions": [{ "id": "effort", "value": "low" }], "runtimeMode": "full-access", "interactionMode": "default" }providerInstanceId / model null = T3 default (set together).
GET /api/preferences
200 { "pinned": ModelDefaults | null, "lastUsed": ModelDefaults | null, "notifyEvents": NotificationEvent[] }. lastUsed is written by every POST /api/tasks (its model fields). notifyEvents (default all: ["needs_input","failed","done","settled"], in that order) is the global filter of Notification rules.
PUT /api/preferences/pinned
Body: ModelDefaults (pin; runtimeMode / interactionMode default to full-access / default when empty) or null (unpin). Empty body 400.
200 Preferences. 400 invalid enum or provider/model not set together.
PUT /api/preferences/notifications
Body: { "events": NotificationEvent[] } (needs_input|failed|done|settled; duplicates dropped; [] = all off). Applies to every device.
200 Preferences. 400 unknown event or missing events.
Stats
GET /api/stats
Query: from, to (default last 30 days: to = now, from = now - 30d), tz (IANA zone for day buckets, default UTC), subagents (include|exclude, default include; exclude drops subagent threads from all thread counts). Thread counts use mirrored threads by createdAt in range; task counts are current totals (not range-filtered).
200 Stats:
{
"from": "2026-10-09T10:00:00Z",
"to": "2026-11-08T10:00:00Z",
"tz": "UTC",
"threadsByDay": [{ "day": "2026-11-08", "count": 14 }],
"threadsByProject": [{ "projectId": 1, "t3ProjectId": "proj_abc", "title": "stage", "count": 9 }],
"threadsByStatus": [{ "status": "completed", "count": 20 }],
"tasksByState": [{ "state": "running", "count": 2 }],
"needsInput": 1
}threadsByDay ascending, days with zero omitted. threadsByProject count desc; projectId null and title = t3ProjectId when no app project. needsInput = tasks in needs_input. 400 bad tz or subagents.
Agent interface
stage subcommands (CLI, --json), stage mcp (stdio MCP) and POST|GET /mcp (streamable HTTP MCP, stateless, served by the app) expose the same operations, all through this REST API (HTTP MCP calls the app's own /api on loopback). Guide: Agents.
Tools: get_context list_projects list_models list_tasks get_task create_task update_task delete_task start_task stop_task followup_task duplicate_task link_task order_tasks unlink_task wait_task list_schedules get_schedule create_schedule update_schedule delete_schedule run_schedule preview_schedule list_sessions search_sessions get_session get_session_questions.
get_session_questions/stage sessions questions IDread pending questions; agents cannot answer them.search_sessions(stage sessions search QUERY [--project P] [--limit N]) wrapsGET /api/threads/searchand returns{ results: [{ t3ThreadId, title, match, snippet?, project?, state?, taskId?, lastWordPreview?, t3WebUrl? }], t3 }.- Results are the REST shapes (lists as compact rows;
list_tasksandlist_sessionsrows carrylastWordPreview,get_taskandget_sessioncarry the excerpt inthread.lastWord, and the CLI prints it as "last word";get_taskandget_sessionalso carrypullRequestsandthread.recentRuns, which the CLI prints as aprsblock and arunsblock: newest 10,(10 of N runs),--jsonhas every stored run); errors are tool errors carrying the APIerror. - Caller:
T3CODE_THREAD_IDand cwd from the environment (CLI, stdio), or tool argscallerThreadId/cwd(override; required for context over HTTP). - Project: explicit
project(id or exact title) > the caller session's project (GET /api/threads/:id) > the project whoseworkspaceRootis the longest prefix of cwd. - Model: explicit (
providerinferred frommodelvia the catalog) >preferences.pinned>preferences.lastUsed> T3 default. get_sessionreturnscreatedTasks(see Session batch);get_taskreturnsbatch.order_taskstakestaskIdsand returns{ "tasks": Task[] }.create_task/create_schedulesetoriginThreadIdto the caller session.afterMe=afterThreadIdof the caller session (error without one).create_task,update_taskandlink_tasktake optionalhandoff(CLItask add|link --handoff=false,task edit --handoff=true|false); omitted keeps the task's value (default true).get_taskreturnshandoffandcurrentHandoff;stage task showprintshandoff on|offfor linked tasks and the current attempt's record.create_task,create_schedule,update_task,update_scheduleandlink_tasktake optionalbase(ref|predecessor; CLItask add|edit --base). On create it needsbaseRef(the fallback); onupdate_task/link_taskwithoutbaseRefit keeps the task's current worktree (error for a root task).get_taskreturnscurrentLaunchBase;stage task showprintsworktree from #12's branch (fallback origin/main, branch -)and, once launched,base feat/api (from #12)orbase origin/main (fallback: <reason>).imagePathsreads local files and uploads them (POST /api/uploads); CLI and stdio only (HTTP MCP rejects them).wait_taskpollsGET /api/tasks/:idevery 3s until the state is inuntil(defaultneeds_input done settled failed interrupted cancelled) ortimeoutSeconds(default 600, max 3600); CLItask waitexits 3 on timeout.