Skip to content

HTTP API

The daemon owns all task state. The dashboard, the natamux command and the agents’ hooks all use this API.

  • Base URL: http://127.0.0.1:4317 by default (NATAMUX_PORT changes the port). When the daemon serves the dashboard (the desktop app, or Docker), the API is also under /api on the dashboard’s address.

  • Send the token in a header on every request:

    Terminal window
    curl -H "Authorization: Bearer $NATAMUX_TOKEN" http://127.0.0.1:4317/tasks
  • GET /health needs no token and answers {"status":"ok"}.

  • A wrong or missing token gets 401.

Task terminals get a second token, the agent token, in NATAMUX_TOKEN. The daemon makes a new one at each start and never prints it or writes it to disk. Agents and the natamux command in a task terminal use it, so the history shows what an agent did.

With the agent token:

  • POST /tasks/:id/events records source: agent, whatever source the request names.
  • Stage actions record source: agent.
  • These answer 403 forbidden and change nothing:
    • POST /tasks/:id/events with review.approved, stage.approved, stage.passed, stage.skipped or state.overridden
    • POST /tasks/:id/stage with approve, skip or pass
    • PUT /tasks/:id/ci, PUT /tasks/:id/skip, PUT /tasks/:id/workflow
    • PUT /repos/:id/settings

To do these from a script, use the daemon’s own token, not the one in a task terminal.

Method and path Meaning
GET /tasks?state=<state> List tasks
POST /tasks Create a task (title is required)
GET /tasks/:id One task
DELETE /tasks/:id Delete the task, its notes and its history. A workspace is removed too. Answers 409 when that workspace has changes that are not committed, and the task stays
PATCH /tasks/:id Change title, description, priority, labels or dependsOn
GET /tasks/:id/events The event log of a task
POST /tasks/:id/events Send an event { source, type, payload? }. Answers { task, accepted }
POST /tasks/:id/stage A stage action { action, stage?, terminal?, to?, reason? }. Actions: approve, reopen, skip, pass, fail, report, start
PUT /tasks/:id/ci Whether a stage waits for CI before it passes { stage, on }. Answers 400 when the stage does not offer it
PUT /tasks/:id/skip Whether a stage not reached yet is skipped when the task reaches it { stage, on }. Answers 400 when marking the current stage or an earlier one or the context stage, and for any change on a task that is over
PUT /tasks/:id/workflow Whether a stage runs as a workflow the next time it starts { stage, on }. Answers 400 when the stage does not offer it or the agent is not Claude Code
POST /tasks/:id/workspace Create the worktree and branch. Body { base? }
DELETE /tasks/:id/workspace?deleteBranch=true Remove the worktree
POST /tasks/:id/agent Start an agent { agent, prompt?, base? }
GET /tasks/:id/changes Files changed in the workspace. The answer is kept for 5 seconds; ?fresh=1 reads git now
GET /tasks/:id/graph The history of the task branch next to the base branch. Kept and ?fresh=1 as for changes
POST /tasks/:id/commit-message A commit message for the changes, by the task’s agent: { message, source, agent, reason? }. source is draft when the agent failed and the message is a simple one; reason says why. Answers 409 when nothing changed
POST /workspaces Make a task with a worktree and optionally start an agent
POST /hooks/:agent Where an agent’s hook posts. Always answers 200 {} for a known agent
GET /repos Projects Natamux manages, and the daemon’s worktree folder: { repos, worktreesDir }. A project with its own worktree folder has worktreesDir
POST /repos Add a project { path, name?, worktreesDir? }. worktreesDir is an absolute folder for the project’s worktrees; it is made when missing. Answers 400 when it is not absolute or cannot be made
DELETE /repos/:id Forget a project. Any project can be removed. Answers 409 while it has workspaces
GET /repos/:id/settings The settings of a project: settings, error, path, text (the text of .natamux.json, or null without a file) and version (none without a file)
PUT /repos/:id/settings Save the settings to .natamux.json. Send the version you read in the header If-Match: <version>. Answers { settings, version }. Answers 409 conflict when the file changed since that version. Keys of the file that Natamux does not know are kept
GET /repos/:id/skills The skills of every agent for the project: { skills, notices }. A skill is { name, description, origin, agents, path }, and origin is project, plugin or user. A notice says, for example, that a plugin is on but not installed

