Local API

The local API lets a tool on this machine read and act on what the local manager sees: the agents running here, their files in use, runs, handoffs and messages. The Coordigent extension for VS Code is built on it. It speaks JSON, over a unix socket the daemon serves or through coord api.

Two ways in

WayHowUse it when
SocketHTTP/1.1 over the unix socket ~/.tower/daemon.sockThe daemon is running and current
CLIcoord api <method> --params '<json>', which prints one line of JSONThe socket is missing or refuses, or the daemon is older than the method you need

Both run the same handlers, so a method answers the same either way. For example:

coord api hello
coord api snapshot --params '{"all": true}'
curl --unix-socket ~/.tower/daemon.sock http://localhost/v1/hello

coord api always prints JSON, also on failure, as {"error": …}, and then exits with status 1.

If you set TOWER_DIR, the socket is $TOWER_DIR/daemon.sock. A unix socket path is limited to 104 bytes on macOS and 108 on Linux. Node cannot connect to a longer one, so a client that gets EINVAL should fall back to coord api.

Access

Access is by file permissions. The socket is readable and writable only by you (mode 0600), inside ~/.tower, which is yours alone. There is no TCP port and no token.

A request that changes something (POST or PUT) must send a header naming the client:

X-Coordigent-Client: my-tool/1.0.0

Without it the request is refused with 400. The daemon logs each such request with the client name, in ~/.tower/emit.log.

Methods

In a route, :sid and :id take a full id, or for an agent or run on this machine a unique prefix of at least 4 characters, with or without dashes, as the CLI does. A conflict id and an agent on another machine are named by their full id. Other parameters go in the query string for GET, or a JSON body for POST and PUT. With coord api, all of them go in --params. Query booleans are 1 or true.

Machine

MethodRouteParamsReturns
helloGET /v1/hello{apiVersion, coordVersion, buildSha, via, pid}; via is daemon or cli
snapshotGET /v1/snapshotallEverything the machine view shows; see Snapshot
doctorGET /v1/doctorcwd{ok, checks: [{kind, name, ok, lines}]}, the checks coord doctor runs in cwd
teamGET /v1/teamYour project as the daemon last heard it from the server; see Team
preferences.getGET /v1/preferences{desktopNotifications, autoConnectRepos}; autoConnectRepos is ask (the default), always or never
preferences.putPUT /v1/preferencesdesktopNotifications, autoConnectRepos, each optionalThe same; only the fields given change

Repos

MethodRouteParamsReturns
repos.connectPOST /v1/repos/connectroot{repoRoot, slug, registered, error}: connects the repo as coord connect does, writing no files. registered is false, with error, when logged out, without an origin remote, or refused. Answers the repo's Needs you question.
repos.declinePOST /v1/repos/declineroot{message}: answers the repo's question with no, for good
repos.initPOST /v1/repos/initpath{files, slug}: connects the repo at path as coord connect --repo-files --clients vscode does, and asks nothing
repos.unenrollPOST /v1/repos/unenrollpath{root, slug, ownerHint, undo}, as coord unenroll. ownerHint is true when you own the project holding the repo; undo is the command that turns it back on
repos.removePOST /v1/repos/removepath, purge (default false), confirm{slug, removed, sessions, unenrolled}, as coord repo remove without its questions. removed is soft or purged. purge: true needs confirm set to the repo's owner/name. A refusal, such as from someone who is not the owner, is refused with the CLI's line.

Agents

MethodRouteParamsReturns
sessions.diffGET /v1/sessions/:sid/diffThe agent's diff, as coord sessions diff --json prints it
sessions.stopPOST /v1/sessions/:sid/stop{message}. Stops the agent without asking: confirm with the person first.
sessions.messagePOST /v1/sessions/:sid/messagestext{message}; sends a message, as coord sessions message does
sessions.askPOST /v1/sessions/:sid/askquestion{answer, ok, notes}, as coord ask. Uses model credits.
sessions.reviewPOST /v1/sessions/:sid/reviewreviewer (optional){review, notes}, as coord review; review is null when nothing was stored. Uses model credits.
inboxGET /v1/inboxsession{sessionId, messages: [{message, source, state, deliveredAt}]}

inbox only reads. It does not deliver or mark anything. A message's state is pending until a hook delivers it to the agent, delivered after that, and expired if its time ran out first.

Project

These call the server with your login, with an 8-second limit.

MethodRouteParamsReturns
messages.sendPOST /v1/messagesto, text{message, via}. to is an agent's id, or project for everyone. An agent on this machine can be named by a prefix and gets the message through the local inbox (via: local); an agent on another machine needs its full id and goes through the server (via: server). Messaging everyone, or an agent on another machine, needs a login.
conflicts.dismissPOST /v1/conflicts/:id/dismiss{message}. Dismisses the conflict for everyone in the project. :id is the full id from team.
sessions.endPOST /v1/sessions/:sid/end{message}, as coord sessions end. Marks the agent ended on the server and clears its conflicts; nothing is stopped. Only its own person, the project's owner or an admin can end it.

Handoffs

