# Server API The local server started by `kimi web` exposes two programmatic surfaces: a REST API (`/api/v1`, plus `/api/v2/sessions`) and a WebSocket event stream (`/api/v1/ws`). This page is the protocol reference for both. For how to start the server and its command-line options, see the [kimi command](./kimi-command.md#kimi-web) reference; for an end-to-end walkthrough, see [Local server and API](../guides/server.md). The complete request/response schema of every endpoint is owned by the server's live specification documents: `GET /openapi.json` (OpenAPI) and `GET /asyncapi.json` (AsyncAPI). Both require authentication. ::: warning The REST and WebSocket APIs described on this page are experimental: interface stability is not guaranteed, and endpoints, fields, and event types may change in any release. When integrating, rely on the `/openapi.json` and `/asyncapi.json` documents served by your version. ::: ## Conventions ### Address The default address is `http://127.0.0.1:58627`. When the port is taken, the server retries with the next port (up to 100 times); use `--port` / `--host` to change the bind. Multiple instances can coexist under the same home directory; running instances register under `~/.kimi-code/server/instances/`. ### Authentication All `/api/*` paths (including `/openapi.json` and `/asyncapi.json`) require the bearer token, except: - `OPTIONS` preflight requests - `GET /api/v1/healthz` (liveness probe) - Static web assets (non-`/api/` paths) How to carry it: REST uses the `Authorization: Bearer ` header; the WebSocket upgrade accepts the same header or the subprotocol `kimi-code.bearer.`. Token generation and rotation are covered in [Local server and API: Authentication](../guides/server.md#authentication). Failed authentication returns HTTP 401 with envelope code `40101`. On non-loopback binds, a source that fails authentication 10 times within 60 seconds is banned for 60 seconds, during which every request gets HTTP 429 (code `42901`). ### Response envelope Every JSON response is wrapped in a uniform envelope: ```json { "code": 0, "msg": "success", "data": {}, "request_id": "01JZX4A6E7M8V0R3Q0N2K2M5Q9" } ``` - `code`: the business outcome; `0` means success. See the error-code bands below. - `data`: the payload on success. Note that some "error" envelopes also carry a non-null `data` — for example, resolving an already-resolved approval returns `40902` with `data.resolved` set to `false` — so clients should check `code` first, then `data`. - `request_id`: a ULID for this request. Clients may supply one via the `X-Request-Id` header; invalid values are regenerated by the server. The HTTP status is almost always 200; the business outcome lives in `code`. Exceptions: | Situation | HTTP status | | --- | --- | | Authentication failure / rate limit | 401 / 429 | | Provider created, provider catalog imported | 201 | | Provider deleted | 204 | | Binary/streaming endpoints | 206 (Range) / 304 (ETag unchanged) where supported — capabilities differ per endpoint, see [Binary and streaming endpoints](#binary-and-streaming-endpoints) | | `GET /api/v1/files/{file_id}` download errors | real 404 / 500 (still carrying an envelope body) | The 201 responses still carry the standard envelope (`code` 0) — only the status line follows the REST convention for resource creation. A 204 response has no body by definition, so a successful delete is reported by the status code itself. ### Error codes Error codes are grouped by band: | Band | Meaning | Examples | | --- | --- | --- | | `0` | Success | | | `400xx` | Bad request | `40001` validation failed (`details` lists each field), `40003` provider is OAuth-managed | | `401xx` | Auth and readiness | `40101` unauthorized, `40110` no provider configured, `40113` model not resolved | | `404xx` | Not found | `40401` session, `40408` MCP server, `40409` file path | | `409xx` | State conflict | `40901` session busy, `40902` approval already resolved, `40922` page conditions mismatch `page_token` | | `410xx` | Expired | `41001` approval timed out, `41002` question timed out, `41003` temporary file expired | | `413xx` | Size or boundary exceeded | `41302` file read over 10 MB, `41304` path escapes the session directory | | `429xx` | Rate limited | `42901` auth-failure ban, `42902` too many fs watches | | `500xx` | Server internal error | `50001` uncaught exception, `50003` persistence failure | | `6xxxx` / `7xxxx` / `8xxxx` | Tool runtime / LLM provider / MCP passthrough errors; `msg` carries the upstream text | | ### Pagination List endpoints come in two styles: - **Cursor style**: `before_id` / `after_id` (mutually exclusive) plus `page_size` (1–100), responding with `{ items, has_more }`. Used by the session list, message list, transcript, and others. - **`page_token`**: an opaque token (bound to a fingerprint of the query conditions), used by `POST /api/v1/search` and `GET /api/v2/sessions`. Changing any query condition mid-pagination invalidates the token: v2 returns `40922`, search returns `40001`. `GET /api/v2/sessions` also offers a stateless `page` page-number mode as an alternative. ## REST endpoints Endpoints are grouped by resource below. A `:{action}` suffix in a path is the action convention — POST to `path:action` on a single resource for non-CRUD operations (such as `:fork` and `:archive` on a session). ### Server and metadata | Method and path | Description | | --- | --- | | `GET /api/v1/healthz` | Liveness probe; auth-exempt | | `GET /api/v1/meta` | Server version, capability map, `server_id`, experimental flags | | `POST /api/v1/shutdown` | Graceful shutdown (replies 200 first); mounted only on loopback binds | ### Login and usage | Method and path | Description | | --- | --- | | `GET /api/v1/auth` | Auth readiness snapshot | | `POST /api/v1/oauth/login` | Start the OAuth device-code login flow | | `GET /api/v1/oauth/login` | Poll the login flow state | | `DELETE /api/v1/oauth/login` | Cancel a pending login flow | | `POST /api/v1/oauth/logout` | Log out the managed provider | | `GET /api/v1/oauth/usage` | Plan usage and limits | | `GET /api/v1/oauth/userinfo` | Account profile | ### Config | Method and path | Description | | --- | --- | | `GET /api/v1/config` | Read the global config (secret fields redacted) | | `POST /api/v1/config` | Merge-patch the config; broadcasts `event.config.changed` | ### Models and providers | Method and path | Description | | --- | --- | | `GET /api/v1/models` | List configured model aliases | | `POST /api/v1/models/{model_id}:set_default` | Set the global default model | | `GET /api/v1/providers` | List providers | | `POST /api/v1/providers` | Create a provider (201) | | `GET /api/v1/providers/{provider_id}` | Read a provider (reveals the stored key) | | `PUT /api/v1/providers/{provider_id}` | Replace a provider | | `DELETE /api/v1/providers/{provider_id}` | Delete a provider (204) | | `POST /api/v1/providers/{provider_id}:refresh` | Refresh one provider's model metadata | | `POST /api/v1/providers:{action}` | Collection actions: `refresh` / `refresh_oauth` / `import_catalog` / `import_registry` | | `GET /api/v1/catalog/providers` | Browse the models.dev directory (server-proxied) | | `GET /api/v1/catalog/providers/{catalog_id}` | Read one directory entry | ### Sessions | Method and path | Description | | --- | --- | | `POST /api/v1/sessions` | Create a session (requires `workspace_id` or `metadata.cwd`) | | `GET /api/v1/sessions` | List sessions; cursor pagination with filters such as `busy` and `archived_only` | | `GET /api/v1/sessions/{session_id}` | Read one session | | `GET /api/v1/sessions/{session_id}/profile` | Read the session profile | | `POST /api/v1/sessions/{session_id}/profile` | Update title, metadata, agent config | | `POST /api/v1/sessions/{session_id}:{action}` | Session actions: `fork` / `compact` / `undo` / `abort` / `btw` / `archive` / `restore` | | `GET /api/v1/sessions/{session_id}/children` | List child sessions | | `POST /api/v1/sessions/{session_id}/children` | Create a child session (fork with a tag) | | `GET /api/v1/sessions/{session_id}/status` | Realtime status rollup | | `GET /api/v1/sessions/{session_id}/goal` | Current goal snapshot (`null` when none) | | `GET /api/v1/sessions/{session_id}/warnings` | Session-level warnings | | `POST /api/v1/sessions/{session_id}/export` | Export the session with diagnostics (zip stream, not enveloped) | | `GET /api/v1/sessions/{session_id}/snapshot` | Full snapshot for client rebuilds (with `as_of_seq` and `epoch`) | ### Messages and transcript | Method and path | Description | | --- | --- | | `GET /api/v1/sessions/{session_id}/messages` | Page messages (`before_id` / `after_id` / `role`) | | `GET /api/v1/sessions/{session_id}/messages/{message_id}` | Read one message | | `GET /api/v1/sessions/{session_id}/transcript` | Turn-paged transcript (requires `agent_id`); global state rides along unpaginated | | `GET /api/v1/sessions/{session_id}/transcript/ops` | Op-batch catch-up (`since_seq`); `complete: false` means a full refresh is needed | | `GET /api/v1/sessions/{session_id}/transcript/user-messages` | Turn-opening user inputs, unpaginated | | `GET /api/v1/sessions/{session_id}/transcript/plan` | ExitPlanMode plan content, path, and review outcome | ### Prompts | Method and path | Description | | --- | --- | | `GET /api/v1/sessions/{session_id}/prompts` | Active and queued prompts | | `POST /api/v1/sessions/{session_id}/prompts` | Submit a prompt (content-part array, optional model / permission-mode overrides) | | `POST /api/v1/sessions/{session_id}/prompts:steer` | Steer queued prompts into the active turn | | `POST /api/v1/sessions/{session_id}/prompts/{prompt_id}:abort` | Abort a running prompt | | `POST /api/v1/sessions/{session_id}/prompts/{prompt_id}:steer` | Steer one queued prompt | ### Approvals and questions | Method and path | Description | | --- | --- | | `GET /api/v1/sessions/{session_id}/approvals` | List approval requests (filter with `status=pending`) | | `POST /api/v1/sessions/{session_id}/approvals/{approval_id}` | Resolve an approval | | `GET /api/v1/sessions/{session_id}/questions` | List questions | | `POST /api/v1/sessions/{session_id}/questions/{question_id}` | Answer a question | | `POST /api/v1/sessions/{session_id}/questions/{question_id}:dismiss` | Dismiss a question | ### Background tasks | Method and path | Description | | --- | --- | | `GET /api/v1/sessions/{session_id}/tasks` | List background tasks | | `GET /api/v1/sessions/{session_id}/tasks/{task_id}` | Read a task (optional output preview) | | `POST /api/v1/sessions/{session_id}/tasks/{task_id}:cancel` | Cancel a task | ### Skills, tools, and MCP | Method and path | Description | | --- | --- | | `GET /api/v1/sessions/{session_id}/skills` | Per-session skill catalog | | `GET /api/v1/workspaces/{workspace_id}/skills` | Session-less skill catalog for a workspace | | `POST /api/v1/sessions/{session_id}/skills/{skill_name}:activate` | Activate a skill (starts a turn) | | `GET /api/v1/tools` | List tools of the effective agent | | `GET /api/v1/mcp/servers` | List MCP servers | | `POST /api/v1/mcp/servers/{mcp_server_id}:restart` | Restart an MCP server | ### Terminals PTY terminal endpoints; mounted only on loopback binds. | Method and path | Description | | --- | --- | | `GET /api/v1/sessions/{session_id}/terminals` | List terminals | | `POST /api/v1/sessions/{session_id}/terminals` | Create a terminal | | `GET /api/v1/sessions/{session_id}/terminals/{terminal_id}` | Read a terminal (including scrollback) | | `POST /api/v1/sessions/{session_id}/terminals/{terminal_id}:close` | Close a terminal | ### Workspaces | Method and path | Description | | --- | --- | | `GET /api/v1/workspaces` | List registered workspaces | | `POST /api/v1/workspaces` | Register a workspace (idempotent on the root path) | | `PATCH /api/v1/workspaces/{workspace_id}` | Rename | | `DELETE /api/v1/workspaces/{workspace_id}` | Unregister (keeps on-disk content) | | `GET /api/v1/workspaces/{workspace_id}/trust` | Read the trust state | | `POST /api/v1/workspaces/{workspace_id}/trust` | Grant trust | | `POST /api/v1/workspaces/{workspace_id}/untrust` | Revoke trust | ### File system In-session file operations go through `POST /api/v1/sessions/{session_id}/fs:{action}` with JSON bodies; actions are `list` / `read` / `list_many` / `stat` / `stat_many` / `mkdir` / `search` / `grep` / `git_status` / `diff` / `open` / `open-in` / `reveal`. In addition: | Method and path | Description | | --- | --- | | `POST /api/v1/workspace/fs:search` | Session-less workspace search (the body carries the workspace reference) | | `GET /api/v1/sessions/{session_id}/fs/{path}:download` | Download a session file (binary, see below) | | `GET /api/v1/fs:browse` | List host directories (folder picker) | | `GET /api/v1/fs:home` | The user's home directory and recent workspaces | | `GET /api/v1/fs:content` | Raw bytes of any host file (gated only by the token — be careful when exposing the port) | | `POST /api/v1/fs:mkdir` | Create a directory by absolute path | ### File uploads | Method and path | Description | | --- | --- | | `POST /api/v1/files` | Multipart upload (`file` field, optional `name` and `expires_in_sec`); returns file metadata | | `GET /api/v1/files/{file_id}` | Download (binary; errors use real HTTP statuses) | | `DELETE /api/v1/files/{file_id}` | Delete | ### Global search and misc | Method and path | Description | | --- | --- | | `POST /api/v1/search` | Cross-session full-text search; `mode` is `terms` (default) or `literal` (exact substring); `page_token` pagination | | `GET /api/v1/connections` | List live WebSocket connections | | `GET /api/v2/sessions` | Next-generation session list, see below | | `POST /api/v2/sessions:archive` | Batch-archive sessions, see below | | `POST /api/v2/sessions:restore` | Batch-restore archived sessions, see below | | `/api/v1/debug/*` | Reflection debug RPC; mounted only with `--debug-endpoints` on loopback, not a stable protocol | ### `GET /api/v2/sessions` A next-generation session query for list views — filtering, sorting, and field groups all travel in query parameters: | Parameter | Description | | --- | --- | | `workspace.id` | Filter by workspace; repeatable | | `activity.status` | Filter by activity status: `running` / `approval` / `question` / `failed` / `idle`; repeatable | | `meta.updated_after` | Only sessions updated after this time (epoch milliseconds) | | `meta.updated_before` | Only sessions updated before this time (epoch milliseconds) | | `meta.archived` | `true` / `false` (default) / `all` | | `sort` | `meta.updated_at_desc` (default) / `meta.updated_at_asc` / `meta.created_at_desc` | | `include` | Comma-separated extra field groups; currently only `git` (branch and PR info, deduplicated per directory and cached for 60 seconds) | | `fields` | Comma-separated item projection; currently only `id,archived`, trimming each item to `{ id, archived }` (select-all-matching flows). Not combinable with `include=git` (`40001`) | | `page_size` | 1–100, default 50; up to 10000 with the `id,archived` projection | | `page_token` | Pagination token from the previous page | | `page` | Stateless 1-based page number; mutually exclusive with `page_token` (`40001` when combined) | Every response item carries the `workspace`, `meta`, and `activity` groups, plus `git` when `include=git` — or just `{ id, archived }` under `fields=id,archived`. Every page additionally carries `total`, the size of the filtered set. The page token binds the first page's query conditions (including the projection); changing them mid-pagination returns `40922`. `page` mode is a stateless alternative for jumping to arbitrary pages: every request is an independent snapshot, no token is minted, and `next_page_token` is always `null`. ### `POST /api/v2/sessions:archive` and `POST /api/v2/sessions:restore` Batch archive/restore for session-management views. The body is `{ "ids": ["session_..."] }` — non-empty, at most 5000 unique ids (duplicates collapse). Live sessions go through the full lifecycle; cold sessions are patched on disk without being loaded. Only a body validation failure fails the whole request (`40001`). Otherwise the response is per-item: `data.results` keeps the input order with `{ id, ok }` or `{ id, ok: false, error }` (an unknown id reports `40401` in its own item), plus `succeeded` / `failed` counts. ```json { "code": 0, "msg": "success", "data": { "results": [ { "id": "session_a", "ok": true }, { "id": "session_b", "ok": false, "error": { "code": 40401, "message": "session session_b does not exist" } } ], "succeeded": 1, "failed": 1 }, "request_id": "req_..." } ``` ## WebSocket protocol ### Connect The only endpoint is `ws://:/api/v1/ws`; authentication happens at the upgrade request (see [Authentication](#authentication) above). Once connected, the server immediately sends `server_hello`: ```json { "type": "server_hello", "timestamp": "2026-01-01T00:00:00.000Z", "payload": { "ws_connection_id": "conn_01JZX4...", "protocol_version": 2, "max_event_buffer_size": 1000, "capabilities": { "event_batching": false, "compression": false } } } ``` Note that the server never sends heartbeats and never disconnects an idle connection — keepalive and reconnection are the client's job. ### Control frames Clients send JSON frames `{ "type", "id"?, "payload" }`; every request frame gets an acknowledgement `{ "type": "ack", "id", "code", "msg", "payload" }`, where `code` 0 means success. | Frame | payload | Description | | --- | --- | --- | | `subscribe` | `{ session_ids, cursors?, agent_filter? }` | Subscribe to session events; with `cursors` (per-session `{seq, epoch}`) the server replays missed durable events | | `unsubscribe` | `{ session_ids }` | Drop session subscriptions | | `subscribe_v2` | `{ session_id, transcript, transcript_since? }` | Subscribe to transcript streams (the only transcript channel); `transcript` sets per-agent grades | | `unsubscribe_v2` | `{ session_id, agent_ids? }` | Detach transcript streams; omitting `agent_ids` means the whole session | | `watch_fs_add` / `watch_fs_remove` | `{ session_id, paths, recursive? }` | Subscribe to / unsubscribe from file-change notifications (`event.fs.changed`) | | `client_hello` | `{ client_id }` | Handshake frame; the remaining fields are legacy compatibility | ### Events Event frames look like `{ "type", "seq", "epoch"?, "volatile"?, "offset"?, "session_id"?, "timestamp", "payload" }`, where `type` is the event type itself. Two delivery scopes: - **Global events**: sent to every established connection, no subscription needed — `session.meta.updated`, `event.session.created`, `event.session.work_changed`, `event.session.status_changed`, `event.workspace.*`, `event.config.*`. - **Session events**: sent only to connections subscribed to that session, subject to `agent_filter`. Main families: | Family | Main events | | --- | --- | | Turns | `turn.started`, `turn.ended`, `turn.step.started` / `completed` / `interrupted` / `retrying` | | Streaming text | `assistant.delta`, `thinking.delta` (carry `offset` for alignment) | | Tool calls | `tool.call.started`, `tool.call.delta`, `tool.progress`, `tool.result` | | Interactions | `event.approval.requested` / `resolved`, `event.question.requested` / `answered` / `dismissed` | | Subagents | `subagent.spawned` / `started` / `suspended` / `completed` / `failed` | | Background | `task.started` / `terminated`, `shell.started` / `output` / `completed` | | Misc | `compaction.*`, `skill.activated`, `goal.updated`, `prompt.*`, `error`, `warning` | Events also split into durable and volatile: durable events carry a strictly increasing `seq`, are journaled, and can be replayed; volatile events (the `*.delta` family, `tool.progress`, `shell.*`, and similar) are marked `volatile: true` and never replayed. When consuming a volatile text stream, compare `offset` (the cumulative character offset within the turn) against your locally accumulated text: below the local length means a duplicate frame; above means a gap that needs snapshot recovery. ### Reconnect and recovery After reconnecting, pass each session's last applied `{seq, epoch}` in `subscribe`'s `cursors`; the server replays the gap. If you fall more than the buffer (1000 events) behind, or the cursor is no longer valid, you get `resync_required` instead. In that case, call `GET /api/v1/sessions/{session_id}/snapshot` for a full snapshot (with `as_of_seq` and `epoch`), then subscribe again with the fresh cursor. ### Transcript protocol `subscribe_v2`'s `transcript` field sets a per-agent grade: `off` / `turn` / `block` / `delta` (the `"*"` key sets the default grade), with higher grades pushing finer detail. An agent with a non-`off` grade receives two frame types: `transcript.reset` (a baseline snapshot; history pages in over REST) and `transcript.ops` (incremental op batches with a per-agent strictly increasing `seq`). The agent's legacy events are suppressed on that connection and carried by transcript frames instead. After a disconnect, resume with `transcript_since`; when the server's op journal cannot cover the gap (REST catch-up returns `complete: false`), do a full refresh. The REST counterparts are `GET .../transcript` (turn-paged) and `GET .../transcript/ops?since_seq=` (op-batch catch-up). ## Binary and streaming endpoints The following endpoints stream binary bodies instead of a JSON payload. Their HTTP capabilities differ per endpoint: | Method and path | Description | Range (206) | ETag / 304 | | --- | --- | --- | --- | | `GET /api/v1/files/{file_id}` | Download an uploaded file | Yes | No (sends an `etag` header but ignores `If-None-Match`) | | `GET /api/v1/sessions/{session_id}/fs/{path}:download` | Download a session workspace file | Yes | Yes | | `GET /api/v1/fs:content` | Raw bytes of any host file (gated only by the token — be careful when exposing the port) | Yes | Yes | | `POST /api/v1/sessions/{session_id}/export` | Export the session with diagnostics (zip stream) | No | No | Error semantics differ as well: `GET /api/v1/files/{file_id}` answers lookup and storage failures with real 404 / 500 statuses (parameter validation still uses the HTTP 200 envelope), while the other three report every failure through the standard [response envelope](#response-envelope) — clients must keep checking the envelope `code` on those endpoints. ## Next steps - [Local server and API](../guides/server.md) — startup, authentication, and the end-to-end calling flow - [kimi command](./kimi-command.md#kimi-web) — all `kimi web` command-line options