The task is named in a hook by the X-Natamux-Task header. The X-Natamux-Terminal header names the terminal.

Files, git, GitHub, terminals and templates.

Method and path Meaning
GET /tasks/:id/files The stage files of a task: { files }
GET /tasks/:id/files/:name One stage file, such as spec.md: { name, text }. Answers 404 when it does not exist
PUT /tasks/:id/files/:name Replace a stage file { text }, for example to fix the spec before you approve it
GET /tasks/:id/attachments The files attached to a task: { attachments }
GET /tasks/:id/attachments/:name One attachment. Images come with their type, any other file downloads
GET /tasks/:id/ended-shells The last screens of the shells of a done or cancelled task: { shells }
GET /tasks/:id/changes/file?path=<file> The diff of one changed file
GET /tasks/:id/changes/image?path=<file>&side=original|modified One side of a changed image
GET /tasks/:id/tree?path=<folder> One folder of the worktree: { entries }. No path is the root. .git is never listed
GET /tasks/:id/file?path=<file> One file of the worktree: { path, text, binary, tooLarge, version }
PUT /tasks/:id/file?path=<file> Save a file that exists { text }, with the version you read in If-Match. Answers 409 when the file changed since
POST /tasks/:id/agent/message Paste a comment into the agent’s terminal { text }. Answers 400 when no agent runs there
POST /tasks/:id/commit Commit all changes { message }
POST /tasks/:id/push Push the task branch
POST /tasks/:id/pull-request Open the pull request: { task }
POST /tasks/:id/pull-request/refresh Ask GitHub now for the state of the pull request, instead of waiting for the next check
POST /tasks/:id/issue Open a GitHub issue for the task now
PUT /tasks/:id/issue Link an issue that exists { provider, key, url }
POST /tasks/:id/fan-out Run more tries of a task { count, start? }. count is 1 to 5
GET /tasks/:id/terminals The open shells of a task: { terminals, agents, lanes, programs, restoring }
DELETE /tasks/:id/terminals/:terminal Close one shell. :terminal is its number
POST /tasks/:id/terminal/ticket A one-time ticket for the terminal WebSocket
GET /templates The saved task templates: { templates }
POST /templates Save a template
POST /templates/:id/tasks Make a task from a template
GET /github/issues?repo=<id> The open issues of a project: { issues }
POST /github/issues/import Import issues as draft tasks { numbers, repo? }. Up to 100 numbers
GET /git/branches?repo=<id> The branches of a project, for Branch from
GET /activity What the agent of each task does now, from its last hook
GET /diffstats The size of the changes of every workspace

A browser cannot set headers on a WebSocket, so:

  • GET /events (WebSocket) streams live task changes, including task.deleted when a task is deleted. Pass the token as ?token=<token>. Each message is JSON with a kind. Besides the task messages it sends:
    • repos.changed when a project is added or removed, so a dashboard can read GET /repos again.
    • activity.changed with { taskId, activity } when what an agent does changes. activity is the task’s whole activity by terminal, as GET /activity gives it; {} means none.
    • shells.changed with { taskId } when a terminal of the task starts or ends, when the program in front of a terminal changes (checked when its output goes quiet), when an agent that comes back after a restart starts or is up, or when a hook changes the state of an agent that works on no stage, unless the last GET /tasks/<id>/terminals found no agent in that terminal.
    • workspace.dirty with { taskId } when files in the task’s workspace may have changed: after an agent hook for a tool that ran or for the end of a turn, when its terminals go quiet, after a commit, or after a save in the editor. Each comes 750 ms after a change. Up to 3 in a row come that soon; while changes go on after that, at most one per task every 10 s, and one more after the last change.
    • resync to one socket that missed messages because it read too slowly. The dashboard loads everything again.
  • Terminals open with a one-time ticket. Call POST /tasks/:id/terminal/ticket to get { ticket }, then open GET /tasks/:id/terminal?ticket=... (WebSocket) within 30 seconds.

A request with a bad field answers 400 with an error that names the field and the reason, such as which field is too long.

The daemon README lists every endpoint with its body and errors: packages/daemon/README.md.