Skip to content

Managing Kapelle through its API

For an agent that manages a Kapelle deployment on a person's behalf, and for the person who wants to know what it was told. Kapelle runs teams of coding and other agents in sandboxes; the Console API defines and operates them: agents and their versions, team templates, teams, egress, documents, integrations, runs and spend. The onboarding prompt points here; the contract itself is GET /api/v1/openapi.yaml (or .json) on the deployment, and docs/design.md holds the concepts.

Authentication

Send Authorization: Bearer <token> on every request; the base URL is the one in the prompt (.../api/v1). The first call is GET /me: it answers who the token is. A request the role may not make is a 403: do not retry it, tell the person which role it needs. The roles:

  • viewer: reads only; every write answers 403.
  • operator: reads, and may create, change and archive teams, link them, set budgets, read runs, manage a team's documents and MCP servers, and test integrations. Agents, templates, environments, destinations, secrets, users and integration settings answer 403.
  • admin: everything an operator may, and agents, team templates, environments, destinations, secrets, platform skills, users and integration settings.

Conventions

  • JSON everywhere. Errors are application/problem+json: read detail and errors[] (each names the field).
  • Every write of an existing resource needs If-Match with the ETag of a fresh GET of it: 428 without, 412 when stale (re-read and retry once).
  • An edit of an agent or a team template adds a new version and makes it current; the old version stays and can be made current again.
  • Archiving is the delete for teams. Deleting an agent, template, environment or destination is refused with 409 while something uses it.
  • Lists answer {items, next_cursor}; pass limit and cursor to page.
  • Times are UTC, ISO 8601. Money is US dollars.
  • X-Kapelle-CSRF is for browsers with a session cookie, never for a token.

Read before you write

Call these first so you know what exists: GET /me, GET /agents, GET /teams, GET /team-templates, GET /environments, GET /destinations, GET /integrations, GET /runs?limit=20. Never guess a name; read it.

Safety rules

  • Never print, log or repeat a secret value, including your own token. Secrets go in once with PUT /secrets/{name} and are never read back.
  • Do not set an agent's or a team's egress to allow_all without telling the person what it costs: any public host is reachable through the credential gateway; the deny CIDRs still apply; credentials are injected only on their own hosts and never reach the sandbox; CONNECT to any port is allowed.
  • Never archive a team with running or waiting runs without asking.
  • Ask before raising a budget above what the person named, or acting in a way that spends more than a team's daily budget.
  • Confirm destructive changes (archive, delete, disable a user, remove a secret or a destination) with the person first.
  • A route that is not in the list below does not exist for you: say so instead of inventing one.

Routes

Every route a token can call, by area, relative to the API base URL: the method, the path, the role it needs and what it does. The contract has the request and response shapes. Not listed: log in and out, the account's own tokens and minting an onboarding token, which need a browser session.

agents

  • GET /agents (viewer): List agents
  • POST /agents (admin): Create an agent with its first version (admin)
  • DELETE /agents/{name} (admin): Delete an agent no template or team uses (admin)
  • GET /agents/{name} (viewer): One agent with its current version
  • PATCH /agents/{name} (admin): Edit an agent's display fields (admin)
  • GET /agents/{name}/avatar (viewer): The agent's avatar image
  • PUT /agents/{name}/avatar (admin): Set the agent's avatar image, at most 512 KiB (admin)
  • GET /agents/{name}/documents (viewer): The agent's context documents, without their content
  • DELETE /agents/{name}/documents/{doc} (admin): Remove a context document of the agent (admin)
  • GET /agents/{name}/documents/{doc} (viewer): One context document of the agent, with its content
  • PUT /agents/{name}/documents/{doc} (admin): Create (201) or replace (needs If-Match; 200) a context document of the agent: read-only text the worker puts under /workspace/context in the guest ...
  • GET /agents/{name}/versions (viewer): An agent's versions, newest first
  • POST /agents/{name}/versions (admin): Add an agent version and make it current.
  • GET /agents/{name}/versions/{version} (viewer): One agent version

