# Use the AgentX Web API — instructions for AI assistants

You are helping a person use the **AgentX Web API**: a JSON API for creating and
steering their AgentX agents from their own scripts and tools. AgentX runs coding
agents (Claude Code, Codex, Grok CLI) on machines the person controls, through a
small program called the **daemon**. Their account at https://www.agentxhq.com sees every
agent on every daemon they have claimed. With the API you can list agents and
bubbles (shared conversations between the person and their agents), create them,
send an agent a message, read what it said back, upload a file to it, and claim a
new daemon.

Talk to the person in plain language, in their own language: the one they write
to you in.

## Rules

- **Never ask for the API token in chat, and never print, log or repeat it.** The
  person creates it in their browser and puts it in an environment variable
  themselves. You only ever refer to it as `$AGENTX_API_TOKEN`.
- Never write the token into a file you create (scripts, config, `.env` in a repo,
  shell history via `export AGENTX_API_TOKEN=tok_…`). Read it from the environment.
- Load the OpenAPI description (below) before you build a request. It is the
  authority on field names and shapes; this page is a guide to it. If they
  disagree, the OpenAPI description wins.
- Ask before any call that changes something: creating an agent, creating a
  bubble, sending a message, uploading a file, claiming a daemon. Say what the
  call will do. Reading (list agents, list bubbles, read output) needs no
  confirmation.
- If a call fails, stop and show the person the exact status code and the
  `error.code` and `error.message` from the response. Don't guess around it.
- If the person hasn't said what they want, ask them once the token works.
  Offer two or three examples from "Common tasks" below (e.g. send an agent a
  message and read its reply, list what each agent is doing). Never make them
  edit the prompt they pasted.
- Wording: in the API these are called bubbles (`/api/v1/bubbles`, `bubble.*`
  scopes). When you talk to the person, call them chats.
- Never invent endpoints. Only the routes in the OpenAPI description exist.
- If the person needs help from a human, AgentX support is on Discord:
  https://discord.gg/DakZuWyqeJ

## Step 1 — Get an API token (the person does this)

Tell the person:

1. Open https://www.agentxhq.com/settings and sign in.
2. Make sure the tenant (account) you want to script is the one selected in the
   console. A token works only for the tenant it was created in.
3. Find the **API tokens** card. Type a name for the token under **New API
   token** (for example "my laptop scripts") and click **Create API token →**.
4. The next page shows the token once. It starts with `tok_`. Copy it now; it is
   never shown again. The page also lists the scopes the token carries.
5. Put it in an environment variable in the terminal where I run commands,
   without pasting it to me. For example, in bash or zsh:

   ```sh
   read -rs AGENTX_API_TOKEN && export AGENTX_API_TOKEN
   ```

   (paste the token, press Enter; nothing is echoed). In PowerShell:

   ```powershell
   $env:AGENTX_API_TOKEN = Read-Host -MaskInput "AgentX API token"
   ```

Only an **owner** or **admin** of the tenant can create and use API tokens.
A token created in Settings does not expire. To revoke it, the person clicks
**Revoke →** next to it on the same card.

Check that the variable is set without revealing it:

```sh
test -n "$AGENTX_API_TOKEN" && echo "token is set" || echo "token is NOT set"
```

## Step 2 — Base URL, auth header and the OpenAPI description

- **Base URL:** https://www.agentxhq.com. Every API path starts with `/api/v1`.
  The OpenAPI description's `servers[0].url` names the base URL to use.
- **Auth header:** `Authorization: Bearer $AGENTX_API_TOKEN` on every call except
  the two discovery routes.
- **Body:** JSON (`Content-Type: application/json`), except file upload, which is
  multipart.

Load the description first. It needs no token:

```sh
curl -s https://www.agentxhq.com/api/v1/openapi.json
```

It is OpenAPI 3.1 and lists every route, the one scope each route needs, and the
full request and response schemas. Other ways to find it:

- `GET https://www.agentxhq.com/api/v1` — a short JSON index: method, path, scope and summary
  of every route.
- `GET https://www.agentxhq.com/openapi.json` redirects to `/api/v1/openapi.json`.
- `GET https://www.agentxhq.com/.well-known/api-catalog` — RFC 9727 API catalog.
- A call refused for a missing token (401) or a missing scope
  (403 `INSUFFICIENT_SCOPE`) carries
  `Link: <…/api/v1/openapi.json>; rel="service-desc"`.

The current version is `v1`. Routes are versioned in the path.

## Step 3 — Scopes: what each one allows

Every route needs exactly one scope. A token created in Settings carries all of
the scopes below.

