---
name: opencouncil-api
description: Use the OpenCouncil workspace "your workspace" over its HTTP API: read sessions and their conversations, start sessions, talk to the council of AI agents, edit sessions, read agents and search their knowledge, manage agents and their knowledge. Use when the user asks you to do something in OpenCouncil.
---

# OpenCouncil API

OpenCouncil is a council of AI agents that talk with people in sessions (voice and text). This API acts as the person
who created the key, inside one workspace only, and never beyond what that person may do.

- Base URL: `https://app.opencouncil.build/council-api`
- Workspace: `{ws}` (your workspace)
- Authentication: header `Authorization: Bearer $OPENCOUNCIL_API_KEY` on every request. Read the key from the `OPENCOUNCIL_API_KEY` environment variable; never print it.
- Key scopes: each endpoint lists the scope it needs.

## Rules

- JSON in and out: send `content-type: application/json` with a body. IDs have a prefix (`ses_`, `agt_`, `jvp_`…).
- Errors: `{ "error": { "code", "message" } }`. 401 = key invalid, expired or deleted. 403 = the key lacks the scope,
  belongs to another workspace, or the person may not do it. 404 = not found. 409 = changed by someone else: read
  the resource again and retry with its new `revision`. 429 = over 120 requests a minute: wait and retry.
- Lists are paginated: `?limit=1..100` (default 50) and `?after=<nextCursor>` until `nextCursor` is null.
- Edits and deletes need the `revision` you last read (optimistic locking).
- Keys cannot manage keys, members, groups, connections, invitations, public links or billing.

## Endpoints

### Account

- `GET /v1/me`: Who the key acts as.
  - Returns: `{ user: { id, name, email, personalWorkspaceId } }`
- `GET /v1/workspaces/{ws}` (scope `workspace:read`): The workspace.
  - Returns: `{ workspace: { id, name, ownerId, revision } }`

### Sessions: read

- `GET /v1/workspaces/{ws}/sessions` (scope `sessions:read`): Sessions you can see, paginated.
  - Returns: `{ sessions: Session[], nextCursor }`
- `GET /v1/workspaces/{ws}/sessions/{sessionId}` (scope `sessions:read`): One session: name, visibility, agents, revision, Broadcast options and whether a broadcast is on the air now.
  - Returns: `{ session: Session & { broadcast?: { defaultSource: 'room'|'tab', suggestNames, autoApplyNames, agentsSpeakAloud }, broadcastLive: { source, startedAt } | null } }`
- `GET /v1/workspaces/{ws}/sessions/{sessionId}/voices` (scope `sessions:read`): The Broadcast's voices: the speakers of a talk, panel or stream told apart ("Speaker A") and the name each got (the AI applies a name it is sure of unless autoApplyNames is off; nameSource says who named it). A voice line in the history carries `data.voiceId`; show it under that voice's `name` (or "Speaker <label>"), following `mergedInto`. A voice is never an agent.
  - Returns: `{ voices: [{ id: 'vce_…', label: 'A', name: string|null, nameSource: 'ai'|'user'|null, firstHeardAt, lines, speakingMs, mergedInto?, suggestion?: { name, evidence, confidence, at, dismissed? } }], live: { source: 'room'|'tab', startedAt } | null }`
- `GET /v1/workspaces/{ws}/sessions/{sessionId}/history?after=0&limit=50` (scope `sessions:read`): The conversation, oldest first. Pass the last `sequence` you saw as `after` to get only newer turns (limit 1–100).
  - Returns: `{ turns: [{ sequence, events: [{ id, type: 'human.said'|'agent.spoke'|'jev.skipped', ts, actor?, data: { text, speaker?, agent? } }] }], nextCursor }`
- `GET /v1/workspaces/{ws}/sessions/{sessionId}/participants` (scope `sessions:read`): People in the session (you need to be allowed to manage them).
  - Returns: `{ participants: [], nextCursor }`
- `GET /v1/workspaces/{ws}/sessions/{sessionId}/capabilities` (scope `sessions:read`): What you may do in this session (speak, edit…).
  - Returns: `{ ... }`

### Sessions: create and talk

