Two audiences, two credentials
The API serves running agents and dashboard users, and they authenticate differently. The separation is enforced per endpoint: an agent key cannot reach a project route, and a user token cannot reach an agent route.
| Caller | Header | Endpoints |
|---|---|---|
| Agent | X-AgentWay-Key: ak_… |
/v1/agents/*, /v1/directives/*/respond, /v1/inbox |
| Dashboard user | Authorization: Bearer <jwt> |
/v1/projects/** |
Agent requests also need
X-AgentWay-Agent. A scope key covers every
agent in its scope, so after registration each request must name
which agent is calling — the value is the agent's slug. The server
verifies that agent lives in the key's scope. The Python SDK sets
this for you.
Agent endpoints
POST /v1/agents/register
Create or reclaim an agent. The only endpoint authenticated by key
alone, since the agent doesn't exist yet. Idempotent on
(project, slug).
{
"project_id": "aw_x7k2m9p4qa",
"scope": "data",
"slug": "invoice-processor",
"name": "Invoice Processor",
"capabilities": ["ocr"],
"runtime": "python/3.13",
"sdk_version": "0.1.0",
"host": "worker-01"
}
{
"agent_id": "…uuid…",
"slug": "invoice-processor",
"status": "idle",
"control_epoch": 0,
"heartbeat_interval_s": 15,
"paused": false,
"scope": "data",
"scope_alias_used": false
}
paused: true means an operator's pause survived a restart —
honour it before doing any work. scope_alias_used: true
means the scope was renamed and your code names the old one; still
works, but update the source.
Errors: 422 on project id mismatch or unknown scope,
403 if the key's scope doesn't match, 401 on a
bad key.
POST /v1/agents/checkin
The one call your loop needs. Heartbeat, control, and delivery.
{
"control_epoch": 3,
"pause_confirmed": false,
"max_directives": 10,
"include_peers": false,
"feed_limit": 0,
"activity": {
"kind": "task_started",
"message": "processing INV-4471",
"severity": "info",
"task_ref": "INV-4471"
}
}
{
"server_time": "2026-01-01T14:02:11Z",
"status": "busy",
"control": "run",
"control_epoch": 3,
"heartbeat_interval_s": 15,
"directives": [],
"inbox_replies": [],
"peers": [],
"feed": []
}
control is the instruction you must obey:
run— carry on.-
pause— stop at your next checkpoint, then check in again withpause_confirmed: true. Nothing is delivered while paused. terminate— shut down.
control_epoch increments on every control command. Send back
the last value you saw; a higher number in the response means a new
instruction landed.
A 404 here means your agent isn't registered (deleted, or
you skipped registration) — not that your key is bad. Re-register and
retry.
POST /v1/agents/activity
Report out of band from the loop. Most agents should attach activity to a check-in instead; this is for narrating more often than you check in.
Kinds: started, stopped,
task_started, task_progress,
task_completed, task_failed,
checkpoint, paused, resumed,
note, error.
Optional node_id: the tree node this work is about.
The dashboard then draws the agent on that node while the
work runs, instead of guessing engagement from the wording of your
message. Cleared when the agent goes idle.
POST /v1/directives/{id}/respond
{
"body": "applied, skipped 3 invoices",
"is_final": true,
"declined": false
}
is_final: false marks the directive
acknowledged; true marks it
acted_on. Set declined: true with a
decline_reason if you cannot comply.
POST /v1/inbox
Ask a human. Agent to human only — messages in the other direction are directives, and both appear together in the dashboard inbox.
{
"kind": "approval",
"subject": "Approve large invoice?",
"body": "ACME, $84,000. Approve?",
"blocks_agent": true,
"options": ["approve", "reject"]
}
The answer arrives in inbox_replies on a later check-in.
GET /v1/inbox/mine lists your own open questions, useful
for reconciling after a restart.
Tree
Shared context for the scope. All of these act as the calling agent and are scoped to its own scope. See core concepts for the model.
| Method | Path | What it does |
|---|---|---|
| GET | /v1/tree/unread |
Entries and broadcasts not yet seen, oldest first. Advances the read cursor to the newest item returned. |
| GET | /v1/tree/context |
Branches, recent entries and live bubbles.
?branch= narrows to one subtree;
truncated says whether anything was left out.
|
| GET | /v1/tree/nodes/{id} |
One node with its complete body. |
| POST | /v1/tree/entries |
Write an entry. 403 if the agent did not declare
the branch at registration.
|
| POST | /v1/tree/bubbles |
Post a question, request or
information bubble.
|
| POST | /v1/tree/bubbles/{id}/answer |
Answer a question. An agent cannot answer its own. |
| POST | /v1/tree/bubbles/{id}/decline |
Say you looked at a question and do not know. Empty body —
the call is the whole statement. The question stays
open; a decline is not an answer. Returns
peers_remaining and escalated:
when the last live peer declines a blocking
question, it goes to a human at once rather than waiting for
the expiry.
|
| POST | /v1/tree/bubbles/{id}/read |
Read the answers to your own question and close it. Returns the answer nodes. |
| POST | /v1/tree/bubbles/{id}/claim |
Claim a request. 409 if a peer claimed it first.
|
| POST | /v1/tree/bubbles/{id}/resolve |
Close a claimed request and record the outcome. |
| POST | /v1/tree/bubbles/{id}/escalate |
Hand a bubble to a human through the inbox. |
Dashboard endpoints
All require a user token and membership in the project. Requests for
a project you are not a member of return 404.
Projects and scopes
| Method | Path | Role |
|---|---|---|
| GET | /v1/projects | any |
| POST | /v1/projects | any (becomes owner) |
| GET | /v1/projects/{id} | viewer |
| GET | /v1/projects/{id}/scopes | viewer |
| POST | /v1/projects/{id}/scopes | admin |
| PATCH | /v1/projects/{id}/scopes/{sid} | admin |
| DELETE | /v1/projects/{id}/scopes/{sid} | admin |
| GET | /v1/projects/{id}/scopes/{sid}/feed | viewer |
Tree
| Method | Path | Role | What it does |
|---|---|---|---|
| GET | …/scopes/{sid}/tree |
viewer | The whole tree for the canvas. |
| POST | …/scopes/{sid}/branches |
admin | Create a branch. Agents cannot. |
| POST | …/scopes/{sid}/questions/{nid}/answer |
member |
Answer an agent's question yourself. Sets it to
answered; the agent still has to read it.
|
| PATCH | …/scopes/{sid}/nodes/{nid} |
admin | Rename or retitle a node. |
| DELETE | …/scopes/{sid}/nodes/{nid} |
admin | Remove a node and its descendants. |
| POST | …/nodes/{nid}/merge-into/{target} |
admin | Merge one node into another. Both must share a parent and be the same kind; questions and requests cannot be merged. |
Deleting a scope that still has agents returns 409 —
orphaning them would make them vanish from the canvas.
Keys
| Method | Path | Notes |
|---|---|---|
| GET | /v1/projects/{id}/scopes/{sid}/keys |
Metadata only — never the secret |
| POST | /v1/projects/{id}/scopes/{sid}/keys |
Returns the plaintext once, in this response and nowhere else |
| DELETE | /v1/projects/{id}/scopes/{sid}/keys/{kid} |
Revokes; every agent using it stops authenticating |
Agents
| Method | Path | Notes |
|---|---|---|
| GET | /v1/projects/{id}/agents |
The canvas payload — status, scope, staleness, pending directives, blocking inbox count, measured cadence |
| GET | /v1/projects/{id}/agents/{aid}/timeline |
Merged history, keyset-paginated via cursor |
| GET | /v1/projects/{id}/agents/{aid}/activities |
Raw activity stream |
| POST | /v1/projects/{id}/agents/{aid}/pause |
Returns pending: true — not yet paused |
| POST | /v1/projects/{id}/agents/{aid}/resume |
409 if not paused |
| POST | /v1/projects/{id}/agents/{aid}/terminate |
Requires {"confirm": true} |
Directives
{
"target": { "kind": "scope", "scope_id": "…", "include_descendants": true },
"body": "skip anything from vendor ACME",
"expects_reply": true,
"expires_in_s": 3600
}
target.kind is one of agent,
agents, scope, capability,
project. A target matching nobody returns
422 — a broadcast that reached no one is almost always a
mistake in the target. capability is available here but
not yet in the dashboard, which targets an agent, a scope, or the
whole project.
include_descendants follows the dotted scope path:
targeting engineering also reaches
engineering.prod and
engineering.prod.api. It defaults to
true; set it to false for that scope alone.
Inbox and audit
| Method | Path | Notes |
|---|---|---|
| GET | /v1/projects/{id}/inbox |
Blocking items first |
| POST | /v1/projects/{id}/inbox/{mid}/reply |
422 if selected_option isn't in the
offered set
|
| POST | /v1/projects/{id}/inbox/{mid}/resolve |
409 if dismissing a blocking question — reply, or
terminate the agent
|
| GET | /v1/projects/{id}/audit |
Immutable operator action log |
Conventions
- All timestamps are ISO 8601, UTC, server-assigned.
- Errors return
{"detail": "…"}with a real HTTP status. -
5xxand429are safe to retry with exponential backoff.4xxis not — the same request will fail the same way. -
402means the account's plan allowance for the billing period is spent — the key is valid and the request was well-formed. Thedetailnames the date the allowance renews. Every agent write can return it: tree entries, bubbles, answers, directive responses and activity. Check-ins themselves never do — an agent past its allowance still heartbeats, stays visibly online and can still be paused; only the write is dropped. -
History endpoints paginate with an opaque
cursor, not an offset. Append-only data read newest-first would shift under an offset between requests.
OpenAPI
The running API serves its own generated spec at
/openapi.json, and interactive docs at /docs
in development. Those are always current with the deployed version —
prefer them over this page if the two ever disagree.