| Scope | Route | What it does |
|---|---|---|
| `agentx:agent.read` | `GET /api/v1/agents` | List every agent across all the person's daemons |
| `agentx:agent.read` | `POST /api/v1/output` | Read what an agent has been saying |
| `agentx:agent.create` | `POST /api/v1/agents` | Create an agent on one of the person's daemons |
| `agentx:agent.upload` | `POST /api/v1/agents/{agent}/upload` | Upload a file into an agent's workspace |
| `agentx:bubble.read` | `GET /api/v1/bubbles` | List bubbles |
| `agentx:bubble.create` | `POST /api/v1/bubbles` | Create a bubble anchored to an agent |
| `agentx:bubble.send` | `POST /api/v1/messages` | Send a message to an agent |
| `agentx:daemon.claim` | `POST /api/v1/daemons/claim` | Connect a daemon to the account with its claim code |
| none | `GET /api/v1`, `GET /api/v1/openapi.json` | Discovery; no token needed |

## Step 4 — Common tasks

All examples are bash with `curl`. Pipe through `jq` if it is installed.

### List agents

```sh
curl -s https://www.agentxhq.com/api/v1/agents \
  -H "Authorization: Bearer $AGENTX_API_TOKEN"
```

Returns `agents` (each with `name`, `agent_id`, `backend`, `state`,
`route_daemon_id` and more), `backends`, `total` and `unreachable`. Add
`?include_all=true` to include disabled agents and agents whose backend is
unavailable.

**If `unreachable` is non-empty, the list is partial.** Tell the person which
daemon could not be reached (`name`, `reason`) instead of saying an agent does
not exist.

### List bubbles

```sh
curl -s "https://www.agentxhq.com/api/v1/bubbles?limit=20" \
  -H "Authorization: Bearer $AGENTX_API_TOKEN"
```

Query parameters: `limit` (0 or absent returns all), `cursor` (the `next_cursor`
from the previous page), `query` (only bubbles whose name matches). Each bubble
has `bubble_id`, `name`, `members` and `latest_activity_at`. Keep calling with
`cursor` until `next_cursor` is empty.

### Send a message to an agent

```sh
curl -s -X POST https://www.agentxhq.com/api/v1/messages \
  -H "Authorization: Bearer $AGENTX_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"agent":"Kit","bubble":"release-prep","message":"Run the tests and tell me what fails.","kind":"prompt"}'
```

Name exactly one recipient with `agent`, `address` or `session_id`. Set `bubble`
(a `bubble_id` or a bubble name from `GET /api/v1/bubbles`) when the agent is
addressed inside a bubble. A bubble name that matches more than one bubble is
refused; use the id. The response has `status`, `message_id` and
`target_session_id`; keep `target_session_id` to read the reply.

`kind` is `prompt` (start a new task) or `steer` (redirect an agent that is
already working); any other value is refused. Without `kind` the message is a
`steer`, or a `prompt` when you message yourself. `to` is another way to name the
recipient: an agent name, or `self`. `name` is accepted as another spelling of
`agent`.

The agent sees that the message came from the person through the API, labelled
with the token's name.

### Read an agent's output

```sh
curl -s -X POST https://www.agentxhq.com/api/v1/output \
  -H "Authorization: Bearer $AGENTX_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"agent":"Kit","bubble":"release-prep","lines":50}'
```

Name the agent with `agent` (and then `bubble` is required), or read one session
with `session_id`. `lines` (or `tail`) is how far back to read. Leave `format`
empty for the conversation; `"screen"` returns the raw terminal. The response has
`output`, `state`, `lines` and `truncated`.

This is a read, not a wait: if the agent is still working, read again after a
while. Don't loop rapidly.

### Create a bubble

```sh
curl -s -X POST https://www.agentxhq.com/api/v1/bubbles \
  -H "Authorization: Bearer $AGENTX_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"release-prep","anchor_agent":"Kit"}'
```

`name` and `anchor_agent` are required. The response is the new `bubble`.

### Create an agent

```sh
curl -s -X POST https://www.agentxhq.com/api/v1/agents \
  -H "Authorization: Bearer $AGENTX_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"daemon":"<DAEMON>","name":"Kit","backend":"claude"}'
```

`daemon`, `name` and `backend` are required; the other fields (model, prompts,
sandbox, idle settings and so on) are in the OpenAPI description. Ask the person
which daemon and which backend.

- `daemon` is the daemon's id or its hostname. The API has no "list daemons"
  route: read the id off an existing agent's `route_daemon_id`. A hostname that
  more than one of the person's daemons share is refused; use the id.
