Docs navigation
Reference

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.

Not the public API
/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.

bash
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.

Setting BRAINBASE_API_URL is not the same as using the /api/cli prefix
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.

A stored PAT will not authenticate these calls through the CLI
The PAT that 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.

EndpointUsed byScope required
GET /orgsteam list, agent create, agent list, orchestration create, orchestration list, orchestration add-agent, template publish, skill publishnone
GET /orgs/{org_id}/teamsteam list, agent create, agent list, orchestration create, orchestration list, orchestration add-agentnone
POST /orgs/{org_id}/teamsagent createpublish
GET /orgs/{org_id}/teams/{team_id}/agentsagent listnone

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

EndpointUsed byScope required
POST /agentsagent createpublish
GET /agents/{agent_id}link, sync, status, agent pull, agent push, agent status, agent connectnone
PUT /agents/{agent_id}agent pushpublish

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 is fixed at agent creation
The body accepts 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.
Different verb and path from the public API
The public API updates an agent with 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

EndpointUsed byScope required
GET /agents/{agent_id}/manifestagent pull, agent push, agent status, syncnone
PUT /agents/{agent_id}/manifestagent push, agent createpublish
GET /agents/{agent_id}/revisionstatusnone

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.

Absence means deletion, behind a capability flag
Archive-on-absence for playbooks and evals is opt-in per request — 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

EndpointUsed byScope required
GET /agents/{agent_id}/secretsagent pull, agent push, agent statusnone
PUT /agents/{agent_id}/secretsagent pushpublish

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.

A read-scoped PAT cannot rotate secrets
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.

EndpointUsed byScope required
POST /agents/{agent_id}/keyslink, agent createpublish
GET /agents/{agent_id}/keysnot called by the CLI todaynone
DELETE /keys/{key_id}link, unlinkpublish

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

EndpointUsed byScope required
GET /agents/{agent_id}/connectionsagent connections, agent connectnone
PUT /agents/{agent_id}/connections/slackagent connect slackpublish
DELETE /agents/{agent_id}/connections/slackagent disconnect slackpublish
PUT /agents/{agent_id}/connections/meetingagent connect meetingpublish
DELETE /agents/{agent_id}/connections/meetingagent disconnect meetingpublish
GET /agents/{agent_id}/mcp-serversmcp listnone

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.

These six routes report server age explicitly
They shipped after the commands that call them, so the CLI translates an unrouted 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

EndpointUsed byScope required
GET /agents/{agent_id}/evalsagent eval listnone
GET /agents/{agent_id}/eval-runsagent eval runsnone

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).

A run's status is the judge's outcome, not the verdict
An eval the agent failed is 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

EndpointUsed byScope required
GET /orgs/{org_id}/teams/{team_id}/orchestrationsorchestration listnone
POST /orchestrationsorchestration createpublish
GET /orchestrations/{orch_id}orchestration status, orchestration pullnone
GET /orchestrations/{orch_id}/manifestorchestration pullnone
PUT /orchestrations/{orch_id}orchestration pushpublish

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.

StatusBodyMeaning
401missing bearer tokenNo Authorization header.
401invalid or expired PATThe token does not resolve — revoked, expired, or mistyped.
403PAT lacks required scope: publishA read-scoped PAT on a write. Nothing was modified — the check runs before the handler.
403credential_scope_deniedA resource-scoped credential listing a team's agents. See Resource scope.
403agent-scoped PATs cannot access orchestration configurationA resource-scoped credential on any of the five orchestration endpoints. Reads thread-scoped for a thread-bound token.
404Agent not foundNo such agent, or the caller cannot reach it. The two are deliberately indistinguishable.
404Not FoundThe path is not routed at all — usually a control plane older than the command calling it.
409A base_revision conflict: the cloud advanced since you read it. Pull, then push again.
409machine_kind is immutable after agent creationPUT /agents/{agent_id} was sent a machine_kind other than the agent's current one.
422The request body or a query parameter failed validation.