API reference
Agent actions API
/v1/agents — list open agents, message them, allow or deny their requests, answer their questions, and let them past a guardrail from your own tools.
The agent actions API does from a script what an approver can do in the details panel: see which agents are open, message one, allow or deny its permission request, answer its multiple-choice question, take back a waiting message, or let it continue past a guardrail. Scope: agents:act.
#The key
On Keys & connections, open Agent actions API (the sections there start closed) and create an agent-actions key.
- Only owners and approvers can create one, and only with a sign-in from the last 7 days.
- These keys expire after 30 days, because they can instruct agents on people's computers. Make a new one when it runs out.
- A key acts as the person who made it, with their current role: it can act only on projects they may approve (see Roles and approvers). If they're removed from the team, their keys stop working.
- Every action is recorded in the audit log with the key's name, for example "Allowed Bash for BIL-02 · via API key “release-bot”".
- 60 requests a minute per key.
The same rules as the app apply: messages and approvals need the workspace's Agent chats switched on, and messages reach Claude Code sessions and agents reported through the API or the SDKs — not Codex or Cursor. The agent itself has the last word too: unless whoever runs it allows answers from Agentower (own or team — on the computer for Claude Code and Codex, or in the agent's own ask for API and SDK agents), nothing from the API reaches it — see Who can answer.
Refer to an agent by its short ID, such as BIL-02, or by its name, such as Sarah (any case; URL-encode spaces and accents, e.g. Zo%C3%AB). The short ID never changes, so prefer it in scripts; a name two open agents share gets 409 ambiguous_name. Only open agents can be looked up or acted on.
#GET /v1/agents
Open agents across the team, optionally filtered.
| Query | Notes |
|---|---|
state | starting, working, needs_you, idle or failed. An agent whose turn has ended but whose background helper or command is still running counts as working; its background field is true |
project | A project's name (any case) |
curl "$AGENTOWER_URL/v1/agents?state=needs_you" -H "authorization: Bearer $AGENTOWER_AGENTS_KEY"{
"agents": [
{
"id": "BIL-02",
"name": "Sarah",
"project": "Billing API",
"person": "Maya",
"tool": "claude-code",
"model": "Opus 5.5",
"effort": "high",
"state": "needs_you",
"reason": "Needs your permission to use Bash",
"guardrail": null,
"minutesInState": 6,
"startedAt": "2026-09-30T08:47:00.000Z",
"tokens": 148000,
"subagents": [],
"link": "https://agentower.com/agents?agent=…",
"waitingFor": null,
"answers": "team",
"messageWaiting": false
}
],
"count": 1
}Each agent has the same fields as in the MCP server, plus:
waitingFor:{ "approval": { "id": "…", "tool": "Bash", "detail": "npm run deploy:staging", "complete": true } }when it has an open permission request,{ "question": { "id": "…", "questions": […] } }when it's asking a multiple-choice question (see below),"a message"when it's listening for a message, otherwisenull.idnames the request: send it asask_idto allow or deny.detail— what the tool will do — is included only when the key's person may approve the agent's project.complete: falsemeans Agentower can't show all of what allowing it would let through (the detail was cut to fit, invisible characters were removed from it, the tool's input isn't shown at all — MCP tools, Task, plan approvals — or it's a file change:Write,Edit,MultiEditorNotebookEdit, whosedetailis only the file's path). Such a request can be denied here but must be allowed in the terminal. A plan (tool: "ExitPlanMode") is alwayscomplete: falsehere: the API never carries the plan's text, so read and approve it in the app or the terminal.question.questions— the questions and their options — is included only when the key's person may approve the agent's project. Each is{ "question": "Deploy to staging now?", "options": ["Yes", "Not now"], "multiSelect": false }: up to 4 questions, each with up to 6 option labels (never their descriptions).
answers: what the agent's computer accepts from Agentower —off(nothing),own(only the person running it) orteam(the project's approvers too).messageWaiting:truewhen a message is queued for it.
#GET /v1/agents/{id}
One open agent.
curl "$AGENTOWER_URL/v1/agents/ZZZ-99" -H "authorization: Bearer $AGENTOWER_AGENTS_KEY"{ "error": "no_open_agent", "hint": "Use the agent's short ID (e.g. BIL-02) or its name. Only open agents." }When it exists, the answer is one agent in the shape above.
#POST /v1/agents/{id}/message
Sends a message, up to 2,000 characters. It arrives the way a message from the app does — see Answer agents from Agentower.
curl -X POST "$AGENTOWER_URL/v1/agents/BIL-02/message" \
-H "authorization: Bearer $AGENTOWER_AGENTS_KEY" -H "content-type: application/json" \
-d '{"text":"Run the tests again, then commit."}'{ "ok": true, "delivered": "now" }"delivered": "now" means it was listening and gets the message within a minute; "queued" means it goes out when the agent finishes its current step. The agent is told who wrote it: if the key's person runs that session, it reads as from them; otherwise it reads "Message from Sam (approver), a teammate using Agentower (not the person running this session)". On a computer set to own, only the key's person's own messages to their own agents get through; others are refused with remote_not_allowed.
#DELETE /v1/agents/{id}/message
Takes back a message that's still waiting. You can take back your own; owners can take back anyone's.
{ "ok": true }#POST /v1/agents/{id}/approve
Answers the agent's open permission request. Works for Claude Code, Codex and agents reported through the API or the SDKs that are waiting for one.
Body: decision (allow or deny) and ask_id — the request's id from GET /v1/agents/{id} (waitingFor.approval.id). Naming the request means an answer meant for one request can never land on the next one: if the agent is asking something else by then, you get 409 ask_changed — look it up again.
curl -X POST "$AGENTOWER_URL/v1/agents/BIL-02/approve" \
-H "authorization: Bearer $AGENTOWER_AGENTS_KEY" -H "content-type: application/json" \
-d '{"decision":"allow","ask_id":"<waitingFor.approval.id>"}'{ "ok": true, "decision": "allow", "tool": "Bash" }As in the app, the first answer wins: if someone answered in the terminal first, there's nothing_to_approve. allow works only when waitingFor.approval.complete is true; otherwise it's refused with 409 approve_in_terminal, and deny still works. File changes (Write, Edit, MultiEdit, NotebookEdit) are always complete: false, so they can only be denied here. For a plan whose text Agentower holds, allow is refused with approve_in_app instead: approve it in Agentower or the terminal, where it can be read. deny works for plans too.
#POST /v1/agents/{id}/answer
Answers the agent's open multiple-choice question (Claude Code's question tool), exactly as if it had been answered in the terminal.
Body: ask_id — the question's id from GET /v1/agents/{id} (waitingFor.question.id) — and answers: one answer per question, in the order asked. Each answer is either:
{ "labels": ["Not now"] }— one of that question's offered options, exactly as listed inoptions; several only if the question ismultiSelect; or{ "text": "Wait until the migration has run." }— your own words, 1–2,000 characters, like "Other" in the terminal.
curl -X POST "$AGENTOWER_URL/v1/agents/BIL-02/answer" \
-H "authorization: Bearer $AGENTOWER_AGENTS_KEY" -H "content-type: application/json" \
-d '{"ask_id":"<waitingFor.question.id>","answers":[{"labels":["Not now"]}]}'{ "ok": true }The first answer wins: if it was answered in the terminal first, or the agent moved on, there's nothing_to_answer; if it's asking a different question by then, ask_changed. An answer that doesn't fit the questions — the wrong number of answers, a label that wasn't offered, several labels for a single-choice question — is refused with invalid_answer. Recorded in the audit log without the answer, for example "Answered a question for Sarah (BIL-02) · via API key “release-bot”". Body up to 40,000 bytes.
#POST /v1/agents/{id}/continue
Lets an agent that a guardrail paused carry on for the rest of its session. No body.
{ "ok": true }#POST /v1/agents/{id}/name
Renames an agent. Body: { "name": "Sam" } — 1–24 characters: letters, digits, spaces, hyphens and apostrophes, starting with a letter. Two open agents can't share a name. The short ID stays the same. Recorded in the audit log, for example "Renamed BIL-02 from Sarah to Sam".
{ "ok": true, "agent": "BIL-02", "name": "Sam" }#Errors
| Status | error | When |
|---|---|---|
| 400 | invalid_message, invalid_decision, invalid_name, invalid_json | Missing or wrong text (1–2,000 characters), decision (allow or deny) or name |
| 400 | ask_id_required | approve or answer without the request's ask_id |
| 400 | invalid_answer | answer with answers that don't fit the questions: one per question, in order, each offered labels or 1–2,000 characters of text |
| 401 | unauthorized | Wrong, revoked or expired key, or its person has left the team |
| 403 | not_allowed | The key's person can't approve this project |
| 403 | remote_not_allowed | The agent's computer accepts answers only from the person running it (own), and that isn't the key's person |
| 403 | plan_ended | The workspace's Team plan ended and the key's person isn't the person who created the workspace — see Billing |
| 404 | no_open_agent, not_open, nothing_waiting, not_found | No such open agent, no message to take back, or an unknown path |
| 409 | answering_off | Agent chats is off, so this agent can't be answered from Agentower |
| 409 | remote_off | Answers from Agentower are off on the agent's computer |
| 409 | nothing_to_approve, nothing_to_answer, already_answered | No open permission request or question, or it was just answered |
| 409 | ask_changed | The open request isn't the one ask_id names — the agent is asking something else now |
| 409 | approve_in_terminal | allow for a request Agentower can't show in full (complete: false, including every file change — Write, Edit, MultiEdit, NotebookEdit); deny works |
| 409 | approve_in_app | allow for a plan: approve it in the app or the terminal, where it can be read; deny works |
| 409 | not_paused | continue on an agent that isn't paused |
| 409 | name_taken, ambiguous_name | Another open agent already has that name, or the name you looked up is shared — use the short ID |
| 429 | rate_limited | More than 20 renames a minute in the workspace (on top of the 60 requests a minute per key) |
| 413 | too_long | Together with a message already waiting, over 2,000 characters |
| 413 | payload_too_large | A body over 8,000 bytes (40,000 for answer) |
| 415 | unsupported_media_type | message, approve, answer and name need content-type: application/json |
| 422 | not_supported | Messaging a Codex or Cursor session |
| 429 | rate_limited | More than 60 requests a minute with this key |
curl "$AGENTOWER_URL/v1/agents" -H "authorization: Bearer at_test_00000000000000000000000000000000"{ "error": "unauthorized", "hint": "Send an agent-actions key: authorization: Bearer at_… (they expire after 30 days)" }