- `backend` is one of `claude`, `codex`, `cursor`, `grokcli` (or `grok`),
  `junie`, `opencode` and `pi`. The daemon refuses a backend it does not have;
  the `backends` list from `GET /api/v1/agents` shows the ones in use.

The response is the new `agent`.

### Upload a file to an agent

```sh
FILE=./report.pdf
curl -s -X POST "https://www.agentxhq.com/api/v1/agents/Kit/upload?bubble=release-prep" \
  -H "Authorization: Bearer $AGENTX_API_TOKEN" \
  -F "size=$(wc -c < "$FILE" | tr -d ' ')" \
  -F "file=@$FILE"
```

`{agent}` in the path is the agent's name or its `agent_id`. The multipart body
must send `size` (the exact byte count) **first**, then `file`. `bubble` (name or
id) is required. The response is `{bytes, status}`. A busy daemon refuses with
503 `UNAVAILABLE` and `Retry-After: 1`; retry after a second. A file that does
not fit the daemon's disk is refused with 413, and retrying will not help.

### Claim a daemon

```sh
curl -s -X POST https://www.agentxhq.com/api/v1/daemons/claim \
  -H "Authorization: Bearer $AGENTX_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"claim_code":"<CODE>"}'
```

The claim code is what the daemon writes to its claim file while it waits to be
claimed (see https://www.agentxhq.com/install.md, Step 5). The response is the daemon's
`connection`.

## Errors

Every refusal is JSON in one envelope:

```json
{"error":{"code":"UNAUTHORIZED","message":"missing or invalid bearer token"}}
```

The same code is also in the `X-Error-Code` header. These are the codes you will
commonly see. A refusal that comes from the agent's daemon passes the daemon's own
code through, upper-cased, so you may meet others: show the person
`error.message`.

| Status | `error.code` | Meaning | What to do |
|---|---|---|---|
| 400 | `BAD_REQUEST` | Missing or invalid field, unknown field, ambiguous agent, daemon or bubble name, or a JSON body over 1 MiB | Fix the request; use the id instead of an ambiguous name |
| 401 | `UNAUTHORIZED` | No `Authorization: Bearer` token was sent | Check that `$AGENTX_API_TOKEN` is set |
| 403 | `UNKNOWN` | The token was not accepted: it is mistyped, unknown or revoked | Ask the person to check `$AGENTX_API_TOKEN` or create a new token |
| 403 | `INSUFFICIENT_SCOPE` | The token lacks the scope this route needs (`WWW-Authenticate` names it) | Ask the person to create a new token in Settings |
| 403 | `ROLE_DENIED`, `WORKPLANE_UNAUTHORIZED` | The person is not an owner or admin of the tenant, or not allowed this operation | Tell the person; don't retry |
| 404 | `NOT_FOUND` | Unknown agent, bubble or path | List agents or bubbles to find the right name |
| 409 | `BUBBLE_CLOSED` | The bubble is closed | Use another bubble or create one |
| 409 | `NOT_MEMBER_HOME` | The machine that received the request does not host a member of the bubble | Show the person the message |
| 409 | `CLAIM_ALREADY_CLAIMED` | The daemon is already claimed | Tell the person; don't retry |
| 410 | `CLAIM_EXPIRED` | The claim code has expired | Ask the person for a fresh claim code |
| 422 | `CLAIM_REFUSED` | The claim was refused | Show the person the message |
| 422 | `WORKPLANE_REFUSED` | The daemon refused the request for another reason | Show the person the message |
| 500 | `SERVING_BRIDGE_MISSING` | The agent's route is incomplete on AgentX's side | Don't retry; tell the person to contact support |
| 502 | e.g. `WORKPLANE_UNAVAILABLE`, `CAPABILITY_MISSING`, `ROUTE_UNREACHABLE` | The agent's daemon is offline, or too old to support this call | Tell the person which; the daemon may need to be started or upgraded |
| 503 | `ROUTE_UNDETERMINED` | Temporary; the route could not be worked out | Retry once after a short wait |

File upload has its own refusals:

| Status | `error.code` | Meaning | What to do |
|---|---|---|---|
| 400 | `upload_session_ambiguous` | The agent has more than one session in that bubble | Name the bubble by its id, or ask the person |
| 404 | `upload_session_missing` | The agent has no session in that bubble | Message the agent in that bubble first |
| 413 | `OVERSIZE` | The file does not fit the daemon's disk | Don't retry |
| 503 | `UNAVAILABLE` | The upload is busy (`Retry-After: 1`) | Retry after a second |

Request bodies must be valid JSON with only the documented fields, and at most
1 MiB; a larger body is refused with 400 `BAD_REQUEST`. File upload has no cap at
this door.
