Beta

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.

QueryNotes
statestarting, 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
projectA project's name (any case)
Agents that need someone
curl "$AGENTOWER_URL/v1/agents?state=needs_you" -H "authorization: Bearer $AGENTOWER_AGENTS_KEY"
Response · 200
{
  "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, otherwise null.
    • id names the request: send it as ask_id to allow or deny.
    • detail — what the tool will do — is included only when the key's person may approve the agent's project.
    • complete: false means 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, MultiEdit or NotebookEdit, whose detail is only the file's path). Such a request can be denied here but must be allowed in the terminal. A plan (tool: "ExitPlanMode") is always complete: false here: 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) or team (the project's approvers too).
  • messageWaiting: true when a message is queued for it.

#GET /v1/agents/{id}

One open agent.

One agent
curl "$AGENTOWER_URL/v1/agents/ZZZ-99" -H "authorization: Bearer $AGENTOWER_AGENTS_KEY"
Response · 404
{ "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.

Message an agent
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."}'
json
{ "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.

json
{ "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.

Allow a request
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>"}'
json
{ "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 in options; several only if the question is multiSelect; or
  • { "text": "Wait until the migration has run." } — your own words, 1–2,000 characters, like "Other" in the terminal.
Answer a question
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"]}]}'
json
{ "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.

json
{ "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".

json
{ "ok": true, "agent": "BIL-02", "name": "Sam" }

#Errors

StatuserrorWhen
400invalid_message, invalid_decision, invalid_name, invalid_jsonMissing or wrong text (1–2,000 characters), decision (allow or deny) or name
400ask_id_requiredapprove or answer without the request's ask_id
400invalid_answeranswer with answers that don't fit the questions: one per question, in order, each offered labels or 1–2,000 characters of text
401unauthorizedWrong, revoked or expired key, or its person has left the team
403not_allowedThe key's person can't approve this project
403remote_not_allowedThe agent's computer accepts answers only from the person running it (own), and that isn't the key's person
403plan_endedThe workspace's Team plan ended and the key's person isn't the person who created the workspace — see Billing
404no_open_agent, not_open, nothing_waiting, not_foundNo such open agent, no message to take back, or an unknown path
409answering_offAgent chats is off, so this agent can't be answered from Agentower
409remote_offAnswers from Agentower are off on the agent's computer
409nothing_to_approve, nothing_to_answer, already_answeredNo open permission request or question, or it was just answered
409ask_changedThe open request isn't the one ask_id names — the agent is asking something else now
409approve_in_terminalallow for a request Agentower can't show in full (complete: false, including every file change — Write, Edit, MultiEdit, NotebookEdit); deny works
409approve_in_appallow for a plan: approve it in the app or the terminal, where it can be read; deny works
409not_pausedcontinue on an agent that isn't paused
409name_taken, ambiguous_nameAnother open agent already has that name, or the name you looked up is shared — use the short ID
429rate_limitedMore than 20 renames a minute in the workspace (on top of the 60 requests a minute per key)
413too_longTogether with a message already waiting, over 2,000 characters
413payload_too_largeA body over 8,000 bytes (40,000 for answer)
415unsupported_media_typemessage, approve, answer and name need content-type: application/json
422not_supportedMessaging a Codex or Cursor session
429rate_limitedMore than 60 requests a minute with this key
Wrong key
curl "$AGENTOWER_URL/v1/agents" -H "authorization: Bearer at_test_00000000000000000000000000000000"
Response · 401
{ "error": "unauthorized", "hint": "Send an agent-actions key: authorization: Bearer at_… (they expire after 30 days)" }