destinations

  • GET /destinations (viewer): List destinations
  • POST /destinations (admin): Create a destination.
  • DELETE /destinations/{name} (admin): Delete a destination no current agent version and no team role override names (not a built-in one) (admin)
  • GET /destinations/{name} (viewer): One destination
  • PUT /destinations/{name} (admin): Replace a destination (not a built-in one).

environments

  • GET /checkpoints (viewer): Checkpoint ids in use
  • GET /environments (viewer): List environments
  • POST /environments (admin): Create an environment; template versions and teams made afterwards copy it (admin)
  • DELETE /environments/{name} (admin): Delete an environment no current agent version or template version names (admin)
  • GET /environments/{name} (viewer): One environment
  • PUT /environments/{name} (admin): Replace an environment's settings.
  • GET /images (viewer): Agent images from the build manifests, newest first

import

  • POST /import (admin): Import roles, team templates and credgw policies from the repository (dry run by default) (admin)

integrations

  • GET /integrations (viewer): The five integrations with their status
  • GET /integrations/mattermost/role-bots (viewer): The Mattermost role bots: one per role of an active team, against the bots the gateway holds, with the display name and picture each shows and what ...
  • GET /integrations/{kind} (viewer): One integration
  • PUT /integrations/{kind} (admin): Replace an integration's record: enabled and its settings.
  • GET /integrations/{kind}/deliveries (viewer): Recent webhook deliveries
  • POST /integrations/{kind}/test (operator): Test an integration's connection: the gateway probes Mattermost, Slack, Linear and Jira with the credentials it runs with, the controller probes ...

mcp-servers

  • GET /mcp-servers (viewer): List MCP servers: the platform ones first (scope platform, no team), then each team's
  • POST /mcp-servers (operator): Add an MCP server to a team.
  • GET /mcp-servers/{id} (viewer): One MCP server; the id is platform: or /
  • DELETE /mcp-servers/{team}/{name} (operator): Remove a team MCP server (operator)
  • PUT /mcp-servers/{team}/{name} (operator): Replace a team MCP server; a proposed one becomes curated.
  • POST /mcp-servers/{team}/{name}/accept (operator): Promote an MCP server an agent proposed to curated; idempotent (operator)

meta

  • GET /openapi.json (viewer): This document as JSON (any logged-in role; it lists routes only and holds no data)
  • GET /openapi.yaml (viewer): This document as YAML, from the generator that writes console/api/openapi.yaml (any logged-in role)

onboarding

  • GET /onboarding (viewer): The two-sentence onboarding prompt and AGENTS.md snippet for the caller's role and this deployment, with no token in them: what a person gives their ...

