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
| Way | How | Use it when |
|---|---|---|
| Socket | HTTP/1.1 over the unix socket ~/.tower/daemon.sock | The daemon is running and current |
| CLI | coord api <method> --params '<json>', which prints one line of JSON | The 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 hellocoord api snapshot --params '{"all": true}'curl --unix-socket ~/.tower/daemon.sock http://localhost/v1/hellocoord 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.0Without 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
| Method | Route | Params | Returns |
|---|---|---|---|
hello | GET /v1/hello | {apiVersion, coordVersion, buildSha, via, pid}; via is daemon or cli | |
snapshot | GET /v1/snapshot | all | Everything the machine view shows; see Snapshot |
doctor | GET /v1/doctor | cwd | {ok, checks: [{kind, name, ok, lines}]}, the checks coord doctor runs in cwd |
team | GET /v1/team | Your project as the daemon last heard it from the server; see Team | |
preferences.get | GET /v1/preferences | {desktopNotifications, autoConnectRepos}; autoConnectRepos is ask (the default), always or never | |
preferences.put | PUT /v1/preferences | desktopNotifications, autoConnectRepos, each optional | The same; only the fields given change |
Repos
| Method | Route | Params | Returns |
|---|---|---|---|
repos.connect | POST /v1/repos/connect | root | {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.decline | POST /v1/repos/decline | root | {message}: answers the repo's question with no, for good |
repos.init | POST /v1/repos/init | path | {files, slug}: connects the repo at path as coord connect --repo-files --clients vscode does, and asks nothing |
repos.unenroll | POST /v1/repos/unenroll | path | {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.remove | POST /v1/repos/remove | path, 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
| Method | Route | Params | Returns |
|---|---|---|---|
sessions.diff | GET /v1/sessions/:sid/diff | The agent's diff, as coord sessions diff --json prints it | |
sessions.stop | POST /v1/sessions/:sid/stop | {message}. Stops the agent without asking: confirm with the person first. | |
sessions.message | POST /v1/sessions/:sid/messages | text | {message}; sends a message, as coord sessions message does |
sessions.ask | POST /v1/sessions/:sid/ask | question | {answer, ok, notes}, as coord ask. Uses model credits. |
sessions.review | POST /v1/sessions/:sid/review | reviewer (optional) | {review, notes}, as coord review; review is null when nothing was stored. Uses model credits. |
inbox | GET /v1/inbox | session | {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.
| Method | Route | Params | Returns |
|---|---|---|---|
messages.send | POST /v1/messages | to, 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.dismiss | POST /v1/conflicts/:id/dismiss | {message}. Dismisses the conflict for everyone in the project. :id is the full id from team. | |
sessions.end | POST /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
| Method | Route | Params | Returns |
|---|---|---|---|
handoffs.list | GET /v1/handoffs | session (optional) | {handoffs}, newest first, as coord handoff --list |
handoffs.get | GET /v1/handoffs/:id | One handoff, as coord handoff --show |
Reading a handoff does not use it up.
Runs
| Method | Route | Params | Returns |
|---|---|---|---|
runs.start | POST /v1/runs | task, repo, mode, and optional client, from, allowTools, terminal | {run, launch, notes}, as coord sessions start |
runs.show | GET /v1/runs/:id | diff | {run, diff}, as coord runs show --json; marks the run seen |
runs.stop | POST /v1/runs/:id/stop | force | {message}, as coord runs stop |
runs.discard | POST /v1/runs/:id/discard | force | {message}, as coord runs discard |
runs.prune | POST /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.
| Field | Is |
|---|---|
loggedIn | False with no login or a refused one; nothing else is sent then |
rev | Changes when the team does; sent as the ETag, so If-None-Match gets 304 |
syncedAt | When the daemon last read the team from the server |
live | Whether the daemon's connection to the server is open now; always false from coord api |
project | {id, name}, as in identity |
agents | Every 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 |
conflicts | Open conflicts: {id, repo, files, agents} |
repos | Each repo in the project: {id, slug, liveAgents, lastActivityAt} |
needsYou | Your agents that are waiting or blocked: {sessionId, status, repo, task, since} |
messages | The latest messages in the project, newest first |
visibility | The 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:
| Event | Data | When |
|---|---|---|
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. Withall, every ended one too.needsYou: the same listcoord statusshows.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:
| Field | Is |
|---|---|
loggedIn | Whether 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 |
machine | This machine's hostname |
serverUrl | The 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:
| Code | HTTP | When |
|---|---|---|
bad_request | 400 | Params 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_found | 404 | No such method, route, agent, run or handoff |
refused | 409 | A 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. |
unsupported | 501 | A method this daemon knows but cannot serve |
internal | 500 | Anything 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
- Local manager describes the daemon and the machine view this API serves.
- Extension manager is the VS Code extension built on it.