Beta

API reference

SDKs (TypeScript and Python)

The agentower packages for TypeScript and Python — put any agent on the board from your own code, wait for an answer from the app, and list, message, approve and export from scripts.

The agentower packages put any agent on your team's board from your own code: agents built with Anthropic's Claude Agent SDK, OpenAI's Agents SDK or your own loop, CI jobs and scripts. They appear next to your Claude Code, Codex and Cursor agents, with the same states, and can wait for Allow or Deny, an answer or a message given in Agentower. The same packages list, message, approve and export from scripts.

Both use only the routes documented in this API reference, so anything they do you can also do with plain HTTP.

#Install

TypeScript (npm)
npm install agentower
Python (PyPI)
pip install agentower

Neither has dependencies. The TypeScript package runs anywhere fetch does: Node 20+, Bun, Deno and Cloudflare Workers. The Python package needs Python 3.9+ and has the same methods in snake_case (needs_you, ask_approval, wait_for_message); its calls block the thread that makes them, while reports go out from a background thread.

#Keys

Create keys on Keys & connections in the app. Each key does one job — see Keys and scopes:

You want toKeyUsed by
Report agents and wait for answersingestsession(), track(), report(), the adapters, askApproval, askQuestion, waitForMessage
List, message, approveagent-actions (owners and approvers; expires after 30 days)agents.*
Export dataexportexport.*
Read published counterspublic statsstats()
TypeScript
import { Agentower } from "agentower";

const tower = new Agentower({
  key: process.env.AGENTOWER_KEY, // the default
  tool: "release-bot",            // the name on the card
  project: "billing-api",         // created if new
});
Python
from agentower import Agentower

tower = Agentower(tool="release-bot", project="billing-api")  # key from AGENTOWER_KEY

The address defaults to AGENTOWER_URL, then https://ingest.agentower.com. It must be https:// — a plain http:// address is refused (TypeScript throws, Python raises ValueError), except localhost for local testing.

#Report an agent

A session is one card on the board. Each call moves it to a state:

TypeScript
const agent = tower.session();      // pass { id } to pick your own session ID
agent.start();                      // Starting
agent.working();                    // Working (repeats within 30 seconds aren't sent)
agent.needsYou("Check the release notes before I publish"); // Needs you
agent.idle();                       // Idle: finished, session still open
agent.failed("Tests failed");       // Failed
agent.done();                       // Done; done({ ok: false, reason }) ends as Failed
await tower.flush();                // before a script exits
Python
agent = tower.session()
agent.start()
agent.working()
agent.needs_you("Check the release notes before I publish")
agent.idle()
agent.done()
tower.flush()

Sessions never throw into your agent. Reports go out in order, in the background; one that can't be sent is dropped and passed to onError (by default, one warning). While a session is Working it sends a sign every 10 minutes, so it stays open on the board. Needs you reaches your notification channels like any other agent's.

Each call is one POST /v1/events report: the same limits apply, and reporting is always free.

#A whole job as one agent

track shows Starting, then Working, then Done — or Failed with the error, which it raises again:

TypeScript
await tower.track("nightly-import", async (agent) => {
  await importEverything();
});
Python
with tower.track("nightly-import") as agent:
    import_everything()

#Claude Agent SDK

For Anthropic's Claude Agent SDK, the adapter's hooks report each Claude session as one card:

TypeScript
import { query } from "@anthropic-ai/claude-agent-sdk";
import { Agentower } from "agentower";
import { claudeAgentSdk } from "agentower/claude-agent-sdk";

const agentower = claudeAgentSdk(new Agentower());

for await (const message of query({
  prompt: "Fix the failing test",
  options: {
    hooks: agentower.hooks,
    canUseTool: agentower.canUseTool(askMyTeam), // optional: Needs you while a person decides
  },
})) { /* … */ }
Python
from claude_agent_sdk import ClaudeAgentOptions, query
from agentower import Agentower
from agentower.claude_agent_sdk import claude_agent_sdk_hooks

agentower = claude_agent_sdk_hooks(Agentower())
options = ClaudeAgentOptions(hooks=agentower.hooks, can_use_tool=agentower.can_use_tool(ask_my_team))

async for message in query(prompt="Fix the failing test", options=options):
    ...

The card is filed under the session's folder name unless you pass { project }. Wrapping your own canUseTool makes the card show Needs you while a person decides; a callback that decides by itself within 1.5 seconds never shows it. Use one claudeAgentSdk() (claude_agent_sdk_hooks()) per query() running at the same time.

#OpenAI Agents SDK

TypeScript
import { Agent, Runner } from "@openai/agents";
import { Agentower } from "agentower";
import { reportResult, watchRunner } from "agentower/openai-agents";

const tower = new Agentower({ tool: "openai-agents", project: "support-bot" });
const agent = tower.session();
const runner = new Runner();
watchRunner(runner, agent);                        // Working while agents and tools run

const result = reportResult(agent, await runner.run(triage, input)); // Needs you if it stopped for an approval, else Idle
Python
from agents import Runner
from agentower import Agentower
from agentower.openai_agents import report_result, run_hooks