runs

  • GET /runs (viewer): List runs, newest first
  • GET /runs/{id} (viewer): One run
  • POST /runs/{id}/cancel (operator): Cancel a run; the same command as /cancel in chat.
  • GET /runs/{id}/posts (viewer): The conversation of one run in order (the gateway's console posts: tasks, replies, questions, answers, notices, completion).
  • POST /runs/{id}/replies (operator): Answer a run's open question, or send a follow-up task in the same run.
  • GET /runs/{id}/trace (viewer): The spans of one run, read from Jaeger by the run's trace id (empty when Jaeger is not configured, unreachable or has no such trace)
  • GET /teams/{name}/runs (viewer): A team's runs, newest first
  • POST /teams/{name}/tasks (operator): Give a team a task from the Console: a new run on the console surface, the lead role gets the text.

secrets

  • GET /secrets (viewer): Secret reference metadata (never a value)
  • DELETE /secrets/{name} (admin): Remove a secret's value: a destination secret's reference is removed with it, an integration secret's stays, marked unset; a destination secret is ...
  • GET /secrets/{name} (viewer): One secret reference (never a value)
  • PUT /secrets/{name} (admin): Write a secret's value, once; it is never returned.

session

  • GET /me (viewer): Who the credentials are: the user (id, name, role) and how they were presented, session or token.
  • GET /session (viewer): The logged-in user and the CSRF token

skills

  • GET /skills (viewer): The platform skill catalogue, without bodies
  • DELETE /skills/{name} (admin): Remove a platform skill; a file under roles/skills/ comes back at the next import (admin)
  • GET /skills/{name} (viewer): One platform skill with the text of SKILL.md and its files
  • PUT /skills/{name} (admin): Create (201) or replace (needs If-Match; 200) a platform skill; the catalogue revision of the skill becomes the highest plus one (admin)

team-templates

  • GET /team-templates (viewer): List team templates
  • POST /team-templates (admin): Create a team template with its first version (admin)
  • DELETE /team-templates/{name} (admin): Delete a team template no active team uses (admin)
  • GET /team-templates/{name} (viewer): One team template with its current version
  • PATCH /team-templates/{name} (admin): Edit a team template's description (admin)
  • GET /team-templates/{name}/versions (viewer): A template's versions, newest first
  • POST /team-templates/{name}/versions (admin): Add a team template version and make it current (admin)
  • GET /team-templates/{name}/versions/{version} (viewer): One template version

teams

  • GET /spend (viewer): Spend of all active teams per UTC day, with each team's series (cached 60 s)
  • GET /teams (viewer): List teams
  • POST /teams (operator): Create a team from a template; answers when the team is ready (operator)
  • DELETE /teams/{name} (operator): Archive a team; answers when the archive is done (operator)
  • GET /teams/{name} (viewer): One team with role settings and members
  • PATCH /teams/{name} (operator): Change a team's budget, rework rounds, sleep times, or a role's model, iteration cap, replicas, concurrent runs and confirmation policy.
  • GET /teams/{name}/budget (viewer): A team's budget and its LiteLLM spend per UTC day (cached 60 s; 503 without LiteLLM, 502 when LiteLLM fails; 404 for an unknown team and 404 with ...
  • GET /teams/{name}/documents (viewer): The team's context documents, without their content
  • DELETE /teams/{name}/documents/{doc} (operator): Remove a context document of the team (operator)
  • GET /teams/{name}/documents/{doc} (viewer): One context document of the team, with its content
  • PUT /teams/{name}/documents/{doc} (operator): Create (201) or replace (needs If-Match; 200) a context document of the team: read-only text the worker puts under /workspace/context in the guest ...
  • GET /teams/{name}/links (viewer): A team's surface links
  • PUT /teams/{name}/links (operator): Link a team to a repository, chat channel, Linear team or Jira project; idempotent.
  • DELETE /teams/{name}/links/{surface}/{external_id} (operator): Remove a team's link to a surface; external_id may contain slashes (owner/repo).
  • DELETE /teams/{name}/roles/{role}/egress (admin): Remove the team's egress override for a role; the agent definition's applies (admin)
  • GET /teams/{name}/roles/{role}/egress (viewer): A team role's egress override (null when the agent definition's own applies)
  • PUT /teams/{name}/roles/{role}/egress (admin): Set the team's egress for one role, replacing the agent definition's.
  • GET /teams/{name}/skills (viewer): The team's own stored skills (curated or proposed), read-only, without bodies
  • POST /teams/{name}/upgrade (operator): Move a team to another version of its template; role fields it set by hand are kept (operator)

tools

  • GET /models (viewer): The LiteLLM model aliases and the model each points at
  • GET /tools (viewer): The built-in tool catalogue

users

  • GET /users (admin): List Console users (admin)
  • POST /users (admin): Create a Console account without a password; the answer carries a one-time setup token to hand to the person (there is no mail) (admin)
  • GET /users/{id} (admin): One user (a user may read their own) (admin)
  • PATCH /users/{id} (admin): Change a user's name, role or disabled flag; never leaves the Console without an enabled admin (409); disabling revokes the user's sessions (admin)
  • POST /users/{id}/setup-token (admin): Issue a fresh one-time setup token for a user; earlier unused ones stop working (admin)

Playbooks

Twelve things a person asks an agent to do, as the routes to call in order (paths relative to /api/v1; every write of an existing resource needs If-Match with the ETag of a fresh GET). Generated from console/playbooks.py; the agents page carries the same text.

1. Create a team from a template with a repository and a chat room