MethodRouteParamsReturns
handoffs.listGET /v1/handoffssession (optional){handoffs}, newest first, as coord handoff --list
handoffs.getGET /v1/handoffs/:idOne handoff, as coord handoff --show

Reading a handoff does not use it up.

Runs

MethodRouteParamsReturns
runs.startPOST /v1/runstask, repo, mode, and optional client, from, allowTools, terminal{run, launch, notes}, as coord sessions start
runs.showGET /v1/runs/:iddiff{run, diff}, as coord runs show --json; marks the run seen
runs.stopPOST /v1/runs/:id/stopforce{message}, as coord runs stop
runs.discardPOST /v1/runs/:id/discardforce{message}, as coord runs discard
runs.prunePOST /v1/runs/prune{message}, as coord runs prune

For runs.start, mode is headless or interactive, client is claude-code (the default) or codex, from is the id of an agent on this machine whose work the run continues (or null), and allowTools is a list of tool names the agent may use.

An interactive run with terminal: "host" opens nothing itself. The response's launch holds {scriptPath, worktree, title}, and the run waits as starting until your tool runs /bin/sh <scriptPath> in a terminal of its own. Without terminal, an interactive run opens where coord sessions start --interactive would. The run's environment comes from the process that answers the call, which is the daemon for the socket, so a variable set only in your editor's shell is not passed on. See Runs.

Team

team answers from what the daemon keeps on disk; the call itself never reaches the server. The daemon holds a live connection to the server while logged in, so it is current within moments.

FieldIs
loggedInFalse with no login or a refused one; nothing else is sent then
revChanges when the team does; sent as the ETag, so If-None-Match gets 304
syncedAtWhen the daemon last read the team from the server
liveWhether the daemon's connection to the server is open now; always false from coord api
project{id, name}, as in identity
agentsEvery agent in the project: {id, userId, person, mine, repo, branch, tool, status, task, lastEventAt, files}. status is working, waiting, blocked, idle or ended; files lists its files in use, newest first
conflictsOpen conflicts: {id, repo, files, agents}
reposEach repo in the project: {id, slug, liveAgents, lastActivityAt}
needsYouYour agents that are waiting or blocked: {sessionId, status, repo, task, since}
messagesThe latest messages in the project, newest first
visibilityThe project's visibility, or null

A daemon older than a field leaves it out.

Events

GET /v1/events, on the socket only, is a Server-Sent Events stream that says when to read again. The first event is hello; then:

EventDataWhen
local-changed{rev}The snapshot changed
team-changed{rev}The team changed, or the daemon's connection to the server opened or dropped
message{sessionId, from, text}A message arrived for one of your agents
needs-you{sessionId, reason}Something new needs you

A : ping comment every 8 seconds keeps the connection open. Treat each event as a hint: read snapshot or team again rather than patching your state from it.

Snapshot

snapshot returns {rev, at, sessions, needsYou, runs, claims, unread, daemon, identity}:

  • sessions: the live agents on this machine and the recently ended ones, Needs you first. With all, every ended one too.
  • needsYou: the same list coord status shows.
  • runs: every run still going, then the 20 newest finished ones.
  • claims: the files in use right now, each with the agent that holds it.
  • unread: for each live agent with waiting messages, how many.
  • daemon: {pid, startedAt, jobs}, or null when no daemon is running.
  • identity: who this machine is signed in as.

identity lets a tool show the user and project without asking for an id:

FieldIs
loggedInWhether this machine holds a login the server has not refused
user{name, email}, or null until the server has answered for this login
project{id, name}, or null until then; the id is what dashboard links use
machineThis machine's hostname
serverUrlThe server this machine is logged in to, which also serves the dashboard; null when logged out

A snapshot never waits on the server. The daemon refreshes the user and project in the background and the next poll has them. A daemon older than identity leaves it out: treat that as not known.

rev changes exactly when something a view shows changes. The socket sends it as the ETag, and a request with If-None-Match: "<rev>" gets 304 when nothing changed. Poll as often as you like: a snapshot is built at most twice a second however many tools ask, and a POST or PUT starts a fresh one.

Errors

Every failure is {"error": {"code", "message", "wouldLose"}}, where wouldLose appears only on some refusals:

CodeHTTPWhen
bad_request400Params that do not parse, an ambiguous id prefix, a path that is not a git repo or has no origin remote, a missing client header, the wrong verb
not_found404No such method, route, agent, run or handoff
refused409A safety check said no: an ended agent, a run already going, uncommitted changes, a purge without the matching confirm, the server refusing a repo remove. message is the line the CLI would print. A discard refusal lists in wouldLose what forcing it would delete.
unsupported501A method this daemon knows but cannot serve
internal500Anything else; logged to ~/.tower/emit.log

Versioning

hello returns apiVersion, currently 5: 2 added team and the events stream, 3 repos.unenroll, 4 repos.remove, and 5 messages.send, conflicts.dismiss and sessions.end. It goes up when a method is added or a shape changes in a way an older tool cannot read. Check it on connect. When the daemon is older than you need, use coord api, or ask the person to run coord update.

Next