tower = Agentower(tool="openai-agents", project="support-bot")
agent = tower.session()

result = report_result(agent, await Runner.run(triage, input, hooks=run_hooks(agent)))

Use one Runner (one set of hooks) per session.

#Wait for an answer from the app

A session can ask for Allow or Deny, ask a multiple-choice question, or wait for a message — answered by a person in the agent's details panel, from any device. While it waits for a decision or an answer, the card shows Needs you ("Needs your permission to use deploy", "Asked you a question"), then Working again; while it waits for a message, it shows Idle.

TypeScript
const decision = await agent.askApproval({
  tool: "deploy",
  detail: "Deploy billing-api 2.4.1 to production",
});
if (decision === "allow") await deploy();

const answers = await agent.askQuestion([
  { question: "Which environment?", options: ["Staging", "Production"], multiSelect: false },
]);

const message = await agent.waitForMessage(); // { text, from, own } or null
Python
decision = agent.ask_approval("deploy", "Deploy billing-api 2.4.1 to production")
if decision == "allow":
    deploy()

answers = agent.ask_question([{"question": "Which environment?", "options": ["Staging", "Production"]}])

message = agent.wait_for_message()  # {"text", "from", "own"} or None
  • They never throw — except for a remote other than own or team (TypeScript throws, Python raises ValueError). null (None in Python) means nobody answered in time, or answering is off — your code decides by itself, as it would without Agentower.
  • How long they wait: approvals and questions 10 minutes, messages 60 minutes. Pass waitSeconds (Python: wait, in seconds) to change it: 10 seconds to 60 minutes.
  • Who may answer: remote: "own" (the default) lets only the person whose key reports the agent answer it; "team" lets the project's approvers and owners answer too.
  • Only when the workspace allows it: Agent chats must be on, and the person answering must be allowed to answer that project — see Who can answer.
  • Approve only what can be seen: pass complete: false (complete=False) when detail isn't the whole story of what the step will do, and the app offers Deny only. The Python package also sends false when it had to shorten the detail.
  • Until the agent's code first waits for an answer, its chat in the app says "This agent takes answers from Agentower once its code waits for one". A message sent before that waits until the code next calls waitForMessage.
  • An answer is handed over once, and only to the key of the person whose agent asked.
  • A question has 1–4 parts, each with up to 6 options; the answer has one item per part, in order: { labels: [...] } for options chosen, or { text } for the person's own words.
  • A message says who wrote it (from) and whether it came from the person running the agent (own). Treat a teammate's message as a request, not as consent.

Under the hood these use POST /v1/asks and GET /v1/asks/{id}.

#Act on agents (agent-actions key)

TypeScript
const ops = new Agentower({ key: process.env.AGENTOWER_AGENTS_KEY });

for (const a of await ops.agents.list({ state: "needs_you" })) {
  console.log(a.id, a.name, a.project, a.reason);
}

await ops.agents.allow("BIL-02", askId);   // or deny(...)
await ops.agents.message("BIL-02", "Run the tests again, then commit.");
await ops.agents.answer("BIL-02", askId, [{ labels: ["Not now"] }]);
await ops.agents.continue("BIL-02");       // past a guardrail's pause
Python
ops = Agentower(key=os.environ["AGENTOWER_AGENTS_KEY"])

for a in ops.agents.list(state="needs_you"):
    print(a["id"], a["name"], a["project"], a["reason"])

ops.agents.message("BIL-02", "Run the tests again, then commit.")
ops.agents.continue_("BIL-02")  # continue is a Python keyword

The rules are the same as in the app: the key acts as the person who made it, only on projects they may approve, and every action is in the audit log with the key's name. See Agent actions API. These calls count as API calls — see API pricing.

#Export (export key)

TypeScript
const data = new Agentower({ key: process.env.AGENTOWER_EXPORT_KEY });
for await (const row of data.export.rows("agents", { from: "2026-09-01" })) { /* every page */ }
const csv = await data.export.csv("usage_daily", { from: "2026-09-01", to: "2026-09-30" });

The datasets and their columns are listed in Export API.

#Errors

Reporting and waiting for answers never throw (apart from the setup mistakes above: a plain http:// address, or a wrong remote). Every other call — agents.*, export.*, stats() — throws an AgentowerError with status, code (Agentower's error, such as rate_limited or no_open_agent), hint and, for a 429, retryAfter in seconds. Every code is listed in Errors and limits and on each endpoint's page.

#What's sent

Only what you pass: the session ID, the event, the tool's name, the project and a short reason (one line, at most 160 characters). Keep secrets and content out of reasons. The adapters send states only — never prompts, or tools' input or output; when something needs permission, they also send the tool's name.

An approval or question also sends its detail or questions. They're held only while the request is open, shown only to people who may approve the project, and deleted once it's answered, expires or closes — like Claude Code's. See What Agentower sends and keeps.

#Source and licence

Both packages are open source under the MIT licence: github.com/Agentower/agentower-js (TypeScript) and github.com/Agentower/agentower-python (Python).