Control-plane API
This is the surface the Brainbase CLI itself talks to. It is documented here so you can read a CLI failure, reproduce a command with curl, or wire the same operations into CI — not as a public API contract.
/v2/cli exists to serve the CLI we ship, and it is not part of the versioned public API. It carries no independent version and can change whenever the CLI does. If you are building an integration, use the Universal Harness API instead — agents, threads and messages under /v2 are the supported surface.Host and prefix
Every path below is relative to https://api.brainbaselabs.com/v2/cli. Point the CLI elsewhere with BRAINBASE_CONTROL_PLANE_URL; it appends the /v2/cli prefix itself.
curl "https://api.brainbaselabs.com/v2/cli/orgs" \
-H "Authorization: Bearer $BRAINBASE_TOKEN"
MAS also serves this same router under /api/cli, so calling https://api.brainbaselabs.com/api/cli/… directly hits the same handlers with the same scope checks — one implementation, two prefixes.
BRAINBASE_API_URL means "this is the legacy KLS control plane", not "use the other prefix". On that path the CLI refuses three things before sending: declarative machine_kind / default_model writes, orchestration creation, and schedule-trigger writes. Use BRAINBASE_CONTROL_PLANE_URL to point at another MAS host. See Environment variables.Authentication
Send a bearer token: either a PAT (bbpat_…) or the access token from a browser login session. Both are accepted on every endpoint here.
Every write requires the publish scope; no read requires any scope. Each table below names the requirement per endpoint. A session token carries no scope list and is never scope-checked. See PAT scopes for the full model.
token create writes to ~/.brainbase/token.json is not read by agent, orchestration, link, unlink, sync, status or team. Export it as BRAINBASE_TOKEN instead. Raw curl against these endpoints is unaffected — you are supplying the header yourself.Organizations and teams
A team is a group inside an organization, and every agent lives in a team. These four endpoints back brainbase team list and the --team values the agent commands take. The two GETs have a long list of callers because every command that needs a team resolves one first — whether you passed --org / --team or are picking interactively.
| Endpoint | Used by | Scope required |
|---|---|---|
GET /orgs | team list, agent create, agent list, orchestration create, orchestration list, orchestration add-agent, template publish, skill publish | none |
GET /orgs/{org_id}/teams | team list, agent create, agent list, orchestration create, orchestration list, orchestration add-agent | none |
POST /orgs/{org_id}/teams | agent create | publish |
GET /orgs/{org_id}/teams/{team_id}/agents | agent list | none |
GET /orgs returns every organization the caller is a member of, with the caller's role in each. The agents listing returns everything granted to the team — including agents owned by other members, matching the team view in the web app — newest grant first, capped by limit (default 200, no cursor). Group membership is checked on top of organization membership, so an organization member cannot enumerate a team they do not belong to.
POST /orgs/{org_id}/teams is reached from one place in the CLI: the "+ Create a new team" option in agent create's interactive team picker. No other command offers it.
Agents
| Endpoint | Used by | Scope required |
|---|---|---|
POST /agents | agent create | publish |
GET /agents/{agent_id} | link, sync, status, agent pull, agent push, agent status, agent connect | none |
PUT /agents/{agent_id} | agent push | publish |
PUT /agents/{agent_id} updates agent metadata and the two writable capability toggles — name, tagline, entrypoint, default_model, memory_enabled, browser_enabled. Omitted fields are left alone. The integration flags (slack_connected, meeting_connected, github_connected) are deliberately not writable here; they are changed through the connection endpoints below.
machine_kind, but only as the value the agent already has — that is a no-op. Any other value is refused with 409 machine_kind is immutable after agent creation, naming both the current and requested values. To change it, create a new agent.PATCH /v2/agents/{agent_id} and a different body. This endpoint is not a substitute for it — pick one surface and stay on it.Manifest and revision
| Endpoint | Used by | Scope required |
|---|---|---|
GET /agents/{agent_id}/manifest | agent pull, agent push, agent status, sync | none |
PUT /agents/{agent_id}/manifest | agent push, agent create | publish |
GET /agents/{agent_id}/revision | status | none |
GET .../manifest returns the agent's whole content as a list of components — instructions, skills, MCP servers, playbooks, evals — each with its files and a content hash, plus the current revision. It may also return warnings describing content the server could not represent.
PUT .../manifest is a whole-manifest replacement, reconciled per component type: components present are created or updated, and components absent are unlinked or archived. Pass base_revision so the server can refuse the push with a conflict when the cloud has moved since you last read the manifest.
reconcile_playbooks and reconcile_evals. Without the flag, a manifest that omits them is read as "this client does not model them" rather than "archive them", so an older client cannot silently wipe rows. Send the flag only when your manifest is authoritative for that content.GET .../revision returns just {"revision": <n>} — a hash over the agent's content, not a counter. It is the cheap drift check. It covers instructions, skills, MCP servers, active playbooks, active evals, and secret key names — adding or removing a key moves it, changing a value does not. It does not cover integrations, so connecting or disconnecting Slack leaves the revision unchanged.
reconcile_evals requires base_revision in the same request. Without it the server refuses with 400 rather than let a client that never pulled the evals archive them.
Secrets
| Endpoint | Used by | Scope required |
|---|---|---|
GET /agents/{agent_id}/secrets | agent pull, agent push, agent status | none |
PUT /agents/{agent_id}/secrets | agent push | publish |
Both carry {"secrets": {"KEY": "value"}}. The GET returns values in plaintext. The PUT is a whole-map replace: keys you omit are removed, so read before you write. Keys must be valid environment-variable names; anything else is rejected with 400.
PUT .../secrets plants environment variables into the agent's next sandbox, so it needs publish. Reading them back does not. This is the only place in the CLI surface where a read and its matching write differ that sharply in consequence.Agent keys
Agent keys (bb_live_…) are a third credential class, distinct from PATs and sessions. link mints one so an agent's sandbox can call back to Brainbase; unlink revokes it.
| Endpoint | Used by | Scope required |
|---|---|---|
POST /agents/{agent_id}/keys | link, agent create | publish |
GET /agents/{agent_id}/keys | not called by the CLI today | none |
DELETE /keys/{key_id} | link, unlink | publish |
The POST returns the plaintext key once, with its id and prefix; only a hash is stored. The GET lists an agent's keys by prefix, never the secret. DELETE takes the key id and is idempotent from the caller's point of view — a key that is already gone answers 404.
Connections and MCP
| Endpoint | Used by | Scope required |
|---|---|---|
GET /agents/{agent_id}/connections | agent connections, agent connect | none |
PUT /agents/{agent_id}/connections/slack | agent connect slack | publish |
DELETE /agents/{agent_id}/connections/slack | agent disconnect slack | publish |
PUT /agents/{agent_id}/connections/meeting | agent connect meeting | publish |
DELETE /agents/{agent_id}/connections/meeting | agent disconnect meeting | publish |
GET /agents/{agent_id}/mcp-servers | mcp list | none |
GET .../connections reports every integration on the agent and, per integration, whether the CLI can change it — manageable_from_cli, with an unmanageable_reason when it cannot. Slack and meeting are manageable from the terminal; GitHub and phone need the web app.
Both PUTs are idempotent: re-running with fresh credentials rotates them in place. Slack takes bot_token and signing_secret (plus optional app_id and app_name); meeting takes bot_name and an optional bot_image_url. DELETE removes the install rather than deactivating it, matching what the console's disconnect leaves behind.
GET .../mcp-servers returns each server's transport, enabled state, authorization state (none, oauth_required, oauth_connected, oauth_expired) and last connectivity result. Tokens are never returned — only whether one exists and when it lapses.
404 into "this control plane does not support reading or changing connections yet" rather than letting it read as "that agent does not exist". A 404 carrying {"detail":"Agent not found"} is a real answer about a real agent and is passed through untouched.Evals
| Endpoint | Used by | Scope required |
|---|---|---|
GET /agents/{agent_id}/evals | agent eval list | none |
GET /agents/{agent_id}/eval-runs | agent eval runs | none |
Read-only by design: eval definitions are managed declaratively through the manifest, so these exist to answer "what does the cloud think?" from a terminal. evals takes include_archived (default false) to include definitions a push has archived. eval-runs returns verdicts newest first, optionally narrowed with task_id, and limit (default 50, max 200).
status: "passed" with passed: false — the judge ran successfully and returned a failing verdict. status: "failed" means the judge itself errored, and error says why.Orchestrations
| Endpoint | Used by | Scope required |
|---|---|---|
GET /orgs/{org_id}/teams/{team_id}/orchestrations | orchestration list | none |
POST /orchestrations | orchestration create | publish |
GET /orchestrations/{orch_id} | orchestration status, orchestration pull | none |
GET /orchestrations/{orch_id}/manifest | orchestration pull | none |
PUT /orchestrations/{orch_id} | orchestration push | publish |
The bare GET returns the orchestration with its members and edges but no per-member manifests. The /manifest variant inlines every member's full agent manifest under members[].manifest, which is what orchestration pull consumes in a single round trip.
PUT is a partial update, with one exception: members and edges are whole-replace. Omit them to leave them unchanged; send [] to clear them. Pass base_revision for the same conflict protection the agent manifest has. Every declared member must already have access to the parent team, or the request is refused with 400.
Errors
Errors are JSON with a detail field, either a string or an object carrying code and message.
| Status | Body | Meaning |
|---|---|---|
401 | missing bearer token | No Authorization header. |
401 | invalid or expired PAT | The token does not resolve — revoked, expired, or mistyped. |
403 | PAT lacks required scope: publish | A read-scoped PAT on a write. Nothing was modified — the check runs before the handler. |
403 | credential_scope_denied | A resource-scoped credential listing a team's agents. See Resource scope. |
403 | agent-scoped PATs cannot access orchestration configuration | A resource-scoped credential on any of the five orchestration endpoints. Reads thread-scoped for a thread-bound token. |
404 | Agent not found | No such agent, or the caller cannot reach it. The two are deliberately indistinguishable. |
404 | Not Found | The path is not routed at all — usually a control plane older than the command calling it. |
409 | — | A base_revision conflict: the cloud advanced since you read it. Pull, then push again. |
409 | machine_kind is immutable after agent creation | PUT /agents/{agent_id} was sent a machine_kind other than the agent's current one. |
422 | — | The request body or a query parameter failed validation. |