Goal: A new team of agents that works on the person's repository and talks in a chat room.

  1. GET /team-templates: pick a template; read GET /team-templates/{name} for its roles
  2. POST /teams: operator. {name, template: {name}, repo_url, home: {surface: "mattermost"|"slack"|"none"}, budget_usd_per_day}; answers when the team is ready (up to a minute); a template that needs a repository is a 422 without repo_url
  3. PUT /teams/{name}/links: operator. {surface: "github", external_id: "owner/repo"} links the repository (422 when the GitHub App is not installed on it); the same route links a Linear team or a Jira project
  4. GET /teams/{name}: read state, home (the room) and role_settings

Check: the team's links list the repository as verified and the answer's home names the room: report the team's name and the room to the person.

2. Change an agent's system prompt and see which teams pick it up

Goal: A new prompt for one agent; the old version stays and can be made current again.

  1. GET /agents/{name}: admin to write; note current_version and the ETag
  2. POST /agents/{name}/versions: admin, with If-Match of that ETag. {from_version: <current>, system_prompt} changes only what you send; the new version becomes current
  3. GET /teams: then GET /teams/{name} for each: role_settings[].agent names the agent

Check: every team whose roles run that agent starts its next run on the new version (a run that is already going keeps the old one). To undo, post a version from_version the old one.

3. Set a daily budget and read today's spend

Goal: A team stops overspending and the person sees what it costs.

  1. GET /teams/{name}: note the ETag and the current budget_usd_per_day
  2. PATCH /teams/{name}: operator, with If-Match: {budget_usd_per_day: 5}
  3. GET /teams/{name}/budget: spent_today_usd, state (normal|warning|exceeded), series per UTC day
  4. GET /spend: all active teams per day, with each team's series

Check: state is normal and today's spend is below the limit; ask the person before raising a budget above what they named.

4. Read what a team is doing now and answer a waiting question

Goal: Find out whether a team is working, stuck or failed, and why.

  1. GET /teams/{name}/runs: newest first; state is running|waiting|succeeded|failed|cancelled
  2. GET /runs/{id}: waiting_for ({role, kind: question|confirmation, since}) for a waiting run, failure_reason for a failed one, chat_thread_url and jaeger_url
  3. GET /runs/{id}/trace: the spans: which role did what, the cost of each

Check: no route answers a waiting run: tell the person who is waiting and for what, and give them chat_thread_url, where they answer in the room.

5. Give an agent a document or a skill

Goal: Context the agent reads at its next run start, or a skill it can use.

  1. PUT /teams/{name}/documents/{doc}: operator. {media_type: "text/markdown", description, content}; 201 on create, 200 with If-Match on replace. Text only, at most 256 KiB each, 20 documents and 384 KiB per owner; it appears under /workspace/context
  2. PUT /agents/{name}/documents/{doc}: admin: the same for every team that runs the agent
  3. PUT /skills/{name}: admin. {body: <SKILL.md text>, resources: {path: text}}, text only; a platform skill any role can name
  4. GET /teams/{name}/skills: a team's own skills are proposed by its agents and curated in chat; read-only

Check: read the document or skill back with its GET route; an agent sees it from its next run.

6. Restrict or open an agent's egress

Goal: Choose which hosts an agent may reach through the credential gateway.

  1. GET /destinations: the named places (host, methods, paths, inject_secret)
  2. POST /agents/{name}/versions: admin. {from_version, egress: {mode: "allowlist"|"allow_all", destinations: [names], upstream_deny_cidrs}} for every team on that agent
  3. PUT /teams/{name}/roles/{role}/egress: admin, with If-Match of its GET: the same body for one team's role; DELETE returns to the agent's own

Check: tell the person what allow_all costs before you set it: any public host is reachable through the gateway; the deny CIDRs still apply; named destinations' credentials are injected only on their own hosts and no secret is ever in the guest; CONNECT to any port is allowed. llm, mcp and otel are always included.

7. Connect a destination that needs a secret and use it from an MCP server

Goal: An agent calls a tool of a server that needs a credential, without the guest ever holding it.

  1. PUT /secrets/{name}: admin. {value} once, never returned; never print it
  2. POST /destinations: admin. {name, host, methods, paths, header_allowlist, inject_secret: "secret:<name>"}; a loopback or private host needs dev_only: true
  3. POST /mcp-servers: operator. {team, name, url, destination, auth: "bearer", roles}; the answer's warnings lists roles whose egress lacks the destination
  4. PUT /teams/{name}/roles/{role}/egress: admin, if warnings named a role: add the destination to its egress
  5. POST /agents/{name}/versions: admin. A PLATFORM server is attached with {from_version, mcp_servers: [names]}