- `POST /v1/workspaces/{ws}/sessions` (scope `sessions:create`): Start a session with library agents (empty list: the workspace's default Master joins).
  - Body: `{ "name": "Weekly review", "visibility": "private" | "workspace", "agentIds": ["agt_…"], "jevPromptId": null, "briefMarkdown"?: "context for the agents" }`
  - Returns: `201 { session: Session }`
- `POST /v1/workspaces/{ws}/sessions/{sessionId}/duplicate` (scope `sessions:create`): Copy a session's setup (agents, brief, instructions) into a new session, without history.
  - Returns: `201 { session: Session }`
- `POST /v1/workspaces/{ws}/sessions/{sessionId}/messages` (scope `messages:send`): Say something in the session's chat, as you. Retrying with the same requestId is safe (it is not sent twice).
  - Body: `{ "requestId": "<new UUID v4 per message>", "text": "Hello, council" }`
  - Returns: `{ sequence, events }`

### Sessions: edit

- `PATCH /v1/workspaces/{ws}/sessions/{sessionId}` (scope `sessions:write`): Edit the session (name, visibility, brief, Broadcast options…). Send the `revision` you last read. `broadcast` changes only the options sent.
  - Body: `{ "name"?: "…", "visibility"?: "…", "briefMarkdown"?: "…", "broadcast"?: { "defaultSource"?: "room"|"tab", "suggestNames"?: true, "autoApplyNames"?: true, "agentsSpeakAloud"?: false }, "revision": 3 }`
  - Returns: `{ session: Session }`
- `DELETE /v1/workspaces/{ws}/sessions/{sessionId}` (scope `sessions:write`): Delete the session.
  - Body: `{ "revision": 3 }`
- `DELETE /v1/workspaces/{ws}/sessions/{sessionId}/history` (scope `sessions:write`): Clear the conversation.
  - Body: `{ "revision": 3 }`
  - Returns: `{ session: Session }`
- `POST /v1/workspaces/{ws}/sessions/{sessionId}/agents` (scope `sessions:write`): Add an agent to the session: a copy of a library agent, or a new one.
  - Body: `{ "source": "library", "agentId": "agt_…", "revision": 3 } or { "source": "new", "config": AgentConfig, "revision": 3 }`
  - Returns: `201 { session: Session }`
- `PATCH /v1/workspaces/{ws}/sessions/{sessionId}/agents/{agentId}` (scope `sessions:write`): Edit this session's copy of an agent.
  - Body: `{ "config": AgentConfig, "revision": 3 }`
  - Returns: `{ session: Session }`
- `PATCH /v1/workspaces/{ws}/sessions/{sessionId}/agents/{agentId}/state` (scope `sessions:write`): Mute, hide or remove an agent in this session.
  - Body: `{ "muted"?: true, "hidden"?: false, "remove"?: true, "revision": 3 }`
  - Returns: `{ session: Session }`
- `PATCH /v1/workspaces/{ws}/sessions/{sessionId}/voices/{voiceId}` (scope `sessions:write`): Name a Broadcast voice (its old lines show the new name), clear its name with null, or dismiss the AI's suggested name. No revision needed.
  - Body: `{ "name": "Stephan Bitton" | null, "nameSource"?: "user"|"ai" } or { "dismissSuggestion": true }`
  - Returns: `{ voice, voices }`
- `POST /v1/workspaces/{ws}/sessions/{sessionId}/voices/{voiceId}/merge` (scope `sessions:write`): Two letters that are the same person: this voice's lines become `into`'s.
  - Body: `{ "into": "vce_…" }`
  - Returns: `{ voice, voices }`

### Agents, prompts and knowledge: read

- `GET /v1/workspaces/{ws}/agents` (scope `agents:read`): Library agents, paginated.
  - Returns: `{ agents: [{ id, config: { name, persona, trigger, voiceBackend, voice, … }, revision }], nextCursor }`
- `GET /v1/workspaces/{ws}/agents/{agentId}` (scope `agents:read`): One library agent.
  - Returns: `{ agent }`
- `GET /v1/workspaces/{ws}/jev-prompts` (scope `agents:read`): Decision prompts (how the council takes turns).
  - Returns: `{ prompts: [], nextCursor }`
- `GET /v1/workspaces/{ws}/templates` (scope `agents:read`): Your saved templates.
  - Returns: `{ templates: [], nextCursor }`
- `GET /v1/workspaces/{ws}/agents/{agentId}/knowledge` (scope `agents:read`): An agent's knowledge files.
  - Returns: `{ files: [{ id, name, chars, chunkCount }], usage }`
- `POST /v1/workspaces/{ws}/agents/{agentId}/knowledge/search` (scope `agents:read`): Search an agent's knowledge by meaning.
  - Body: `{ "query": "…", "limit"?: 5 }`
  - Returns: `{ matches: [] }`

### Agents, prompts and knowledge: edit

- `POST /v1/workspaces/{ws}/agents` (scope `agents:write`): Create a library agent.
  - Body: `{ "config": { "name": "Critic", "persona": "…", "trigger": "When relevant.", "voiceBackend": "text", "voice": "alloy", "textOnly": true, "aliases": [], "master": false }, "credentialId": null }`
  - Returns: `201 { agent }`
- `PATCH /v1/workspaces/{ws}/agents/{agentId}` (scope `agents:write`): Edit a library agent (sessions keep their own copies).
  - Body: `{ "config"?: AgentConfig, "revision": 2 }`
  - Returns: `{ agent }`
- `DELETE /v1/workspaces/{ws}/agents/{agentId}` (scope `agents:write`): Delete a library agent.
  - Body: `{ "revision": 2 }`
- `POST /v1/workspaces/{ws}/jev-prompts` (scope `agents:write`): Create a decision prompt.
  - Body: `{ "name": "…", "instructions": "…" }`
  - Returns: `201 { prompt }`
- `PATCH /v1/workspaces/{ws}/jev-prompts/{promptId}` (scope `agents:write`): Edit a decision prompt (send the whole prompt).
  - Body: `{ "name": "…", "instructions": "…", "revision": 1 }`
  - Returns: `{ prompt }`
- `DELETE /v1/workspaces/{ws}/jev-prompts/{promptId}` (scope `agents:write`): Delete a decision prompt.
  - Body: `{ "revision": 1 }`
- `POST /v1/workspaces/{ws}/agents/{agentId}/knowledge` (scope `agents:write`): Add a Markdown knowledge file to an agent: inline up to 200,000 characters, or a file uploaded through knowledge-uploads (up to 50 MB). A large file takes a while: the server embeds it before answering.
  - Body: `{ "name": "notes.md", "content": "# …" } or { "name": "notes.md", "uploadId": "kf_…" }`
  - Returns: `201 { file }`
- `POST /v1/workspaces/{ws}/knowledge-uploads` (scope `agents:write`): Start a large upload: PUT the file's exact bytes to `url` with the given headers within 15 minutes (no Authorization header there), then add it with its uploadId.
  - Body: `{ "name": "notes.md", "size": <bytes> }`
  - Returns: `201 { uploadId, method: 'PUT', url, headers: { 'content-type': 'text/markdown' }, expiresAt }`
- `DELETE /v1/workspaces/{ws}/agents/{agentId}/knowledge/{fileId}` (scope `agents:write`): Remove a knowledge file.

## Shell examples

```sh
BASE=https://app.opencouncil.build/council-api
WS={ws}
AUTH="Authorization: Bearer $OPENCOUNCIL_API_KEY"
```

### Talking to the council

1. Create a session (or pick one from the list) and keep its `id`.
2. Send a message with a new UUID as `requestId`.
3. Poll `history?after=<last sequence you saw>` every few seconds. Agent replies are `agent.spoke` events
   (`actor` = agent name, `data.text` = what it said). A `jev.skipped` event means no agent spoke for that turn.
   Agents answer while the session is running in OpenCouncil; if nothing comes back, the session may not be live.
4. A Broadcast (a talk or a stream heard by the session) is started only in the browser, where its audio comes from;
   through the API you read its voices, name or merge them, and change its options.

```sh
SES=$(curl -s -X POST "$BASE/v1/workspaces/$WS/sessions" -H "$AUTH" -H 'content-type: application/json' \
  -d '{"name":"From my assistant","visibility":"private","agentIds":[],"jevPromptId":null}' | jq -r .session.id)

curl -s -X POST "$BASE/v1/workspaces/$WS/sessions/$SES/messages" -H "$AUTH" -H 'content-type: application/json' \
  -d "{\"requestId\":\"$(uuidgen | tr A-Z a-z)\",\"text\":\"Hello council, what should we decide today?\"}"

curl -s "$BASE/v1/workspaces/$WS/sessions/$SES/history?after=0" -H "$AUTH"
```

### Adding a large Markdown file to an agent's knowledge

```sh
AGENT=agt_…   # from GET /agents
UP=$(curl -s -X POST "$BASE/v1/workspaces/$WS/knowledge-uploads" -H "$AUTH" -H 'content-type: application/json' \
  -d "{\"name\":\"notes.md\",\"size\":$(wc -c < notes.md | tr -d ' ')}")
curl -s -X PUT "$(echo "$UP" | jq -r .url)" -H 'content-type: text/markdown' --data-binary @notes.md
curl -s -X POST "$BASE/v1/workspaces/$WS/agents/$AGENT/knowledge" -H "$AUTH" -H 'content-type: application/json' \
  -d "{\"name\":\"notes.md\",\"uploadId\":\"$(echo "$UP" | jq -r .uploadId)\"}"
```