Check: GET /mcp-servers/{id} shows secret.status set and no warnings; a team that needs its own credential gets its own destination.

Goal: Mattermost, Slack, GitHub, Linear or Jira working, and a team linked to it.

  1. PUT /secrets/{name}: admin. One call per secret of the kind, named integrations/<kind>/<setting>
  2. PUT /integrations/{kind}: admin, with If-Match: {enabled, settings}; enabling needs the required settings and secrets (the 422 names what is missing)
  3. POST /integrations/{kind}/test: operator. state ok|error|unknown with a one-line detail
  4. GET /integrations/{kind}: status.applied: pending means the gateway reads the change when it restarts
  5. PUT /teams/{name}/links: operator: link the team to the repository, Linear team or Jira project

Check: the test says ok and status.applied is current: an agent cannot restart the gateway, so when it is pending tell the person that it must be restarted.

9. Invite a colleague and change their role

Goal: A new person can log in to the Console with the role they need.

  1. POST /users: admin. {name, email, role}; the answer holds setup_token once. Give the person the link <console URL>/#setup?token=<setup_token>
  2. POST /users/{id}/setup-token: admin: a fresh link when the first expired
  3. PATCH /users/{id}: admin, with If-Match: {role}; never leaves the Console without an admin

Check: GET /users/{id} shows the role; confirm with the person before disabling anyone.

10. Archive a team and clean up

Goal: A team that is no longer needed stops and releases what it holds.

  1. GET /teams/{name}/runs: nothing running or waiting, or ask the person first
  2. DELETE /teams/{name}: operator, with If-Match; answers when the archive is done
  3. GET /teams/{name}: state is archived

Check: archiving cancels open work, destroys the team's sandboxes, revokes its LiteLLM keys and removes its roles, links and members, which frees the name; the runs, the spend history and the team's memory stay.

11. Give a team a task and read the answer

Goal: A task for a team from the Console, with no chat surface in between.

  1. POST /teams/{name}/tasks: operator. {text}; answers 202 with {run_id, work_item_id, state} and a Location; 409 when the team is archived or lives on a chat surface, 429 when its budget is spent, 503 when no gateway answers
  2. GET /runs/{id}: poll with the run_id: state is running, waiting, succeeded or failed; the lead's task appears under tasks a moment after the 202
  3. GET /runs/{id}/posts: the conversation in order; pass the last seq seen as cursor to read only what is new. The answer and completed posts hold what the team produced; delivery on the run names a branch and pull request

Check: the run is succeeded and the last completed or answer post is what you report; on failed, report failure_reason. To stop early, POST /runs/{id}/cancel.

12. Answer a question a run is waiting on

Goal: A run that paused for a person goes on with the person's answer.

  1. GET /runs: state=waiting: each such run has open_question ({seq, role, text}); the same field is on GET /runs/{id}
  2. POST /runs/{id}/replies: operator. {text, in_reply_to: <open_question.seq>}; without in_reply_to the text is a follow-up task in the same run. 202 on success; 409 for a run that lives on a chat surface (answer it there)
  3. GET /runs/{id}/posts: read on from the question's seq (cursor) to see the run continue

Check: the run's open_question is null and its state is running again; ask the person before answering anything that commits them (a confirmation, a merge).

When something fails

  • Read the problem's detail and errors[] and fix the named field.
  • A run's failure_reason and waiting_for (GET /runs/{id}) say why it stopped; its jaeger_url opens the trace.
  • POST /integrations/{kind}/test says whether an integration's credentials work; GET /integrations/{kind}/deliveries lists its recent webhook deliveries; status.applied says whether the gateway runs the stored settings.
  • There is no audit log route yet; do not look for one.
  • A 5xx or a 502 is Kapelle or a service behind it: say so, retry once, then report it instead of trying other routes.

A Kapelle MCP server

A Kapelle MCP server (kapelle-mcp) that exposes these routes as typed tools is planned. Until it exists, use the HTTP API as described here.