Server API
The local server started by kimi web exposes two programmatic surfaces: a REST API (/api/v1, plus /api/v2/sessions and /api/v2/mcp) 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 reference; for an end-to-end walkthrough, see Drive a session over the API below.
This page is a curated, human-readable reference: it documents every endpoint's parameters, request bodies, and response shapes below. The precise machine-readable schema of every endpoint is owned by the server's live specification documents: GET /openapi.json (OpenAPI) and GET /asyncapi.json (AsyncAPI), both generated from the same validation schemas the server enforces at runtime. Both require authentication; when this page and the live spec ever disagree, the live spec wins.
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:
OPTIONSpreflight requestsGET /api/v1/healthz(liveness probe)- Static web assets (non-
/api/paths)
How to carry it: REST uses the Authorization: Bearer <token> header; the WebSocket upgrade accepts the same header or the subprotocol kimi-code.bearer.<token>. Token generation and rotation are covered in Using Kimi Code in the browser: Getting started.
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:
{
"code": 0,
"msg": "success",
"data": {},
"request_id": "01JZX4A6E7M8V0R3Q0N2K2M5Q9"
}code: the business outcome;0means success. See the error-code bands below.data: the payload on success. Note that some "error" envelopes also carry a non-nulldata— for example, an undo that cannot run returns40911withdataset to{ reason, requestedCount, undoableCount }— so clients should checkcodefirst, thendata.request_id: a ULID for this request. Clients may supply one via theX-Request-Idheader; 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 |
GET /api/v1/files/{file_id}, /api/v1/fs:content, and session :download errors | real 4xx / 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, 40006 duplicate workspace source folder |
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 interaction already ended, 40922 page conditions mismatch page_token, 40931 session occupied by another client, 40932 session updated by another client, 40935 Tower mode cannot be entered |
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 |
40931 and 40932 only appear when the same session is open in multiple clients. 40931 means another Kimi Code process is writing to the session right now; wait for it to go idle, then retry. If that process is known to be gone and the lock was left behind, call POST /api/v1/sessions/{session_id}:takeover and retry. When the session was updated by another client, prompt submission, skill activation, and compaction reload it on the server, send subscribers a fresh turns frame, and run the request again, so callers never see 40932 from them. Only undo returns 40932: the server has already reloaded the session and nothing was undone; let the user review the refreshed history before undoing again.
Pagination
List endpoints come in two styles:
- Cursor style:
before_id/after_id(mutually exclusive) pluspage_size(1–100), responding with{ items, has_more }. Used by the session list, message list, and others. Agent turns page by turnordinalinstead (before_ordinal/page_size). page_token: an opaque token (bound to a fingerprint of the query conditions), used byPOST /api/v1/searchandGET /api/v2/sessions. Changing any query condition mid-pagination invalidates the token: v2 returns40922, search returns40001.GET /api/v2/sessionsalso offers a statelesspagepage-number mode as an alternative.
Drive a session over the API
The minimal flow with curl: check the server → create a session → subscribe to events → submit a prompt → read history back. The examples assume the server runs at the default address and the token is stored in the shell variable TOKEN.
- Check server status:
curl -s -H "Authorization: Bearer $TOKEN" http://127.0.0.1:58627/api/v1/metaEvery JSON response is wrapped in a uniform envelope — { "code": 0, "msg": "success", "data": ..., "request_id": "..." }. The business outcome lives in code (0 means success); the HTTP status only reports transport-level results.
- Create a session;
metadata.cwdsets the working directory:
curl -s -X POST http://127.0.0.1:58627/api/v1/sessions \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"metadata": {"cwd": "/path/to/project"}}'The returned data.id (shaped like session_...) is the session id used by every subsequent request.
- Connect to the WebSocket and subscribe to session events. Any WebSocket client works; below is a dependency-free Node.js script (Node.js 22+ ships a built-in
WebSocketclient):
// subscribe.mjs — usage: TOKEN=... node subscribe.mjs session_...
const ws = new WebSocket('ws://127.0.0.1:58627/api/v1/ws', [
`kimi-code.bearer.${process.env.TOKEN}`,
]);
ws.onmessage = (e) => console.log(e.data);
ws.onopen = () =>
ws.send(
JSON.stringify({
type: 'subscribe',
request_id: '1',
session_id: process.argv[2],
}),
);- Submit a prompt:
curl -s -X POST http://127.0.0.1:58627/api/v1/sessions/<session_id>/prompts \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"content": [{"type": "text", "text": "Introduce this repository in one sentence"}]}'The subscriber first gets a turns frame with the main agent's latest turns, then, in order: turn.started (turn begins) → item.ended for the prompt → item.started, item.delta, and item.ended as reasoning, the reply, and tool calls stream → turn.updated when a step ends → turn.ended (turn finishes). See Conversation protocol.
- Read history back over REST at any time:
curl -s -H "Authorization: Bearer $TOKEN" \
"http://127.0.0.1:58627/api/v1/sessions/<session_id>/messages?page_size=20"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 |
GET /api/v1/healthz
Liveness probe for scripts and process supervisors. It is the one /api endpoint exempt from the bearer token (see Authentication) and answers without touching config or the engine.
On success, data is { "ok": true }.
GET /api/v1/meta
Returns this instance's identity and capability map. Most fields are frozen at boot; experimental_flags and features are resolved per request, so a flag flip or a failed feature shows up in the next response.
On success, data carries:
| Field | Type | Description |
|---|---|---|
server_version | string | Server version |
capabilities | object | Capability map — websocket, file_upload, fs_query, mcp, tasks, all always true |
server_id | string | Unique id of this server instance |
started_at | string | Boot time, ISO 8601 |
open_in_apps | array | Host apps usable as open-in targets (finder / cursor / vscode / iterm / terminal); currently always empty |
dangerous_bypass_auth | boolean | Whether the server was started with --dangerous-bypass-auth (clients may skip the token prompt) |
backend | string | Engine backend, v1 or v2; always v2 for this server |
web_title | string | Custom browser tab title from --web-title; omitted when unset |
experimental_flags | object | Experimental flag id → enabled, resolved at request time |
features | array | Engine features as { name, state, meta }; state is Pending / Activating / Active / Unloading / Failed |
POST /api/v1/shutdown
Asks the server to shut down gracefully. The reply is sent first and the shutdown runs immediately after, so the caller can trust the response it received. The route is mounted only on loopback binds — on a non-loopback bind it is not registered at all (requests hit a 404) unless the server was started with --allow-remote-shutdown.
On success, data is { "ok": true }.
Login and usage
These endpoints drive the managed Kimi OAuth login lifecycle and expose account-level information. The managed provider is named managed:kimi-code; the optional provider parameter on every endpoint below defaults to it.
| Method and path | Description |
|---|---|
GET /api/v1/auth | Auth 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 quota and booster wallet |
GET /api/v1/oauth/userinfo | Account profile |
GET /api/v1/oauth/region | Resolve the client region (mainland-cn / global) |
GET /api/v1/auth
Auth snapshot: whether the default model resolves to a usable provider configuration, plus the managed provider's login state. models_ready is true when the global default_model alias exists in the model table and resolves to a configured provider — including providerless flat models carrying their own base_url and models injected through KIMI_MODEL_* environment variables. It does not verify credentials, so a prompt can still fail afterwards with 40111 / 40112.
On success, data carries models_ready (boolean), providers_count (number of configured providers), and managed_provider (null, or { name, status } with status one of authenticated / expired / revoked / unauthenticated). The global default model alias itself is read from GET /api/v1/config (default_model), not from this endpoint.
POST /api/v1/oauth/login
Starts an OAuth device-code login flow for the managed provider; starting a new flow aborts any pending flow for the same provider. When the account is already authenticated, no user interaction is needed and the response reports authenticated immediately.
| Parameter | In | Type | Description |
|---|---|---|---|
provider | body | string | Managed provider name. Default managed:kimi-code |
region | body | string | mainland-cn or global; overrides the region resolution described under GET /api/v1/oauth/region for this flow |
On success, data has one of two shapes. A pending flow — { flow_id, provider, status: "pending", verification_uri, verification_uri_complete, user_code, expires_in, interval, expires_at }: open verification_uri_complete (or verification_uri and enter user_code), then poll GET /api/v1/oauth/login every interval seconds until the flow resolves or expires_at passes (expires_in is the same deadline in seconds). The already-authenticated fast path — { flow_id, provider, status: "authenticated" }.
GET /api/v1/oauth/login
Polls the login flow state for a provider. Returns null when no flow has been started.
| Parameter | In | Type | Description |
|---|---|---|---|
provider | query | string | Managed provider name. Default managed:kimi-code |
On success, data is null or a flow snapshot: { flow_id, provider, status, verification_uri, verification_uri_complete, user_code, expires_in, expires_at, interval }, where status is pending / authenticated / denied / expired / cancelled. Once the flow leaves pending, resolved_at records when it reached its terminal state and error_message describes a failed flow.
DELETE /api/v1/oauth/login
Cancels the pending login flow for a provider. When no flow is pending, the call is a no-op that reports the last known state.
| Parameter | In | Type | Description |
|---|---|---|---|
provider | query | string | Managed provider name. Default managed:kimi-code |
On success, data is { cancelled, status }: cancelled is true only when a pending flow was actually aborted, and status is the flow state after the call.
POST /api/v1/oauth/logout
Logs out the managed provider: discards the stored OAuth credential, aborts any pending login flow, and removes the managed provider from the configuration. OAuth-managed providers reject manual edit and delete (see PUT / DELETE /api/v1/providers/{provider_id} below), so log out first to remove one.
For the default Kimi provider (managed:kimi-code), logout also tears down this machine's Remote Control and clears cached Remote Control cloud sessions. When no tunnel is running or another process hosts it, logout first deactivates this machine's device and holds the machine lock so no process on this machine can start Remote Control again until the credential is discarded; a cleanup failure aborts the logout. When this server hosts the tunnel, the credential is discarded first and the tunnel is torn down right after the response is flushed, so a logout sent through the Remote Control link still receives its response. Logging out any other provider leaves Remote Control untouched.
| Parameter | In | Type | Description |
|---|---|---|---|
provider | body | string | Managed provider name. Default managed:kimi-code |
On success, data is { logged_out: true, provider }.
GET /api/v1/oauth/usage
Plan quota and booster wallet of the managed account, fetched live from the account service. An upstream failure does not fail the envelope — it comes back in-band with kind: "error".
| Parameter | In | Type | Description |
|---|---|---|---|
provider | query | string | Managed provider name. Default managed:kimi-code |
On success, data is { kind: "ok", quota } or { kind: "error", message, status? }, where status is the upstream HTTP status when one exists. In the ok shape, quota is { usages, extraUsage }: usages carries one { usedRatio, resetAt? } entry per quota window the account has — limit5h, limit7d, monthTotal, monthCode — with usedRatio as a 0–1 float and resetAt as an RFC3339 reset timestamp, and clients render whichever entries are present; extraUsage (nullable) is the pay-as-you-go wallet: { balanceCents, totalCents, monthlyChargeLimitEnabled, monthlyChargeLimitCents, monthlyUsedCents, currency }.
GET /api/v1/oauth/userinfo
Profile of the managed account, with the same in-band kind: "error" convention as GET /api/v1/oauth/usage.
| Parameter | In | Type | Description |
|---|---|---|---|
provider | query | string | Managed provider name. Default managed:kimi-code |
On success, data is { kind: "ok", userInfo } or { kind: "error", message, status? }. userInfo always carries userId, nickname, status, region, userLevel, userLevelName, domain, and domainName, and may add globalId, bio, avatar, username, email, phone ({ countryCode, number }), createdTime, and lastLoginTime.
GET /api/v1/oauth/region
Resolves which Kimi region this client belongs to without probing over the network. An OAuth host pinned by environment or config wins first, then the configured OAuth key, then the install-channel marker in <home>/region.
When the client is logged out and none of those inputs decides, it infers the region from the device time zone and locale. A successful inference is cached in <home>/region.json and invalidated when the login state changes; raw signal values are not uploaded. Missing or conflicting signals fall back to mainland-cn for the current process without persisting that fallback, so the next start tries again. Set KIMI_CODE_REGION_MARKER=off to disable both the install-channel marker and local inference.
On success, data is { region } with region one of mainland-cn / global.
Remote Control
These endpoints manage the machine-wide Remote Control tunnel (see the Remote Control guide) and the account's device registry. The tunnel state is per machine; the device registry is per account and lives in the cloud, so device endpoints require a Kimi login and proxy the relay with a server-side session — OAuth credentials never reach the caller.
| Method and path | Description |
|---|---|
GET /api/v1/remote-control | Read the tunnel status |
POST /api/v1/remote-control | Start or stop the tunnel |
GET /api/v1/remote-control/devices | List the account's devices |
PATCH /api/v1/remote-control/devices/{id} | Rename a device |
POST /api/v1/remote-control/devices/{id}/deactivate | Take a device offline |
DELETE /api/v1/remote-control/devices/{id} | Remove a device from the registry |
GET /api/v1/remote-control
Returns the current tunnel status. When the tunnel is off but another live process on this machine holds the single-instance lock, data.conflict carries that holder; the lock is re-inspected on every read, so a dead holder clears both conflict and the stored error.
On success, data is { enabled, state, url?, device_id?, device_name?, error?, conflict? } with state one of off / starting / on / stopping and conflict as { pid, local_origin, device_id, url, started_at }.
POST /api/v1/remote-control
Starts or stops the tunnel.
| Parameter | In | Type | Description |
|---|---|---|---|
enabled | body | boolean | true to start the tunnel, false to stop it |
On success, data is the same status object as GET /api/v1/remote-control. Starting while another live process on this machine holds the single-instance lock fails with REMOTE_CONTROL_ALREADY_RUNNING (40928), with the lock holder in details in the same shape as conflict.
GET /api/v1/remote-control/devices
Lists the account's Remote Control devices from the cloud registry.
On success, data is { devices, max_devices? }. Each device is { device_id, alias, status, platform, client_version, local_base_url, created_at, updated_at, last_remote_access_at } with status one of online / offline.
PATCH /api/v1/remote-control/devices/{id}
Renames a device.
| Parameter | In | Type | Description |
|---|---|---|---|
id | path | string | Device identifier |
alias | body | string | New display name, trimmed; 1–50 characters |
Blank or over-long aliases fail with VALIDATION_FAILED before reaching the cloud. On success, data is { ok: true }.
POST /api/v1/remote-control/devices/{id}/deactivate
Takes a device offline in the cloud registry. Offline devices stay listed and do not consume the plan's device quota; a device comes back the next time it turns on Remote Control. When the target is this machine, the local tunnel is stopped first (an in-flight start is cancelled); the request then holds the machine lock while deactivating, so a concurrent enable cannot re-register the device behind a successful response. A holder that does not stop in time fails the request with INTERNAL_ERROR instead of reporting success.
| Parameter | In | Type | Description |
|---|---|---|---|
id | path | string | Device identifier |
On success, data is { ok: true }.
DELETE /api/v1/remote-control/devices/{id}
Removes a device from the cloud registry: it disappears from the device list and its tunnel, if still connected, is disconnected. Deactivating keeps an offline device listed; deleting removes it entirely. When the target is this machine, the local tunnel is stopped first and the machine lock is held while deleting, so the device cannot re-register behind a successful response.
| Parameter | In | Type | Description |
|---|---|---|---|
id | path | string | Device identifier |
On success, data is { ok: true }.
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 |
POST /api/v1/client_configs | Fetch a named client configuration controlled by the Kimi Code service |
GET /api/v1/config
Returns the resolved global configuration — the effective result of config.toml plus overlays. Secrets are redacted: each provider reports only has_api_key, never the stored key.
On success, data is the config object; its fields mirror the top-level domains documented under Top-level fields:
| Field | Type | Description |
|---|---|---|
providers | object | Map of provider id → { type, base_url?, default_model?, has_api_key } |
default_provider | string | Global default provider id |
default_model | string | Global default model alias |
models | object | Map of model alias → model record |
thinking | object | Default parameters for Thinking mode |
plan_mode | boolean | Plan mode flag |
yolo | boolean | Derived: true when default_permission_mode is yolo |
default_permission_mode | string | Default permission mode for new sessions |
default_plan_mode | boolean | Whether new sessions start in Plan mode |
permission | object | Initial permission rules |
hooks | array | Lifecycle hooks |
services | object | Built-in external service configuration |
merge_all_available_skills | boolean | Whether to merge Agent Skills from all available directories |
extra_skill_dirs | array | Extra skill search directories |
loop_control | object | Agent loop control parameters |
background | object | Background task runtime parameters |
subagent | object | Subagent configuration |
secondary_model | object | Secondary model pool for subagents |
experimental | object | Experimental flag id → enabled |
telemetry | boolean | Whether anonymous telemetry is enabled |
auto_session_title | boolean | Whether clients may automatically generate session titles |
disable_feedback_survey | boolean | Whether the session rating prompt is turned off |
raw | object | Raw parsed config.toml content, unmodeled fields included |
POST /api/v1/config
Merge-patches the global configuration: each top-level domain in the body is deep-merged into that domain, and domains absent from the body are left untouched. Setting yolo to true is shorthand for default_permission_mode: "yolo"; a rejected patch (invalid value or persistence failure) returns 40001 with the underlying message.
Every config change — a successful update through this endpoint, an external edit of config.toml, or a server-side write such as an OAuth login refresh — is broadcast as the global event.config.changed event. Changes inside a short window are merged into one event carrying the affected domain names in changedFields (camelCase config domains, for example defaultModel) and the full current config projection in config (same shape as the GET /api/v1/config response).
The body is a partial config object — any subset of the response domains above except raw, all optional:
| Parameter | In | Type | Description |
|---|---|---|---|
providers | body | object | Map of provider id → provider table |
default_provider | body | string | Global default provider id |
default_model | body | string | Global default model alias |
models | body | object | Map of model alias → model record |
thinking | body | object | Default parameters for Thinking mode |
plan_mode | body | boolean | Plan mode flag |
yolo | body | boolean | true maps to default_permission_mode: "yolo"; false is ignored |
default_permission_mode | body | string | manual / yolo / auto |
default_plan_mode | body | boolean | Whether new sessions start in Plan mode |
permission | body | object | Initial permission rules |
hooks | body | array | Lifecycle hooks |
services | body | object | Built-in external service configuration |
merge_all_available_skills | body | boolean | Whether to merge Agent Skills from all available directories |
extra_skill_dirs | body | array | Extra skill search directories |
loop_control | body | object | Agent loop control parameters |
background | body | object | Background task runtime parameters |
subagent | body | object | Subagent configuration |
secondary_model | body | object | Secondary model pool for subagents |
experimental | body | object | Experimental flag id → enabled |
telemetry | body | boolean | Whether anonymous telemetry is enabled |
auto_session_title | body | boolean | Whether clients may automatically generate session titles |
disable_feedback_survey | body | boolean | Whether the session rating prompt is turned off |
On success, data is the full updated config in the same shape as GET /api/v1/config.
POST /api/v1/client_configs
Fetches a named client configuration that the Kimi Code service controls remotely — which model or thinking-effort switches ask the user to confirm, and the client upgrade policy. The server fetches it from the Kimi Code service of the account's region, or from KIMI_CODE_BASE_URL when that is set, and normalizes it to the shape that name expects.
| Parameter | In | Type | Description |
|---|---|---|---|
name | body | string | Required. model_switch_confirm, effort_switch_confirm, or client_upgrade_policy |
On success, data is { name, config }. config is null for any other name or when the fetch fails, so the client falls back to its built-in behavior. A fetched value never turns into null: fields that are missing or malformed take their off defaults, so a malformed model_switch_confirm, for example, comes back as { enable: false, rules: [] }.
Models and providers
These endpoints manage the two halves of model configuration — the providers table and the model-alias table of config.toml — plus a server-proxied models.dev directory for one-shot imports. A model alias id is the exact configured alias key: aliases created through the provider-management endpoints take the form provider_id/model (for example my-provider/kimi-for-coding), while a bare model-table key such as turbo is used as-is; anywhere the API takes a model_id, including the global default_model, it means this alias id. An unsupported action on a :{action} route returns 40001.
| 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 |
GET /api/v1/models
Lists every configured model alias across all providers.
On success, data.items is an array of { provider, model, display_name?, max_context_size, capabilities?, support_efforts?, default_effort?, upgrade? }: model is the alias id (provider_id/model for provider-managed aliases, otherwise the bare key), provider the owning provider id, max_context_size the context window in tokens, and capabilities / support_efforts / default_effort describe capability flags and Thinking-mode effort support. For custom-registry models, upgrade may contain { model, migration_markdown, retirement_at? } to guide users toward a replacement model; placeholders in migration_markdown are already replaced with the model ids.
POST /api/v1/models/{model_id}:set_default
Sets the global default_model to an existing alias. model_id is the exact configured alias key — for a bare key like turbo the call is POST /api/v1/models/turbo:set_default; URL-encode the id when it contains /, as in POST /api/v1/models/my-provider%2Fkimi-for-coding:set_default.
| Parameter | In | Type | Description |
|---|---|---|---|
model_id | path | string | Required. The exact configured model alias key; URL-encode it when it contains / |
On success, data is { default_model, model } — the alias now in effect and its catalog item (same shape as a GET /api/v1/models item).
40001: malformed or unsupported action suffix in the path40413: no model alias with that id
GET /api/v1/providers
Lists every configured provider with its credential and model-discovery state, without revealing any key. This is the provider item shape referenced by the other provider endpoints.
On success, data.items is an array of:
| Field | Type | Description |
|---|---|---|
id | string | Provider id |
type | string | Wire protocol: kimi / openai / openai_responses / anthropic / google-genai / vertexai |
base_url | string | API base URL, when set |
default_model | string | The provider's default model alias, when set |
has_api_key | boolean | Whether a credential is stored |
status | string | connected when an API key or cached OAuth token exists, unconfigured otherwise (error is reserved in the schema) |
models | array | The provider's model alias ids |
POST /api/v1/providers
Creates a provider and its model aliases in one save; the reply is HTTP 201 with the standard envelope. When no global default_model is configured at all (fresh setup), it is seeded with the new provider's default_model (or first model); an existing default is never modified.
| Parameter | In | Type | Description |
|---|---|---|---|
id | body | string | Required. Provider id — letters, digits, -, _, and spaces; must start with a letter or digit |
type | body | string | Required. Wire protocol: kimi / openai / openai_responses / anthropic / google-genai / vertexai |
api_key | body | string | API key, stored in config.toml |
base_url | body | string | API base URL; must not contain an environment variable placeholder (${...}) |
default_model | body | string | The provider's default model; must be one of models[].model |
models | body | array | Required. At least one entry, no duplicate model values; entry shape below |
Each models[] entry declares one alias whose id becomes id/model:
| Field | Type | Description |
|---|---|---|
model | string | Required. Upstream model name |
max_context_size | integer | Required. Context window in tokens, ≥ 1 |
display_name | string | Display name |
capabilities | array | Capability flags such as thinking or image_in |
max_output_size | integer | Max output tokens, ≥ 1 |
support_efforts | array | Supported Thinking-mode effort levels |
adaptive_thinking | boolean | Adaptive thinking toggle |
On success, data is the created provider item (same shape as a GET /api/v1/providers item).
40921: a provider with thisidalready exists
GET /api/v1/providers/{provider_id}
Reads one provider. Unlike the list route, the response reveals the stored api_key when one is set, so a local edit form can prefill — keep this in mind when exposing the port.
| Parameter | In | Type | Description |
|---|---|---|---|
provider_id | path | string | Required. Provider id |
On success, data is the provider item plus api_key when a key is stored.
40412: provider not found
PUT /api/v1/providers/{provider_id}
Replaces a provider in one save: type, base_url, and the model list are rewritten, and the provider's aliases are rebuilt from models — aliases no longer listed disappear from config.toml, while other providers' aliases are untouched. api_key is tri-state: omitted keeps the stored key, "" clears it, any other value replaces it. Beyond the new_id rename migration, the global default pointers are never modified.
| Parameter | In | Type | Description |
|---|---|---|---|
provider_id | path | string | Required. Current provider id |
new_id | body | string | Rename the provider; the providers key, model aliases, default_provider, a default_model pointing at an old alias, and the subagent secondary-model pool all migrate. Same id rules as POST /api/v1/providers |
type | body | string | Required. Wire protocol: kimi / openai / openai_responses / anthropic / google-genai / vertexai |
api_key | body | string | Tri-state, see above |
base_url | body | string | API base URL; must not contain an environment variable placeholder (${...}) |
default_model | body | string | The provider's default model; must be one of models[].model |
models | body | array | Required. At least one entry, no duplicate model values; same entry shape as POST /api/v1/providers |
On success, data is { provider } with the saved provider item.
40001: a renamed alias id would collide with another provider's alias40003: provider is OAuth-managed — log out viaPOST /api/v1/oauth/logoutinstead40412: provider not found40921:new_idis already taken
DELETE /api/v1/providers/{provider_id}
Deletes a provider and all of its model aliases; the subagent secondary-model pool is cascaded. The global default_provider / default_model pointers are left untouched, even when they point at the deleted provider — they are the user's settings, not this endpoint's to garbage-collect.
| Parameter | In | Type | Description |
|---|---|---|---|
provider_id | path | string | Required. Provider id |
On success the server answers 204 with no body — the status line itself reports the delete (see Response envelope).
40003: provider is OAuth-managed — log out viaPOST /api/v1/oauth/logoutinstead40412: provider not found
POST /api/v1/providers/{provider_id}:refresh
Re-discovers one provider's model metadata from its upstream source and rewrites the provider's aliases. Providers with a static model source are reported unchanged without any network call. When at least one provider's aliases or managed model metadata changes, the server broadcasts the global event.model_catalog.changed event.
| Parameter | In | Type | Description |
|---|---|---|---|
provider_id | path | string | Required. Provider id |
On success, data is a refresh report: changed is an array of { provider_id, provider_name, added, removed } (added/removed alias counts), unchanged is an array of provider ids with no diff, and failed is an array of { provider, reason }. When a refresh finds that the registry dropped the current default model and that model had declared an upgrade replacement, data also carries an optional retired_default: { from, to, from_display_name } — the default model has been redirected to to, open sessions still bound to from have been moved to to as well, and from_display_name is the dropped model's display name for notices. The same field appears on the event.model_catalog.changed event.
40001: malformed or unsupported action suffix in the path40412: provider not found
POST /api/v1/providers:refresh
Refreshes model metadata for every provider. The body is optional and ignored.
On success, data is the same refresh report as POST /api/v1/providers/{provider_id}:refresh (changed / unchanged / failed).
POST /api/v1/providers:refresh_oauth
Same refresh as POST /api/v1/providers:refresh, limited to OAuth-backed providers. The body is optional and ignored.
On success, data is the refresh report (changed / unchanged / failed).
POST /api/v1/providers:import_catalog
Imports one models.dev directory entry as a configured provider; the reply is HTTP 201 with the standard envelope. The wire protocol and endpoint come from the catalog resolution, and every catalogued model is written as an alias. Importing an id that already exists is a refresh — the provider entry and its aliases are rewritten from the catalog, and an omitted api_key keeps the stored key. The global default pointers are never modified, except that default_model is seeded from the first imported model when none is configured at all.
| Parameter | In | Type | Description |
|---|---|---|---|
catalog_id | body | string | Required. Directory entry id from GET /api/v1/catalog/providers |
id | body | string | Override the catalog id as the local provider id. Same id rules as POST /api/v1/providers |
api_key | body | string | API key for the imported provider |
base_url | body | string | Override the catalog-resolved endpoint; required when the entry's needs_base_url is true |
On success, data is { provider, models_imported } — the provider item and the number of aliases written.
40001:catalog_idmissing or another body validation failure40003: the target provider exists and is OAuth-managed40004: the entry cannot be imported (rejected, requires abase_url, has no importable models, or its id is unusable as a provider id)40417: no directory entry with thatcatalog_id50004: the models.dev directory is unavailable
POST /api/v1/providers:import_registry
Imports a models.dev-shaped private registry — an api.json URL plus an optional Bearer key — as configured providers; the reply is HTTP 201 with the standard envelope. Every listed provider is written with a source record so scheduled refreshes rediscover it. Re-importing the same URL removes providers that disappeared upstream — the URL is the registry's stable identity, so rotating the key is safe. The global default pointers follow the same rules as :import_catalog.
| Parameter | In | Type | Description |
|---|---|---|---|
url | body | string | Required. URL of the registry's api.json |
api_key | body | string | Bearer key for the registry; when omitted, the key from the previous import of the same URL is reused |
On success, data is { providers, models_imported } — an array of provider items and the total number of aliases written.
40001:urlmissing or another body validation failure40003: a listed provider exists and is OAuth-managed40005: the registry cannot be fetched or parsed, or lists no importable providers
GET /api/v1/catalog/providers
Browses the models.dev directory, proxied by the server with a 10-minute in-memory cache and a built-in snapshot fallback. Items keep the upstream directory order. Entries the server cannot import carry rejected: true with a machine-readable reject_reason; entries with needs_base_url: true require a base URL at import time.
On success, data.items is an array of { id, name, wire_type, base_url, guessed, needs_base_url, rejected, reject_reason, env_key, models }: wire_type is the resolved protocol (nullable, same enum as a provider type), base_url is the resolved endpoint (nullable; null for entries that need a base URL or were rejected), guessed marks a heuristic resolution, env_key is the upstream's conventional API-key environment variable (nullable), and models is an array of { id, name?, max_context_size, capabilities?, reasoning }.
50004: the directory is unavailable (both the live fetch and the built-in snapshot failed)
GET /api/v1/catalog/providers/{catalog_id}
Reads one models.dev directory entry by catalog id — the same item shape as GET /api/v1/catalog/providers.
| Parameter | In | Type | Description |
|---|---|---|---|
catalog_id | path | string | Required. Directory entry id |
On success, data is the directory entry (same shape as a GET /api/v1/catalog/providers item).
40417: no directory entry with thatcatalog_id50004: the directory is unavailable
Sessions
These endpoints create, list, and inspect sessions, drive session-level actions (fork, compact, undo, and friends), and read per-session rollups. Most of them return a session in the wire shape documented once under The session object; non-CRUD operations use the :{action} convention described above.
| 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}/title/generate | Generate a title via the managed chat_title tool |
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}/goal | Current goal snapshot (null when none) |
PATCH /api/v1/sessions/{session_id}/goal | Edit the current goal's objective in place |
GET /api/v1/sessions/{session_id}/warnings | Session-level warnings |
GET /api/v1/sessions/{session_id}/environment | Read the main agent's environment binding |
GET /api/v1/sessions/{session_id}/environments | List the environments registered for the session workspace |
POST /api/v1/sessions/{session_id}/export | Export the session with diagnostics (zip stream, not enveloped) |
GET /api/v1/sessions/{session_id}/media/{file_id} | Download prompt media by file id (binary) |
GET /api/v1/sessions/{session_id}/file-history/changes | List the files one turn changed |
GET /api/v1/sessions/{session_id}/file-history/content | Read a file as it was at the start or end of a turn |
POST /api/v1/sessions/{session_id}/rating | Ask whether to show the satisfaction survey, and report how it went |
The session object
Every endpoint that returns a session uses this wire shape. The live facts (busy, main_turn_active, pending_interaction, last_turn_reason) are resolved from the session's activity aggregate: a session that is not loaded in this server process (a cold session) always reports not-busy with no pending interaction. A few fields are placeholders in the current projection — this is noted per field.
| Field | Type | Description |
|---|---|---|
id | string | Session id (session_...) |
workspace_id | string | Owning workspace id |
title | string | Session title; "" when untitled |
created_at / updated_at | string | Creation and last-update times, ISO 8601 |
archived | boolean | Whether the session is archived (hidden from the default session list) |
archived_at | string | Archive time, ISO 8601; present only when archived |
busy | boolean | Any agent has an active turn or background task |
main_turn_active | boolean | The main agent has an active turn |
pending_interaction | string | none / approval / question — an unanswered interaction is waiting |
last_turn_reason | string | Main agent's latest turn outcome: completed / cancelled / failed |
last_prompt | string | Most recent user prompt text, when present |
metadata | object | Custom metadata; always carries cwd (the session's working directory) |
agent_config | object | Projected as { model }; model is the main agent's current model when the session is loaded in this server process and its main agent has one — as in the creation response when the request set a model — and "" otherwise |
usage | object | Token rollup { input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, context_tokens, context_limit?, total_cost_usd?, turn_count? }; currently always zeros. The live context size comes with each agent's usage frame over the WebSocket (see Events) |
permission_rules | array | Session permission rules; currently always [] |
message_count | integer | Message count; currently always 0 |
POST /api/v1/sessions
Creates a session and returns it. The target directory comes from workspace_id (an already-registered workspace) or from metadata.cwd (the workspace is registered on first use); passing both requires them to agree. Creation broadcasts the global event.session.created event.
| Parameter | In | Type | Description |
|---|---|---|---|
workspace_id | body | string | Required when metadata.cwd is absent. Registered workspace id; the session is created at that workspace's root |
metadata | body | object | Custom metadata. metadata.cwd is the working directory and is required when workspace_id is absent; with both given, it must equal the workspace root. Other keys are kept as the session's custom metadata, except the reserved keys goal, parent_session_id, and child_session_kind, which are dropped |
title | body | string | Initial title (at least 1 character); the session is untitled otherwise |
agent_config | body | object | Partial main-agent config, with the same fields as POST /api/v1/sessions/{session_id}/profile plus profile (the agent profile, defaulting to agent; cannot be changed later), applied before the session is returned. When it cannot be applied, the session is not created and the error is returned. tower_mode: true is rejected (40001): entering Tower mode changes the repository and cannot be undone if creation fails, so enable it through POST /api/v1/sessions/{session_id}/profile once the session exists |
On success, data is the session object of the new session.
40001: neitherworkspace_idnormetadata.cwdgiven,metadata.cwddoes not match the workspace root, oragent_configsetstower_mode: true(detailslists the field); also whenagent_config.modelis not configured,agent_config.profileis unknown, or the model does not supportagent_config.thinking40113: no model is available whileagent_configsetsprofileorthinking40409: the working directory does not exist or is not a directory40410: no registered workspace with thatworkspace_id40913–40918,40920:agent_configcould not be applied because of a goal error
GET /api/v1/sessions
Lists sessions across workspaces, newest updated_at first. Cursor pagination follows Pagination, with one twist: without page_size (and without archived_only) the response is a single unpaginated window whose has_more is always false, so pass page_size to actually page.
| Parameter | In | Type | Description |
|---|---|---|---|
before_id | query | string | Only sessions older than this id; mutually exclusive with after_id |
after_id | query | string | Only sessions newer than this id; mutually exclusive with before_id |
page_size | query | integer | 1–100. When paging applies, the default is 20; see the note above for the unpaginated default behavior |
busy | query | boolean | Keep only busy (or only idle) sessions |
include_archive | query | boolean | Include archived sessions alongside live ones. Default false |
archived_only | query | boolean | Keep only archived sessions; mutually exclusive with include_archive; implies cursor paging even without page_size |
exclude_empty | query | boolean | Drop sessions that carry no user prompt |
workspace_id | query | string | Restrict to one workspace (aliases are resolved) |
On success, data is { items, has_more } where each item is the session object.
40001: validation failure — for examplebefore_idcombined withafter_id, orarchived_onlycombined withinclude_archive40410: unknownworkspace_id
GET /api/v1/sessions/{session_id}
Reads one session from the index. Live facts are included when the session is loaded in this process; a cold session reports not-busy with its last persisted turn outcome.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
On success, data is the session object.
40401: session not found, or its workspace can no longer be resolved
GET /api/v1/sessions/{session_id}/profile
Reads the session profile — the same wire payload as GET /api/v1/sessions/{session_id}.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
On success, data is the session object.
40401: session not found
POST /api/v1/sessions/{session_id}/profile
Updates the session's profile: title, custom metadata, and the main agent's config. A title set here becomes a custom title, which wins over generated titles; setting one broadcasts the global session.meta.updated event.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
title | body | string | New title (at least 1 character); becomes a custom title |
metadata | body | object | Keys merged into the session's custom metadata |
agent_config | body | object | Partial main-agent config; fields below, all optional |
Each agent_config field is applied immediately to the main agent:
| Field | Type | Description |
|---|---|---|
model | string | Model alias id; an empty string is ignored |
thinking | string | Thinking-mode effort level |
permission_mode | string | manual / yolo / auto |
plan_mode | boolean | Enter or exit Plan mode |
swarm_mode | boolean | Enter or exit swarm mode |
goal_objective | string | Create a goal with this objective |
goal_control | string | pause / resume / cancel the current goal |
On success, data is the updated session object.
40401: session not found
POST /api/v1/sessions/{session_id}/title/generate
Generates a title from the session's prompts through the managed provider's chat_title tool and applies it, broadcasting session.meta.updated. Generation requires the managed OAuth login; without force, a session that already has a custom or generated title is reported unavailable instead of being overwritten.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
force | body | boolean | Regenerate even when a custom or generated title exists. Default false |
source | body | string | Title input: user_prompts (default) / first_turn / digest |
On success, data is { title } — the title now applied to the session.
40401: session not found40923: generation unavailable — the flag is off, there is no managed OAuth login or no prompt content yet, an existing title withoutforce, or the backend request failed
POST /api/v1/sessions/{session_id}:{action}
Session actions are dispatched through one route: the path tail is parsed as {session_id}:{action}, the body is validated against the action's schema, and a missing or unknown action fails 40001 (unsupported action: ...). Every action resolves the session first, so all of them can return 40401 for an unknown session. The supported actions are documented one by one below.
POST /api/v1/sessions/{session_id}:fork
Copies the session — its conversation, agent state, and files — into a new session in the same workspace, and broadcasts event.session.created. The source keeps running. If a turn is active, the fork retains the last completed turn before it and the active turn's user prompt as a prompt-only cancelled turn; active assistant output, tool activity, queued input, and later events are excluded.
| Parameter | In | Type | Description |
|---|---|---|---|
title | body | string | Title for the fork (at least 1 character). Default Fork: <source title> |
metadata | body | object | Custom metadata for the fork |
turn_index | body | integer | Persistent engine turnId to fork through, including that turn. Gaps are allowed. Must be a non-negative safe integer and already complete |
On success, data is the session object of the new session.
40001:turn_indexis invalid, does not exist, or selects the active turn40901: another session history operation is in progress
POST /api/v1/sessions/{session_id}:compact
Starts a manual full compaction of the main agent's context. The call returns immediately; progress and completion are delivered as the compaction.* WebSocket events.
| Parameter | In | Type | Description |
|---|---|---|---|
instruction | body | string | Extra guidance for the compaction summary; a blank value is ignored |
On success, data is an empty object.
40910: a turn or another context change is active, or the history has nothing to compact
POST /api/v1/sessions/{session_id}:undo
Undoes a turn and every turn after it in an agent's conversation, including the system turns and triggered turns in between, and reconciles the derived session state (including the session's last_prompt). Files are not rolled back.
| Parameter | In | Type | Description |
|---|---|---|---|
turn_id | body | string | The turn to undo. It must be an agent turn whose opening user_message comes from the user, a user-invoked slash skill, or a plugin command |
agent_id | body | string | The agent whose conversation is undone. Default main |
On success, data is { removed_turn_ids }: the removed turns in ordinal order, starting with turn_id. Subscribers receive a turns.removed frame with the same ids; the response and the frame may arrive in either order.
40421: the turn does not exist40901: the agent is working (a running turn, queued input, or a running compaction) — wait for it to finish, then retry40911: the turn cannot be undone: it does not open with user input, or the removed range crosses a context compaction
POST /api/v1/sessions/{session_id}:abort
Cancels the main agent's running turn — the programmatic equivalent of the user aborting the turn in the TUI.
On success, data is { aborted: true }.
POST /api/v1/sessions/{session_id}:btw
Starts a "by the way" side conversation: forks the main agent into a child agent whose tool calls are limited to the read-only tools Read, Grep, and Glob, so quick side questions run in isolation without touching the working context. Requires a usable model configuration.
On success, data is { agent_id } — the id of the new child agent.
POST /api/v1/sessions/{session_id}:archive
Marks the session archived: it disappears from the default session list (it stays listed with include_archive or archived_only), and the server broadcasts the global event.session.archived event.
On success, data is { archived: true }.
POST /api/v1/sessions/{session_id}:restore
Un-archives the session and resumes it. When the session was archived, the server broadcasts the global event.session.restored event.
On success, data is the session object with archived: false.
POST /api/v1/sessions/{session_id}:reload
Reloads the session with the latest on-disk configuration: re-reads config.toml and plugins, reloads the workspace skill and subagent catalogs, then closes and resumes the session from disk. The main agent is also notified of external changes to AGENTS.md files, skills, and subagent profiles or models via system reminders.
On success, data is the session object.
40901: the session has an active turn and cannot be reloaded
POST /api/v1/sessions/{session_id}:takeover
Removes the lock another Kimi Code process holds on the session, so a request that failed with 40931 can be retried. Use it only when that process is gone and left its lock behind: if it is still running, both processes write to the session until it goes idle, and the session history can interleave.
On success, data is { taken_over: true }.
40401: session not found
GET /api/v1/sessions/{session_id}/children
Lists the session's children — the sessions created through POST /api/v1/sessions/{session_id}/children. Cursor pagination follows Pagination.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
before_id | query | string | Only children older than this id; mutually exclusive with after_id |
after_id | query | string | Only children newer than this id; mutually exclusive with before_id |
page_size | query | integer | 1–100. Default 100 |
busy | query | boolean | Keep only busy (or only idle) children |
On success, data is { items, has_more } where each item is the session object.
40401: session not found
POST /api/v1/sessions/{session_id}/children
Creates a child session: a fork of this session recorded as its child, so it shows up under GET /api/v1/sessions/{session_id}/children. Running sources use the same completed-turn boundary and prompt-only cancelled active turn as :fork; child creation does not accept a historical turn_index.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
title | body | string | Title for the child (at least 1 character). Default Child: <source title> |
metadata | body | object | Custom metadata for the child |
On success, data is the session object of the new session, and the server broadcasts event.session.created.
40901: another session history operation is in progress
GET /api/v1/sessions/{session_id}/goal
Reads the session's current goal snapshot, or null when no goal is active. Note that this payload uses camelCase keys, unlike most of this API.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
On success, data is null or { goalId, objective, completionCriterion?, status, turnsUsed, tokensUsed, wallClockMs, budget, terminalReason? }, where status is active / paused / blocked / complete and budget reports the token, turn, and wall-clock budgets together with the remaining amounts and per-budget reached flags (each nullable when no such budget is set).
40401: session not found
PATCH /api/v1/sessions/{session_id}/goal
Replaces the objective of the current goal without changing its goalId, status, progress, or budget. Works on active, paused, and blocked goals. A running turn is not interrupted; the agent receives the new objective on its next turn. Connected clients are notified through the goal.updated event.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
objective | body | string | Required. New objective; surrounding whitespace is trimmed |
On success, data is the updated goal snapshot, in the same camelCase shape as GET /api/v1/sessions/{session_id}/goal.
40401: session not found40914: no current goal40915: the goal is complete40917: the objective is empty40918: the objective exceeds 4000 characters
GET /api/v1/sessions/{session_id}/warnings
Reads session-level warnings. The current producer is the oversized AGENTS.md check (agents-md-oversized), so the list is empty for most sessions.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
On success, data is { warnings }, each entry { code, message, severity } with severity one of info / warning / error.
40401: session not found
GET /api/v1/sessions/{session_id}/environment
Reads the main agent's environment binding — which environment the session's agent loop runs on.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
On success, data is { workspace_id, environment_id, cwd? }; cwd is the working directory on the bound environment, present when the binding carries one.
40401: session not found
GET /api/v1/sessions/{session_id}/environments
Lists the environments available to the session.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
On success, data is { environments }. Each environments entry is { environment_id, type, status, connect_error? } with type one of local / ssh / docker / command and status one of pending / connecting / ready / disconnected / disposed; pending means no live connection and no observed failure (never connected), and connect_error carries the recorded failure reason for a disconnected entry.
40401: session not found
POST /api/v1/sessions/{session_id}/export
Exports the session together with diagnostic logs as a zip attachment (kimi-session-<id>.zip). The response is a binary stream, not a JSON envelope — capabilities and failure semantics are covered under Binary and streaming endpoints.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
web_log | body | string | Client log text to include in the archive, at most 256 KB UTF-8 |
desktop | body | boolean | Also include the desktop host's log. Default false |
GET /api/v1/sessions/{session_id}/media/{file_id}
Downloads a prompt media file (an image or other attachment referenced by the session's prompts) by file id; an id not yet committed to the session falls back to the staged uploads. The response is binary with Range support (206 on ranged requests) — see Binary and streaming endpoints for the shared conventions; unlike the enveloped endpoints there, a missing session or file answers with a real 404 status carrying an envelope body.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
file_id | path | string | Required. Media file id |
GET /api/v1/sessions/{session_id}/file-history/changes
Lists the files one turn of the main agent changed, from the turn-level file history: the contents captured when the turn started are compared with those captured when it ended, or with the files on disk while the turn is still running. Unchanged files are left out.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
turn_number | query | integer | Required. The turn's turn_number (see Conversation protocol) |
On success, data is { changes, recorded }. Each change is { path, status, additions, deletions, binary?, oversize? }: status is added / modified / deleted, and additions / deletions count changed lines. binary marks a file whose content is not text or is no longer stored, and oversize a file too large to capture; both report 0 lines. recorded is false when the turn's file history is incomplete — it was never recorded, or some captured contents have since been cleaned up — so changes may be empty or partial.
40001:turn_numbermissing or invalid40401: session not found
GET /api/v1/sessions/{session_id}/file-history/content
Reads a file as the main agent's file history captured it at the start or end of one turn, for example to show the turn's diff.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
turn_number | query | integer | Required. The turn's turn_number |
path | query | string | Required. File path, as listed by file-history/changes |
phase | query | string | start (default) for the content before the turn, end for the content after it |
On success, data is { content }. content is { version, content?, binary? }, where version is the file's version in the history: the text comes in content, binary is true for a file that is not text, and neither is present when the file did not exist at that point. With phase=end, a file the turn did not touch reads as it was at the start. content is null when the turn has no capture for that phase, or the file was not captured, was too large, or its stored content is gone.
40001:turn_numberorpathmissing or invalid40401: session not found
POST /api/v1/sessions/{session_id}/rating
Drives the satisfaction survey a client may show after a turn: the client asks whether to show it once a turn completes, then reports when it shows the survey and how the user answered. Whether a survey is offered follows sampling rules set by the Kimi Code service plus local cooldowns, so most evaluations offer none.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
op | body | string | Required. evaluate / shown / respond / abandon |
turn_id | body | string | Required with evaluate: the turn_id of the turn that just completed |
appearance_id | body | string | Required with shown / respond / abandon: the id returned by evaluate |
response | body | string | Required with respond: bad / fine / good / dismissed |
With evaluate, data is { appearance }: { id, survey } when the client should show a survey now, with survey either session or long_context, and null otherwise — including when the turn is not the main agent's latest completed turn, when disable_feedback_survey is on, or when telemetry is off. Report shown once the survey is on screen, respond with the user's answer (dismissed when they close it), or abandon when it goes away unanswered, for example because the user moved on; these return data: null.
40001: validation failure
Messages and turns
The messages endpoints page the main agent's flattened message history, while the turns endpoint pages one agent's conversation as turns and items — the same model the WebSocket Conversation protocol streams live. Use the endpoints for older history and the WebSocket subscription for everything after its turns frame.
| 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}/agents/{agent_id}/turns | Page an agent's turns (before_ordinal / page_size) |
GET /api/v1/sessions/{session_id}/messages
Pages the main agent's message history — the flattened context transcript — newest first. Cursor pagination follows Pagination; reading the history resumes the session when it is cold.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
before_id | query | string | Only messages older than this message id; mutually exclusive with after_id |
after_id | query | string | Only messages newer than this message id; mutually exclusive with before_id |
page_size | query | integer | 1–100. Default 50 |
role | query | string | Keep only one role: user / assistant / tool / system. The filter applies after the page is sliced, so a filtered page can hold fewer than page_size items while has_more is still true — keep paging until has_more is false |
On success, data is { items, has_more } where each item is a message object { id, session_id, role, content, created_at, prompt_id?, parent_message_id?, metadata? }; content is an array of content parts in the wire format documented under Prompts (text, tool_use, tool_result, image, video, file, thinking).
40001: validation failure — for examplebefore_idcombined withafter_id40401: session not found
GET /api/v1/sessions/{session_id}/messages/{message_id}
Reads one message from the same history by id.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
message_id | path | string | Required. Message id |
On success, data is the message object in the item shape documented under GET /api/v1/sessions/{session_id}/messages above.
40401: session not found40403: no message with that id in this session
GET /api/v1/sessions/{session_id}/agents/{agent_id}/turns
Returns one page of an agent's turns, oldest first, in the turn and item shape described under Conversation protocol. Reading loads the session, and a finished subagent, when they are not live.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
agent_id | path | string | Required. Agent id; a plain id without path separators |
before_ordinal | query | integer | Only turns whose ordinal is below this value; no turn needs to have that ordinal, for example after an undo. Default returns the latest page |
page_size | query | integer | 1–200 turns. Default 50 |
On success, data is { turns, has_more }; has_more reports that older turns remain, read them by passing the first turn's ordinal as before_ordinal. A page stays within 1 MiB: when page_size turns do not fit, the oldest of them are left out and has_more is true, while the newest turn is always included. The latest page holds the running turn, if any, with what it has produced so far. Tasks, the agent's status and settings, context usage, the goal, the todo list, and pending interactions are not part of the turns; they arrive as the WebSocket task, agent, usage, goal, todo, and interaction frames (see Events).
40001: validation failure40401: session or agent not found
Prompts
A prompt is one unit of user input: submitting one enqueues it on the session's main agent (or a named agent), a queued prompt can be steered into the active turn, and a running prompt can be aborted. Turn progress itself streams over the WebSocket events, not these endpoints.
| 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}/steer | Steer message content directly into the active turn |
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 |
GET /api/v1/sessions/{session_id}/prompts
Reads the main agent's prompt queue snapshot.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
On success, data is { active, queued }: active is the running prompt (null when idle) and queued lists the pending prompts in order. A prompt is { prompt_id, user_message_id, status, content, created_at } with status one of running / queued / blocked / failed and content in the content-part format accepted by POST /api/v1/sessions/{session_id}/prompts.
40401: session not found
POST /api/v1/sessions/{session_id}/prompts
Submits a user prompt to the session. Media references are validated first, then the optional overrides are applied to the target agent — model, thinking, permission_mode, swarm_mode, and disabled_tools — and the prompt is enqueued; the response returns as soon as the prompt is accepted, without waiting for the turn. With skills, the prompt runs as a bundled skill activation instead of a plain user prompt.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
content | body | array | Required. Non-empty array of content parts; variants below |
agent_id | body | string | Target agent. Default the main agent |
prompt_id | body | string | Client-chosen prompt id that makes the submission idempotent, also with skills: sending the same request again with the same id returns the result of the first submission, waiting for it while it is still being processed, and does not submit again; reusing the id for a different request fails 40927 |
skills | body | array | Bundled skill activations, at least 1 entry of { name, path?, args? }; every skill must exist and be user-activatable. path selects a same-name variant from the session catalog |
model | body | string | Model alias to switch the agent to |
thinking | body | string | Thinking-mode effort level |
permission_mode | body | string | manual / yolo / auto |
swarm_mode | body | boolean | Enter or exit swarm mode |
disabled_tools | body | array | Tool names to disable for the session |
The schema also accepts metadata (saved with the prompt), plan_mode, goal_objective, and goal_control, though the submit route does not apply the mode/goal fields. Each content part is an object discriminated by type:
| Part | Fields | Description |
|---|---|---|
text | text | Plain text |
image / video | source | Media input; source is one of { kind: "url", url, id? }, { kind: "base64", media_type, data }, { kind: "file", file_id } (an upload from POST /api/v1/files), or { kind: "session_media", file_id } (media already committed to this session) |
file | file_id, name, media_type, size | A file attachment uploaded through POST /api/v1/files |
The schema also accepts the tool_use, tool_result, and thinking parts of the shared message format, but they are not meaningful in a user prompt. Unknown or mis-kinded file_id references are rejected before the prompt is created and before any override is applied.
On success, data is the accepted prompt { prompt_id, user_message_id, status, content, created_at }. On an idle session the response waits until the prompt starts its turn or ends without one: status is blocked when a hook rejected the prompt, and failed when its submission failed before a turn started, for example because a prompt hook failed or an attachment could not be prepared.
40001: validation failure — for example an unknownmodel, an unsupportedthinkinglevel, or aprofilefield (choose the agent profile withagent_config.profilewhen creating the session)40110: no provider configured yet — finish login first40111: the resolved provider has no credential (details.provider_id)40112: the provider's credential was rejected (details.provider_id)40113: the model could not be resolved (details.model_id/details.provider_idwhen known)40401: session not found40407: a referencedfile_iddoes not exist (or does not match the part's media kind)40415: askillsentry names an unknown skill40912: the skill exists but cannot be activated by the user40927:prompt_idis already used by a different request
POST /api/v1/sessions/{session_id}/steer
Directly steers message content into the active turn in one request, without a separate submit-then-steer round trip. The body accepts the same fields as POST /api/v1/sessions/{session_id}/prompts, including content, agent_id, prompt_id, skills, profile, model, thinking, permission_mode, and disabled_tools. Media references and bundled skills use the same validation and preparation path as prompt submission.
On success, data is the accepted prompt in the same shape as POST /api/v1/sessions/{session_id}/prompts. Errors are the same as prompt submission, including validation, media, profile, environment, and idempotency failures.
POST /api/v1/sessions/{session_id}/prompts:steer
Steers queued prompts into the active turn, so the running turn consumes them immediately instead of finishing first.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
prompt_ids | body | array | Required. Non-empty array of queued prompt ids |
On success, data is { steered: true, prompt_ids }.
40001: validation failure40401: session not found40402: a listed prompt id is not in the queue
POST /api/v1/sessions/{session_id}/prompts/{prompt_id}:abort
Aborts a running prompt. This endpoint and :steer below dispatch through one route, POST /api/v1/sessions/{session_id}/prompts/{tail}: the tail is parsed as {prompt_id}:{action}, and a missing or unknown action fails 40001 (unsupported action: ...).
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
prompt_id | path | string | Required. Prompt id |
On success, data is { aborted: true }.
40401: session not found40402: no prompt with that id40903: the prompt already completed;datacarries{ aborted: false }
POST /api/v1/sessions/{session_id}/prompts/{prompt_id}:steer
Steers one queued prompt into the active turn — the single-prompt form of POST /api/v1/sessions/{session_id}/prompts:steer.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
prompt_id | path | string | Required. Queued prompt id |
On success, data is { steered: true, prompt_ids: [prompt_id] }.
40401: session not found40402: no queued prompt with that id
Approvals and questions
Approvals and questions are the session's two interaction kinds: an approval asks permission for a tool call, a question asks for structured input with labeled options. Pending ones arrive over the WebSocket as interaction frames (see Events); this endpoint answers them.
| Method and path | Description |
|---|---|
POST /api/v1/sessions/{session_id}/interactions/{interaction_id} | Answer a pending approval or question |
POST /api/v1/sessions/{session_id}/interactions/{interaction_id}
Answers a pending interaction, letting the waiting tool call go on. The body's kind must match the interaction's.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
interaction_id | path | string | Required. The interaction_id of the interaction frame |
kind | body | string | Required. approval or question |
An approval takes these fields:
| Parameter | In | Type | Description |
|---|---|---|---|
decision | body | string | Required. approved / rejected / cancelled |
scope | body | string | With approved, session (the only value) also remembers the approval rule for the rest of the session |
feedback | body | string | Free-form feedback handed back to the agent |
selected_label | body | string | The label of the chosen option, when the request offered labeled choices (for example a plan review) |
A question takes exactly one of answers and dismissed:
| Parameter | In | Type | Description |
|---|---|---|---|
answers | body | object | Map of question item id (from the frame's request.questions) to an answer object; variants below |
dismissed | body | boolean | true dismisses the question without answering it |
method | body | string | How the answer was produced: enter / space / number_key / click |
Each answer is an object discriminated by kind:
| Kind | Fields | Description |
|---|---|---|
single | option_id | One chosen option |
multi | option_ids | Several chosen options (at least 1) |
other | text | A free-text answer |
multi_with_other | option_ids, other_text | Options plus free text |
skipped | — | The item was skipped |
On success, data is { outcome, ended_at }, where outcome is approved / rejected / cancelled for an approval and answered / dismissed for a question. Subscribers receive the interaction's last interaction frame as well.
40001: validation failure (detailslists each field), or akindthat does not match the interaction40401: session not found40404: no interaction with that id in the session40902: the interaction has already ended
Skills, tools, and MCP
These endpoints expose the skill catalogs a session or workspace sees, the effective agent's tool list, and its MCP servers. Skill activation and MCP restart use the :{action} convention; activation is the REST analogue of the /<skill> slash command.
| 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 |
GET /api/v1/sessions/{session_id}/skills
Lists the skills available to one session, merged from every source (built-in, plugin, extra, user, project) with the session's precedence applied. Reading the catalog resumes the session when it is cold.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
On success, data is { skills } where each item is a skill descriptor { name, description, path, source, workspace_dir?, type?, disable_model_invocation? }: source is project / user / extra / builtin, type classifies the skill (only user-activatable types can be activated), and disable_model_invocation hides the skill from the model. workspace_dir is the project root for project skills and is omitted for other sources. Same-name skills can appear as multiple variants with distinct path values.
40401: session not found (or not activated)
GET /api/v1/workspaces/{workspace_id}/skills
Lists the skill catalog a session in this workspace would see, without creating or resuming a session — the same merge of built-in, plugin, extra, user, and project sources computed for the workspace root.
| Parameter | In | Type | Description |
|---|---|---|---|
workspace_id | path | string | Required. Registered workspace id |
On success, data is { skills } with the skill descriptor documented under GET /api/v1/sessions/{session_id}/skills above.
40410: workspace not found
POST /api/v1/sessions/{session_id}/skills/{skill_name}:activate
Activates a skill in the session — the REST analogue of the /<skill> slash command — starting a turn on the main agent with the skill's content plus args and attachments. The endpoint dispatches through one route, POST /api/v1/sessions/{session_id}/skills/{tail}: the tail is parsed as {skill_name}:{action}, activate is the only action, and a bare name or an unknown action fails 40001 (unsupported action: ...).
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
skill_name | path | string | Required. Name of the skill to activate |
path | body | string | Optional exact SKILL.md path from the session catalog, used to select a same-name variant. Must match an entry returned by the catalog |
args | body | string | Free-form arguments handed to the skill, like the text after a slash command |
prompt_id | body | string | Client-chosen id of the activation message; it shows up in the prompt_ids of the conversation's user_message item. Like a prompt submission, sending the same request again with the same id returns the first result without activating again |
attachments | body | array | Media parts attached to the activation. Image and video parts carry a source object whose kind is url / base64 / file / session_media (same shapes as the prompt content parts); file parts carry the top-level file_id, name, media_type, and size |
On success, data is { activated: true, skill_name }.
40001: validation failure or unsupported action suffix40401: session not found (or not activated)40407: a referenced attachment file does not exist40415: no skill with that name40912: the skill exists but its type cannot be activated by the user40927:prompt_idis already used by a different request
GET /api/v1/tools
Lists the tools of the effective agent — the main agent of the session given by session_id, or of the most recently created session when the parameter is omitted. When no such session is live in this server process, the list is empty.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | query | string | Session whose main agent to inspect. Default the most recently created session |
On success, data is { tools } where each item is { name, description, input_schema, source, mcp_server_id?, active? }: source is builtin / skill / mcp, mcp_server_id is set on MCP tools (parsed from the mcp__<server>__<tool> name), and active reports the tool policy's verdict. input_schema is currently always null.
GET /api/v1/mcp/servers
Lists the MCP servers configured for the effective agent (the most recently created live session's main agent, as in GET /api/v1/tools). With no live session, the list is empty.
On success, data is { servers } where each item is { id, name, transport, status, last_error?, tool_count }: transport is stdio / http / sse, status is connected / connecting / disconnected / error, and last_error carries the failure text when the server is in error.
POST /api/v1/mcp/servers/{mcp_server_id}:restart
Reconnects one MCP server of the effective agent. The endpoint dispatches through POST /api/v1/mcp/servers/{tail} with restart as the only action — a bare server id or an unknown action fails 40001.
| Parameter | In | Type | Description |
|---|---|---|---|
mcp_server_id | path | string | Required. MCP server id (its configured name) |
On success, data is { restarting: true }.
40001: missing or unknown action suffix40408: no MCP server with that id (also reported when no session is live)
Capabilities and plugins
Capabilities are built-in features with layered readiness — detection steps plus a background install; the current build registers kimi-cu (Kimi Computer Use) and kimi-webbridge (Kimi Browser Extension). Plugins are installed packages of skills, MCP servers, hooks, and commands. These endpoints report capability status and drive capability installs, and manage the plugin lifecycle from marketplace listing to removal.
| Method and path | Description |
|---|---|
GET /api/v1/capabilities | List built-in capabilities with readiness status |
GET /api/v1/capabilities/{capability_id} | Read one capability's status |
POST /api/v1/capabilities/{capability_id}:install | Start a capability install (background; poll GET for progress) |
GET /api/v1/plugins | List installed plugins |
GET /api/v1/plugins/{plugin_id} | Read an installed plugin and its declared capabilities |
POST /api/v1/plugins | Install a custom plugin or a marketplace entry at an expected version |
POST /api/v1/plugins/preview | Inspect a marketplace entry's capabilities without installing it |
GET /api/v1/plugins/marketplace | Marketplace catalog merged with live install state |
POST /api/v1/plugins/{plugin_id}:{action} | Plugin actions: enable / disable / remove |
GET /api/v1/capabilities
Lists every registered capability with its readiness status.
On success, data is { capabilities } where each item is a capability status object { id, pluginId?, displayName, description, supported, state, version?, steps, install }. state is ready (every required detection step ok) / partial (some step ok) / not_installed / unsupported (not available on this platform/architecture); steps lists the detection steps as { id, state, detail?, optional? } with state one of ok / missing / failed; install is the install progress { running, step?, percent?, error?, note? } with percent between 0 and 100.
GET /api/v1/capabilities/{capability_id}
Reads one capability's readiness status — the polling counterpart of the :install action.
| Parameter | In | Type | Description |
|---|---|---|---|
capability_id | path | string | Required. Capability id |
On success, data is the capability status object documented under GET /api/v1/capabilities above.
40418: no capability with that id
POST /api/v1/capabilities/{capability_id}:install
Starts installing a capability in the background and returns immediately with the current status (install.running is true); poll GET /api/v1/capabilities/{capability_id} for progress. The endpoint dispatches through POST /api/v1/capabilities/{tail} with install as the only action — a bare id or an unknown action fails 40001.
| Parameter | In | Type | Description |
|---|---|---|---|
capability_id | path | string | Required. Capability id |
On success, data is the capability status object documented under GET /api/v1/capabilities above.
40001: missing or unknown action suffix40418: no capability with that id40924: an install of this capability is already running40925: the capability is not supported on this platform/architecture
GET /api/v1/plugins
Lists installed plugins.
On success, data is { plugins } where each item is { id, displayName, description?, shortDescription?, longDescription?, version?, enabled, state, skillCount, mcpServerCount, enabledMcpServerCount, hookCount, commandCount, hasErrors, source, originalSource?, github? }: state is ok / error (load failures also set hasErrors), source is local-path / zip-url / github, and github carries the provenance { owner, repo, ref, installedSha? } with ref { kind: branch|tag|sha, value } for GitHub-sourced plugins. Each item also includes origin (marketplace / custom / unknown), optional catalogId, and installedAt (ISO timestamp).
POST /api/v1/plugins
Installs a plugin and returns its summary. The request body has two forms:
- Custom install:
{ "source": "<absolute local path, http(s) ZIP URL, or GitHub URL>" }. GitHub URLs may pin a ref with/tree/<branch-or-sha>,/releases/tag/<tag>, or/commit/<sha>. - Marketplace install:
{ "marketplaceId": "<catalog entry id>", "expectedVersion": "<version shown to the user>", "expectedSha256": "<sha256sum shown to the user>" }. The daemon resolvessource,sha256sum, and the expected plugin ID from the same catalog snapshot, verifies the archive before extraction, and checks the manifest ID. For ZIP entries that declaresha256sum,expectedSha256is required and must match the catalog. Custom entries without a version may omitexpectedVersion; local-directory and GitHub entries do not requireexpectedSha256.
On success, data is the plugin summary documented under GET /api/v1/plugins above.
40001: validation failure — for examplesourceis neither a URL nor an absolute path, or the plugin failed to load40409: the local path does not exist40934: the catalog entry changed after the version shown to the user; refresh the catalog and retry
GET /api/v1/plugins/marketplace
Lists the plugin marketplace catalog merged with live install state. The default catalog URL is derived from the current managed baseURL as ${baseURL}/plugins/marketplace; built-in capability rows are no longer injected. Catalog clients filters entries to desktop, cli, or both; if omitted, both hosts are allowed. Web and Remote Control use the connected server's host identity. platforms independently filters by the daemon operating system: an entry remains visible when it includes the daemon platform or contains only cloud; a cloud entry for another local platform is filtered. Client and operating-system constraints must both match. Cloud-only entries cannot be installed locally. The legacy kimi-webbridge entry is not emitted, and Computer Use exposes only the wiring plugin for the current platform.
Installed records and managed files remain shared under the daemon's home directory. Entries unavailable to the current host are also hidden from its installed list and runtime capabilities. The daemon caches the last successful availability configuration in plugins/availability.json; after a successful catalog refresh, changed client rules hide or restore existing installations without reinstalling or deleting them.
On success, data is { entries } where each item includes id, tier, classification, displayName, description?, shortDescription?, longDescription?, cliDescription?, homepage?, keywords?, version?, source, iconUrl?, sha256sum?, filename?, locale?, defaultLocale?, platforms?, distribution?, billing?, installed?, updateAvailable?, and capabilityId?. distribution.category is official or third_party; platforms uses darwin / win32 / linux / cloud. Marketplace installs use marketplaceId, expectedVersion when available, and expectedSha256 for digest-bearing ZIPs rather than passing source directly; custom installs pass source. Capability rows are marked only by the daemon: kimi-browser-extension maps to the kimi-webbridge capability, and kimi-cu / kimi-cu-win maps to kimi-cu.
defaultLocale declares the language of the top-level description and cliDescription; locale holds per-language displayName, shortDescription, longDescription, and cliDescription translations.
50001: the marketplace is unreachable or returned an invalid catalog
GET /api/v1/plugins/{plugin_id}
Reads an installed plugin's details and capabilities. skills contains the discovered Skill invocation names with optional display names and descriptions. mcpServers contains the declared MCP servers with their names, runtime names, enabled states, transports, non-secret connection settings, and optional display names, descriptions, and icon URLs. Both lists reflect the plugin's actual files and manifest. interface.skillOverrides and interface.mcpOverrides override display information only; they do not change invocation names or runtime configuration.
On success, data is the plugin summary documented under GET /api/v1/plugins, plus root, updatedAt?, manifestKind?, manifestPath?, skills, mcpServers, shadowedManifestPath?, and diagnostics. The optional locale query parameter, such as en-US or zh-CN, selects translations from interface.locales and affects capability display text only. Fields without a matching translation fall back to the default interface.
40419: no installed plugin with that id
POST /api/v1/plugins/preview
Inspects a marketplace entry without installing it. The body is { "marketplaceId": "<catalog entry id>", "expectedVersion": "<version shown to the user>", "expectedSha256": "<sha256sum shown to the user>", "locale": "<optional locale>" }. expectedSha256 is required and must match the current catalog. The daemon downloads the archive into a temporary directory, verifies its SHA-256 before extraction, validates the manifest ID, and discovers its Skills and MCP servers. It does not write an installation record, install a runtime, or retain the extracted plugin.
On success, data is { "skills": [{ "name": "<skill name>", "description": "<optional description>" }], "mcpServers": [{ "name": "<manifest name>", "runtimeName": "<runtime name>", "enabled": true, "transport": "stdio" }] }. Both entry types can also include displayName and description from overrides, and MCP entries can include iconUrl. locale follows the same selection rules as installed plugin details. MCP entries use the same shape as GET /api/v1/plugins/{plugin_id}; the example shows only the common fields.
40001: the request, archive, SHA-256, or manifest is invalid40419: no marketplace entry with that id40934: the catalog entry changed after the version shown to the user; refresh the catalog and retry
POST /api/v1/plugins/{plugin_id}:enable
Enables an installed plugin. Plugin actions dispatch through one route, POST /api/v1/plugins/{tail}: the tail is parsed as {plugin_id}:{action} with enable / disable / remove as the actions, and a bare id or an unknown action fails 40001 (unsupported action: ...).
| Parameter | In | Type | Description |
|---|---|---|---|
plugin_id | path | string | Required. Installed plugin id |
On success, data is { ok: true }.
40001: missing or unknown action suffix40419: no installed plugin with that id
POST /api/v1/plugins/{plugin_id}:disable
Disables an installed plugin without removing it; the dispatch contract matches :enable above.
| Parameter | In | Type | Description |
|---|---|---|---|
plugin_id | path | string | Required. Installed plugin id |
On success, data is { ok: true }.
40001: missing or unknown action suffix40419: no installed plugin with that id
POST /api/v1/plugins/{plugin_id}:remove
Removes an installed plugin; the dispatch contract matches :enable above.
| Parameter | In | Type | Description |
|---|---|---|---|
plugin_id | path | string | Required. Installed plugin id |
On success, data is { ok: true }.
40001: missing or unknown action suffix40419: no installed plugin with that id
Workspaces
Workspaces are the registered project directories sessions live in. These endpoints manage the registry — list, register, rename and update source folders, unregister — plus the per-workspace trust state that gates project-level MCP config. Every endpoint that returns a workspace uses the wire shape documented once under The workspace object.
| Method and path | Description |
|---|---|
GET /api/v1/workspaces | List registered workspaces |
POST /api/v1/workspaces | Register a workspace (fresh entry per call; retries deduped via idempotency_key) |
PATCH /api/v1/workspaces/{workspace_id} | Rename or replace source folders |
POST /api/v1/workspaces/{workspace_id}/primary | Promote a source folder to the primary directory |
GET /api/v1/workspaces/project-config-dirs | Read a project's local.toml additional dirs without mutating anything |
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 |
The workspace object
Every endpoint that returns a workspace uses this wire shape. Registration and update broadcast the global event.workspace.created / event.workspace.updated events.
| Field | Type | Description |
|---|---|---|
id | string | Workspace id: wd_<slug>_<hash12> for implicit registration (CLI/VS Code entering a directory), wm_<uuid> when created via POST /api/v1/workspaces |
root | string | Absolute path of the primary directory |
name | string | Display name, 1–100 characters; defaults to the primary directory's base name |
kind | string | Workspace kind: managed (explicitly created, wm_ id; sessions do not merge the project local.toml) or implicit (registered by directory, wd_ id; sessions merge the project local.toml for legacy compatibility) |
additional_dirs | string[] | Registry-managed extra source folders as absolute paths (excludes project local.toml and ephemeral sources) |
created_at | string | Registration time, ISO 8601 |
last_opened_at | string | Last time the workspace was opened or re-registered, ISO 8601 |
session_count | integer | Number of sessions in the workspace |
GET /api/v1/workspaces
Lists every registered workspace.
On success, data is { items } where each item is the workspace object.
POST /api/v1/workspaces
Registers a new workspace and returns it. Every call creates a fresh entry (wm_<uuid> id) — even when the same path is already another workspace's primary directory, entries are never folded and no warning is issued. Clients retrying a submission (network retry, double click) should send the same idempotency_key; the server dedupes on the key and returns the entry created by the first call.
| Parameter | In | Type | Description |
|---|---|---|---|
root | body | string | Required. Absolute path of an existing directory, used as the primary directory |
name | body | string | Display name, 1–100 characters. Default the primary directory's base name |
additional_dirs | body | string[] | Extra source folders. Each item resolves like add-dir (absolute, relative to the primary directory, or ~ expansion), must exist and be a directory, and must not duplicate the primary directory or another item; at most 64 items |
idempotency_key | body | string | Client-generated idempotency key, 1–200 characters. Retries with the same key return the workspace created by the first call |
On success, data is the workspace object.
40001:rootis missing or not an absolute path, or another request validation failure (detailslists the field)40006:additional_dirscontains duplicates or the primary directory itself.msgisDuplicate source folder: <actual path>;detailsis[{ "path": "additional_dirs.N", "directory": "<resolved absolute path>" }], wherepathlocates the request field anddirectoryis the duplicate directory40409:rootor a path inadditional_dirsdoes not exist or is not a directory
PATCH /api/v1/workspaces/{workspace_id}
Updates a workspace's display name and/or source folders. additional_dirs uses whole-list replace semantics — submit the full edited list; omitting the field keeps the current list; the primary directory never changes. At least one of name or additional_dirs is required.
| Parameter | In | Type | Description |
|---|---|---|---|
workspace_id | path | string | Required. Workspace id |
name | body | string | New display name, 1–100 characters |
additional_dirs | body | string[] | Full replacement list of extra source folders, resolved and validated like POST /api/v1/workspaces |
On success, data is the workspace object.
40001: both fields are missing, or another request validation failure (detailslists each field)40006: duplicate entries or an entry equal to the primary directory;msg,details.path, anddetails.directoryhave the same meaning as forPOST /api/v1/workspaces40409: a path inadditional_dirsdoes not exist or is not a directory40410: workspace not found
POST /api/v1/workspaces/{workspace_id}/primary
Promotes a source folder in the workspace's dir set to the primary directory. The swap is atomic: the path becomes the new root and the previous primary moves into additional_dirs; the workspace id and session bucket are unchanged, and all sessions are kept. After the swap, additional directories coming from the previous primary's local.toml are no longer merged (local.toml follows the primary directory).
For existing sessions on the local environment without an explicit environmentCwd, subsequent default tool working directories and relative paths follow the new primary. Explicit environmentCwd and remote environment bindings keep their configured directory. Additional directories expand allowed access but are not candidate roots for relative paths; use absolute paths or an explicit cwd to access them. Tools remain registered during the swap, and the workspace's existing trust decision is preserved across restarts.
| Parameter | In | Type | Description |
|---|---|---|---|
workspace_id | path | string | Required. Workspace id |
path | body | string | Required. Directory to promote, resolved like add-dir; must be in the current dir set (root ∪ additional_dirs) |
On success, data is the workspace object.
40001:pathis not in the dir set40409:pathdoes not exist or is not a directory40410: workspace not found
GET /api/v1/workspaces/project-config-dirs
Reads workspace.additional_dir from a project's local.toml, read-only: entries are resolved to absolute paths, deduplicated, and the root itself is dropped. Neither the registry nor local.toml is modified; a missing local.toml yields an empty array, while malformed TOML or a configured directory that no longer exists causes the request to fail instead of returning an empty array. Clients can use this to merge legacy project config into a create/edit workspace form, taking effect on save.
| Parameter | In | Type | Description |
|---|---|---|---|
root | query | string | Required. Absolute path of the project primary directory |
On success, data is { additional_dirs: string[] }.
40001:rootis missing or empty
DELETE /api/v1/workspaces/{workspace_id}
Unregisters a workspace. Only the registry entry is removed — the on-disk directory is untouched.
| Parameter | In | Type | Description |
|---|---|---|---|
workspace_id | path | string | Required. Workspace id |
On success, data is { deleted: true }.
40410: workspace not found
GET /api/v1/workspaces/{workspace_id}/trust
Reads the workspace trust state. Trust gates whether project-level MCP config loads for the workspace.
| Parameter | In | Type | Description |
|---|---|---|---|
workspace_id | path | string | Required. Workspace id |
On success, data is { trusted }.
40410: workspace not found
POST /api/v1/workspaces/{workspace_id}/trust
Marks the workspace trusted, loading its project-level MCP config.
| Parameter | In | Type | Description |
|---|---|---|---|
workspace_id | path | string | Required. Workspace id |
On success, data is { trusted: true }.
40410: workspace not found
POST /api/v1/workspaces/{workspace_id}/untrust
Revokes the persisted workspace trust records associated with the current dir set. Unless a process or config override keeps the workspace trusted, its project-level MCP config unloads.
| Parameter | In | Type | Description |
|---|---|---|---|
workspace_id | path | string | Required. Workspace id |
On success, data is { trusted }, containing the effective trust state after revocation. It is normally { trusted: false }, but remains true when KIMI_CODE_TRUST_WORKSPACE or [workspace] require_trust = false applies.
40410: workspace not found
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 / git_status:batch / pull_request / pull_request:batch / diff / open / open-in / reveal. Actions run on the environment the session was created on; open, open-in, and reveal only work for sessions on the local environment. In addition:
| Method and path | Description |
|---|---|
POST /api/v1/workspace/fs:list | Session-less workspace directory listing |
POST /api/v1/workspace/fs:search | Session-less workspace search (the body carries the workspace reference) |
POST /api/v1/workspace/fs:suggest | Session-less file-completion candidates (for @ file mentions) |
POST /api/v1/fs:suggest | File-completion candidates across absolute root directories, without a session or workspace |
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 |
POST /api/v1/sessions/{session_id}/fs:list
Lists the entries of a session workspace directory, optionally recursing into subdirectories.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
path | body | string | Directory to list, relative to the session work directory. Default . |
depth | body | integer | Recursion depth, 1–10. Default 1 |
limit | body | integer | Maximum entries, 1–1000. Default 200 |
show_hidden | body | boolean | Include dotfiles. Default false |
follow_gitignore | body | boolean | Skip gitignored paths. Default true |
exclude_globs | body | string[] | Additional globs to skip |
allow_ignored_globs | body | string[] | Globs to show even when gitignored (e.g. .tmp). Ignored ancestor directories of a match are kept so the match stays reachable. Allowing dot-paths also requires show_hidden: true |
sort | body | string | type_first (default) / name_asc / name_desc / mtime_desc / size_desc |
include_git_status | body | boolean | Attach each entry's git status. Default false |
On success, data is { items, truncated } — plus children_by_path (a path → entries map) when depth is greater than 1. Each item is an entry object { path, name, kind, size?, modified_at, etag?, mime?, language_id?, is_binary?, is_symlink_to?, git_status?, child_count? }, where kind is file / directory / symlink and git_status (present only with include_git_status: true) is one of clean / modified / added / deleted / renamed / untracked / ignored / conflicted; truncated reports that limit cut the listing short.
40001: body validation failure40401: session not found40409: path not found (including apaththat is not a directory)41304: path escapes the session workspace
POST /api/v1/sessions/{session_id}/fs:read
Reads a slice of a session file as text or base64.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
path | body | string | Required. File path, relative to the session work directory |
offset | body | integer | Byte offset to start at. Default 0 |
length | body | integer | Bytes to read, 1–10485760 (10 MiB). Default 1048576 (1 MiB) |
encoding | body | string | auto (default) / utf-8 / base64 |
On success, data is { path, content, encoding, size, truncated, etag, mime, language_id?, line_count?, is_binary }, where encoding reports the encoding actually used (utf-8 or base64) and size is the full file size. With encoding: "auto", text comes back as utf-8 (non-UTF-8 text is transcoded) and binary content as base64; encoding: "utf-8" forces text and rejects binary files.
40001: body validation failure40401: session not found40409: path not found40906: path is a directory40907: binary file requested withencoding: "utf-8"41302: file exceeds the 10 MiB read ceiling41304: path escapes the session workspace
POST /api/v1/sessions/{session_id}/fs:list_many
Lists several session directories in one call; a failing path folds into the response instead of failing the whole request.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
paths | body | string[] | Required. Directories to list, 1–100 entries |
The remaining body fields (depth, limit, show_hidden, follow_gitignore, exclude_globs, sort, include_git_status) have the same types, ranges, and defaults as fs:list. On success, data is { results }, a map from each requested path to its entry array (entry objects as described under fs:list), plus truncated_paths (paths whose listing hit limit) and partial_errors, a map from a failed path to its { code, msg } error.
40001: body validation failure40401: session not found
POST /api/v1/sessions/{session_id}/fs:stat
Stats one path in the session workspace.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
path | body | string | Required. Path to stat, relative to the session work directory |
On success, data is the entry object described under fs:list.
40001: body validation failure40401: session not found40409: path not found41304: path escapes the session workspace
POST /api/v1/sessions/{session_id}/fs:stat_many
Stats many session paths in one call; missing paths report null instead of failing the request.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
paths | body | string[] | Required. Paths to stat, 1–1000 entries |
On success, data is { entries }, a map from each requested path to its entry object (as described under fs:list) or null when the path does not exist.
40001: body validation failure40401: session not found
POST /api/v1/sessions/{session_id}/fs:mkdir
Creates a directory inside the session workspace.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
path | body | string | Required. Directory to create, relative to the session work directory |
recursive | body | boolean | Create missing parent directories. Default false |
On success, data is the created directory's entry object (as described under fs:list).
40001: body validation failure40401: session not found40409: parent directory not found (non-recursive create)40919: path already exists (non-recursive create)41304: path escapes the session workspace
POST /api/v1/sessions/{session_id}/fs:search
Fuzzy-searches file and directory names across the session workspace. An empty query lists the top-level entries instead. When the {session_id} slot carries a workspace reference (a registered workspace id or an absolute root) rather than a session id, the search runs against that workspace — the session-less form for a not-yet-created draft session; the first-class session-less endpoint is POST /api/v1/workspace/fs:search.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id, or a workspace reference |
query | body | string | Required. Search text; "" lists the top level |
limit | body | integer | Maximum hits, 1–200. Default 50 |
include_globs | body | string[] | Only paths matching one of these globs |
exclude_globs | body | string[] | Skip paths matching these globs |
follow_gitignore | body | boolean | Skip gitignored paths when true. Default false |
On success, data is { items, truncated } where each item is { path, name, kind, score, match_positions } — kind is file / directory / symlink, score is the fuzzy-match score between 0 and 1, and match_positions lists the matched character offsets. Hits prioritize match quality and place ignored hits after non-ignored hits of equal quality. Including ignored hits may require a supplemental scan; truncated reports that hits beyond limit were dropped.
40001: body validation failure40401: neither a session nor a resolvable workspace with that reference
POST /api/v1/sessions/{session_id}/fs:grep
Searches file contents across the session workspace — a literal string by default, a regular expression with regex: true.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
pattern | body | string | Required. Text or regex to search for |
regex | body | boolean | Treat pattern as a regular expression. Default false |
case_sensitive | body | boolean | Default true |
include_globs | body | string[] | Only files matching one of these globs |
exclude_globs | body | string[] | Skip files matching these globs |
follow_gitignore | body | boolean | Skip gitignored paths. Default true |
max_files | body | integer | Files to scan at most, 1–10000. Default 200 |
max_matches_per_file | body | integer | Matches kept per file, 1–10000. Default 50 |
max_total_matches | body | integer | Matches kept overall, 1–100000. Default 5000 |
context_lines | body | integer | Context lines around each match, 0–10. Default 2 |
On success, data is { files, files_scanned, truncated, elapsed_ms } where each entry of files is { path, matches } and each match is { line, col, text, before, after } (before / after carry up to context_lines surrounding lines); truncated reports that one of the match budgets cut the results short.
40001: body validation failure40401: session not found41305: the search timed out
POST /api/v1/sessions/{session_id}/fs:git_status
Reads git status, optionally restricted to a set of paths. When root is provided, entry paths are relative to that directory; when omitted, status covers the whole Git worktree and entry paths are relative to the repository root.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
root | body | string | Workspace directory to scope to. When omitted, status covers the whole Git worktree; a relative value is resolved from the session work directory |
paths | body | string[] | Restrict the status to these paths. Relative paths are resolved from root when provided, otherwise from the session work directory |
include_pull_request | body | boolean | Include the associated pull request. Default true |
On success, data is { branch, ahead, behind, entries, additions, deletions, pullRequest } where entries maps each changed path to its status (clean / modified / added / deleted / renamed / untracked / ignored / conflicted) and pullRequest is { number, state, url } (state is open / merged / closed / draft) or null.
40001: body validation failure40401: session not found40908: git is unavailable (not a repository, or no git binary)41304: path escapes the session workspace
POST /api/v1/sessions/{session_id}/fs:git_status:batch
Reads git status for several workspace directories in one call. A failing directory folds into errors instead of failing the whole request.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
roots | body | string[] | Required. Workspace directories to inspect, 1–32 entries |
include_pull_request | body | boolean | Include the associated pull request in each result. Default true |
On success, data is { results, errors }. results maps each successful root to the same status object as fs:git_status; errors maps each failed root to its { code, msg } error, using the same error codes as the single-directory action.
40001: body validation failure40401: session not found
POST /api/v1/sessions/{session_id}/fs:pull_request
Reads the pull request associated with a session workspace directory.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
root | body | string | Workspace directory to inspect. Defaults to the session work directory; a relative value is resolved from it |
On success, data is { number, state, url } (state is open / merged / closed / draft) or null when no pull request is found.
40001: body validation failure40401: session not found40908: git is unavailable (not a repository, or no git binary)41304: path escapes the session workspace
POST /api/v1/sessions/{session_id}/fs:pull_request:batch
Reads pull requests for several workspace directories in one call. A failing directory folds into errors instead of failing the whole request.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
roots | body | string[] | Required. Workspace directories to inspect, 1–32 entries |
On success, data is { results, errors }. results maps each successful root to the same pull request object as fs:pull_request, or null; errors maps each failed root to its { code, msg } error.
40001: body validation failure40401: session not found
POST /api/v1/sessions/{session_id}/fs:diff
Returns the unified git diff of one file in a session workspace directory.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
path | body | string | Required. File to diff, relative to root |
root | body | string | Workspace directory to inspect. Defaults to the session work directory; a relative value is resolved from it |
On success, data is { path, diff, truncated } where diff is the unified diff text and truncated reports an over-long diff cut short.
40001: body validation failure40401: session not found40908: git is unavailable (not a repository, or no git binary)41304: path escapes the session workspace
POST /api/v1/sessions/{session_id}/fs:open
Opens a session file with the host operating system's default handler. Local environment only.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
path | body | string | Required. File to open, relative to the session work directory |
line | body | integer | Line number to jump to where the handler supports it (positive integer) |
On success, data is { opened: true }.
40001: body validation failure40401: session not found40409: path not found41304: path escapes the session workspace
POST /api/v1/sessions/{session_id}/fs:open-in
Opens a session file or directory in a specific host application. Local environment only.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
app_id | body | string | Required. Target application: finder / cursor / vscode / iterm / terminal |
path | body | string | Required. File or directory to open, relative to the session work directory |
line | body | integer | Line number to jump to where the application supports it (positive integer) |
On success, data is { opened: true }.
40001: body validation failure40401: session not found40409: path not found41304: path escapes the session workspace50001: the application failed to launch
POST /api/v1/sessions/{session_id}/fs:reveal
Reveals a session file in the host operating system's file manager. Local environment only.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
path | body | string | Required. File to reveal, relative to the session work directory |
On success, data is { revealed: true }.
40001: body validation failure40401: session not found40409: path not found41304: path escapes the session workspace
GET /api/v1/sessions/{session_id}/fs/{path}:download
Downloads a file from the session workspace; {path} is the workspace-relative file path with the literal :download suffix. The response is a binary stream with range and ETag support — see Binary and streaming endpoints.
| Parameter | In | Type | Description |
|---|---|---|---|
session_id | path | string | Required. Session id |
path | path | string | Required. Workspace-relative file path plus the :download suffix |
allow_external_symlinks | query | string | true to download through symlinks outside the workspace. Default false |
40001: missing or empty path40401: session not found40409: path not found41304: path escapes the session workspace
POST /api/v1/workspace/fs:list
The session-less form of fs:list: the workspace travels in the body instead of the URL.
| Parameter | In | Type | Description |
|---|---|---|---|
workspace | body | string | Required. Registered workspace id or absolute root (registered on the spot) |
path | body | string | Directory to list, relative to the workspace root. Default . |
depth | body | integer | Recursion depth, 1–10. Default 1 |
limit | body | integer | Maximum entries, 1–1000. Default 200 |
show_hidden | body | boolean | Include dotfiles. Default false |
follow_gitignore | body | boolean | Skip gitignored paths. Default true |
exclude_globs | body | string[] | Additional globs to skip |
allow_ignored_globs | body | string[] | Globs to show even when gitignored |
allow_external_symlinks | body | boolean | Follow symlinked directories whose targets are outside the workspace. Default false |
sort | body | string | type_first (default) / name_asc / name_desc / mtime_desc / size_desc |
On success, data has the same shape as fs:list.
40001: body validation failure40409: path not found or not a directory40410: workspace not found and not a usable absolute path40411: permission denied by the host filesystem41304: path escapes the workspace through a symlink andallow_external_symlinksis not enabled
POST /api/v1/workspace/fs:search
The session-less form of fs:search: the workspace travels in the body instead of the URL.
| Parameter | In | Type | Description |
|---|---|---|---|
workspace | body | string | Required. Registered workspace id or absolute root (registered on the spot) |
query | body | string | Required. Search text; "" lists the top level |
limit | body | integer | Maximum hits, 1–200. Default 50 |
include_globs | body | string[] | Only paths matching one of these globs |
exclude_globs | body | string[] | Skip paths matching these globs |
follow_gitignore | body | boolean | Skip gitignored paths when true. Default false |
On success, data is { items, truncated } with the same hit shape and ordering as fs:search.
40001: body validation failure40410: workspace not found and not a usable absolute path
POST /api/v1/workspace/fs:suggest
Suggests file and directory completion candidates in a workspace without a session — the backend for @ file mentions in the composer.
| Parameter | In | Type | Description |
|---|---|---|---|
workspace | body | string | Required. Registered workspace id or absolute root (registered on the spot) |
query | body | string | Required. Partial path text to complete |
limit | body | integer | Maximum candidates, 1–200. Default 50; fd-backed searches return at most 20 |
follow_gitignore | body | boolean | Skip gitignored paths when true. Default false |
show_hidden | body | boolean | Include dotfiles. Default false; fd-backed searches always include them |
stream | body | boolean | Return NDJSON preview/final events when true. Default false |
include_globs | body | string[] | Only paths matching one of these globs |
exclude_globs | body | string[] | Skip paths matching these globs |
On success, data is { items, truncated } where each item is { path, name, kind, score, match_positions }, the same hit shape as fs:search. Candidates prioritize match quality and place ignored candidates after non-ignored candidates of equal quality. When fd is available, searches use the local @-completion strategy: hidden paths are always included, each root scans at most 100 entries per pass, and at most 20 items are returned. Matches beyond the scan cap may be omitted. Including ignored candidates may require a supplemental --no-ignore-vcs scan; the ripgrep/Node fallback applies when fd is unavailable.
With stream:true, the response is NDJSON instead of a single JSON object. Each envelope's data includes phase (preview, final, or error) and the same result fields. A preview contains ordinary fd candidates with truncated:true; the final result replaces it after the ignored scan. Closing the connection cancels the search.
40001: body validation failure40410: workspace not found and not a usable absolute path
POST /api/v1/fs:suggest
Suggests file and directory completion candidates across one or more absolute root directories, without a session or a workspace; nothing is registered. The first root is the primary root: its candidates come back as relative paths, and candidates under the other roots as absolute paths. Overlapping roots are searched once.
The body takes roots in place of workspace, plus the same query, limit, follow_gitignore, show_hidden, stream, include_globs, and exclude_globs fields as POST /api/v1/workspace/fs:suggest:
| Parameter | In | Type | Description |
|---|---|---|---|
roots | body | string[] | Required. 1–32 absolute directory paths; the first is the primary root |
The response, including the NDJSON form with stream: true, is the same as for POST /api/v1/workspace/fs:suggest.
40001: body validation failure, including a root that is not an absolute path40409: a root does not exist or is not a directory (msgnames it)
GET /api/v1/fs:browse
Lists the subdirectories of one host directory — the backend of the folder picker.
| Parameter | In | Type | Description |
|---|---|---|---|
path | query | string | Absolute directory path. Default the user's home directory |
On success, data is { path, parent, entries } where path is the resolved directory, parent its parent (null at the filesystem root), and each entry is { name, path, is_dir: true }.
40001:pathis not absolute40409: path not found40411: permission denied
GET /api/v1/fs:home
Returns the folder picker's landing payload. No parameters.
On success, data is { home, recent_roots } where home is the user's home directory and recent_roots lists the roots of the registered workspaces.
GET /api/v1/fs:content
Streams the raw bytes of any file on the host filesystem — gated only by the API token, so be careful when exposing the port. Range requests and ETag caching are supported; see Binary and streaming endpoints.
| Parameter | In | Type | Description |
|---|---|---|---|
path | query | string | Required. Absolute file path |
40001:pathis not absolute, or not a regular file40409: path not found40411: permission denied40906: path is a directory
POST /api/v1/fs:mkdir
Creates one directory on the host filesystem by absolute path — the folder picker's "new folder" backend. Non-recursive: the parent directory must already exist.
| Parameter | In | Type | Description |
|---|---|---|---|
path | body | string | Required. Absolute directory path |
On success, data is { path }.
40001:pathis not absolute40409: parent path not found40411: permission denied40919: path already exists
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 |
POST /api/v1/files
Uploads a file as multipart/form-data for later reference (for example as a prompt attachment).
| Parameter | In | Type | Description |
|---|---|---|---|
file | body | binary | Required. The multipart file part |
name | body | string | Stored display name. Default the uploaded filename |
expires_in_sec | body | number | Seconds until the file expires (non-negative). Default never expires |
On success, data is the file metadata { id, name, media_type, size, created_at, expires_at? } with media_type taken from the upload's content type.
40001: the multipart body has nofilefield
GET /api/v1/files/{file_id}
Downloads an uploaded file. The response is a binary stream that honors range requests but ignores If-None-Match; failures use real HTTP statuses — see Binary and streaming endpoints.
| Parameter | In | Type | Description |
|---|---|---|---|
file_id | path | string | Required. File id from the upload response |
40407(HTTP 404): no file with that id (including an expired file)
DELETE /api/v1/files/{file_id}
Deletes an uploaded file.
| Parameter | In | Type | Description |
|---|---|---|---|
file_id | path | string | Required. File id from the upload response |
On success, data is { deleted: true }.
40407(HTTP 404): no file with that id
GUI store
A server-backed key/value store that mirrors the browser localStorage interface, persisted under the server's home directory; the web UI keeps cross-client UI state here. Values are opaque strings — serialization is the caller's job.
| Method and path | Description |
|---|---|
GET /api/v1/gui/store/length | Number of stored keys |
GET /api/v1/gui/store/getItem | Read a value by key |
POST /api/v1/gui/store/setItem | Write a value by key |
POST /api/v1/gui/store/removeItem | Delete a value by key |
POST /api/v1/gui/store/clear | Delete all values |
GET /api/v1/gui/store/length
Returns the number of stored keys (mirrors localStorage.length). No parameters.
On success, data is { length }.
GET /api/v1/gui/store/getItem
Reads one value (mirrors localStorage.getItem).
| Parameter | In | Type | Description |
|---|---|---|---|
key | query | string | Required. Key to read, 1–256 characters |
On success, data is { value }, the stored string or null when the key does not exist.
POST /api/v1/gui/store/setItem
Writes one value (mirrors localStorage.setItem).
| Parameter | In | Type | Description |
|---|---|---|---|
key | body | string | Required. Key to write, 1–256 characters |
value | body | string | Required. Value to store |
On success, data is null.
POST /api/v1/gui/store/removeItem
Deletes one value (mirrors localStorage.removeItem).
| Parameter | In | Type | Description |
|---|---|---|---|
key | body | string | Required. Key to delete, 1–256 characters |
On success, data is null.
POST /api/v1/gui/store/clear
Deletes every stored value (mirrors localStorage.clear). No parameters.
On success, data is null.
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/v2/mcp/* | Unified MCP management plane, see below |
/api/v1/debug/* | Reflection debug RPC; mounted only with --debug-endpoints on loopback, not a stable protocol |
POST /api/v1/search
Cross-session full-text search over user messages, assistant replies, and session titles, backed by the server's persistent search index. Searches scoped to one session with container.session_id use the same index, so text a running step is still generating becomes searchable once the step is saved and the index catches up; index_state.stale marks a view that is still catching up.
| Parameter | In | Type | Description |
|---|---|---|---|
query | body | string | Required. Search text |
mode | body | string | terms (default) / literal |
op | body | string | Term combiner in terms mode: AND (default) / OR |
container | body | object | Restrict the search to { session_id?, agent_id? } |
role | body | string | Restrict to user / assistant / title hits |
start_time | body | integer | Only hits at or after this time (epoch milliseconds) |
end_time | body | integer | Only hits at or before this time (epoch milliseconds) |
sort | body | string | score (default) / time_desc / time_asc; ignored by literal mode, which always returns newest-first |
page_size | body | integer | Hits per page, 1–50. Default 20 |
page_token | body | string | Token from the previous page's response |
In terms mode the query is tokenized (ASCII words plus CJK n-grams), deduplicated, and matched against the inverted index with at most 32 terms; literal mode is an exact substring search with zero false positives. On success, data is { items, has_more, page_token?, index_state } where each item is { session_id, workspace_id, session_title, agent_id, role, snippet, time, turn?, step_id?, score }. index_state is { state, indexed_sessions, total_sessions, documents, stale?, degraded? } with state one of building / ready / readonly; stale marks a behind view still catching up, and degraded carries the last refresh failure. An over-budget page additionally carries incomplete, one of candidate_cap / postings_budget / deadline. Page tokens pin the index generation and the query conditions — a rebuild or a changed query invalidates them.
40001: body validation failure, an unusable query (empty, or more than 32 terms), or an invalid page token
GET /api/v1/connections
Lists the WebSocket clients currently connected to this server, oldest connection first. No parameters.
On success, data is { connections } where each item is { id, connected_at, remote_address, user_agent, subscriptions }: connected_at is an ISO 8601 timestamp, remote_address and user_agent are null when unknown, and subscriptions lists the session ids the connection is subscribed to.
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 |
meta.has_prompt | true keeps only sessions that carry a user prompt, false keeps only empty ones (the exclude_empty equivalent of GET /api/v1/sessions) |
view | flat (default) / by_workspace, see below |
group.page_size | Sessions returned per workspace under view=by_workspace: 1–100, default 5 (up to 10000 with the id,archived projection); rejected without the grouped view (40001) |
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 environment and working 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. Under view=by_workspace it counts groups per page |
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. The activity group also reports model: the session's bound model alias while it is live in this process, null for cold (not currently loaded) sessions. 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.
With view=by_workspace the same filtered, sorted set is re-projected into per-workspace groups, so an overview client replaces one polling loop per workspace with a single request:
{
"code": 0,
"msg": "success",
"data": {
"groups": [
{
"workspace": { "id": "wd_my-app_a1b2c3d4e5f6", "cwd": "/Users/dev/my-app" },
"sessions": [ { "id": "session_...", "workspace": { "id": "wd_my-app_a1b2c3d4e5f6", "cwd": "/Users/dev/my-app" }, "meta": { "title": "Fix the login page", "last_prompt": "adjust the button spacing", "created_at": 1787000000000, "updated_at": 1787000100000, "archived": false, "archived_at": null }, "activity": { "status": "idle", "model": "kimi-for-coding" } } ],
"total": 42
}
],
"total": 7,
"has_more": true,
"next_page_token": "eyJ2IjoxLCJmIjoi..."
},
"request_id": "req_..."
}Each group carries the workspace's first group.page_size sessions under the requested sort plus total, the workspace's full matching-session count (for a "view all" entry). Only workspaces with at least one matching session appear; groups order by their first session's sort key, ties broken by workspace id. page and page_token paginate over groups (the outer total is the group count), with the same fingerprint binding: the token also covers view and the grouping parameters, so flipping them mid-pagination returns 40922.
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.
{
"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_..."
}MCP management (/api/v2/mcp)
The /api/v2/mcp/* routes are the server's unified MCP management plane: they manage the MCP server registry itself, independent of any session — global (user-level) CRUD with per-entry validation, connection-test probes, a locator-addressed inspection catalog, per-server auth-status listing, and the full OAuth flow lifecycle.
| Method and path | Description |
|---|---|
GET /api/v2/mcp/servers | List every known MCP server |
GET /api/v2/mcp/servers/{name} | Get one server by runtime name |
POST /api/v2/mcp/servers | Add a server to the user-level mcp.json |
PUT /api/v2/mcp/servers/{name} | Replace a user-level entry |
DELETE /api/v2/mcp/servers/{name} | Remove a user-level entry |
POST /api/v2/mcp/servers:test | Probe a real connection to one server |
POST /api/v2/mcp/servers:inspect | Locator-addressed catalog with a batched connection probe |
GET /api/v2/mcp/auth-statuses | Per-server OAuth state over the catalog |
POST /api/v2/mcp/auth:begin | Begin an interactive OAuth flow |
POST /api/v2/mcp/auth:complete | Await the browser callback and finish the code exchange |
POST /api/v2/mcp/auth:cancel | Tear down a begun OAuth flow |
POST /api/v2/mcp/auth:reset | Clear a server's stored credentials |
Two addressing schemes appear on this plane. The CRUD routes and servers:test take a plain runtime name; the inspection and OAuth routes take a locator — { "source": "global", "name" } for a file-layer entry or { "source": "plugin", "pluginId", "serverName" } for a plugin-manifest entry — because a plugin entry and a file entry can share one runtime name. Inspection items additionally carry a stable serverId wire id: global:<name> or plugin:<pluginId>:<serverName> (URL-encoded).
Most routes accept an optional cwd (a query parameter, or a body field on the :-action routes). Without it the catalog covers the user-level file and plugin manifests only; with it, the project-root and project-local layers of that directory join in — but only when the workspace is trusted, otherwise the project layers are skipped. For servers:test on a stdio server, cwd is also the child process's working directory. Connection probes and OAuth calls wait for the server's configuration to finish loading before acting.
GET /api/v2/mcp/servers and GET /api/v2/mcp/servers/{name}
Lists every MCP server the management plane knows about; the second route returns the single entry with that runtime name.
| Parameter | In | Type | Description |
|---|---|---|---|
name | path | string | Required (get only). Runtime name of the server |
cwd | query | string | Include the project layers of this (trusted) directory |
On success, data is an array of managed servers (a single object for the get route), each { name, config, source, origin, mutable, plugin? }:
source:global(a config-file layer) orplugin(a plugin manifest)origin: where the entry is defined — a file path or a plugin idmutable: only user-level entries are mutable; plugin and project-layer entries are read-onlyconfig: mutable entries carry the full config so edit UIs can prefill it; read-only entries are redacted to sorted key lists (envKeys/headerKeys) and never disclose secret valuesplugin:{ id, name }, present on plugin entries40001: validation failure40408: no server with that name
POST / PUT / DELETE /api/v2/mcp/servers
Global CRUD against the user-level mcp.json. The add body is a full server config including name — transport (stdio / http / sse) discriminates the shape, and each entry is validated before it is written. The update body carries the same config without name (the path names the entry); delete takes no body. All three return the refreshed server list in data. A write whose name collides with a project-layer entry is rejected as read-only — edit the defining file instead; a same-named plugin entry does not block the write, and the new file entry shadows it.
40001: validation failure, or the target entry is read-only40408: (update/delete) no server with that name
POST /api/v2/mcp/servers:test
Probes a real connection to one server and never persists anything. Pass either name to test a registry entry (plugin and trusted project layers included) or server (a full inline config, name included) to probe it as-is; passing both or neither fails 40001.
| Parameter | In | Type | Description |
|---|---|---|---|
name | body | string | Runtime name of a registry entry |
server | body | object | Inline server config to probe as-is |
cwd | body | string | Project layers join the resolution; also the stdio working directory |
On success, data is { success, output }: when the connection succeeds, output lists the server's available tools; otherwise it carries the failure text.
40001: both or neither target form passed, an invalid inline config, or a runtime name shared by multiple enabled servers40408: no server with that name
POST /api/v2/mcp/servers:inspect
The locator-addressed catalog (redacted configs) plus a batched real-connection probe of every OAuth candidate.
| Parameter | In | Type | Description |
|---|---|---|---|
targets | body | array | Locators narrowing the catalog; omitted inspects all servers |
cwd | body | string | Include the project layers of this (trusted) directory |
On success, data is an array of inspections, each { serverId, locator, runtimeName, canonicalUrl?, origin, config, enabled, editable, authStatus, checkedAt?, error? }: canonicalUrl is the credential URL of a remote server, config is the redacted view, and authStatus is one of not-applicable / bearer-token / oauth-required / oauth-authorized / oauth-expired / unavailable. A runtime name shared by multiple enabled servers cannot be probed unambiguously and reports unavailable with an explanatory error. A probe that hits an expired grant may refresh or invalidate the stored credentials.
40001: validation failure40408: atargetslocator matches nothing
GET /api/v2/mcp/auth-statuses
Per-server OAuth state over the registry catalog — the lightweight alternative to servers:inspect when only the auth dimension is needed.
| Parameter | In | Type | Description |
|---|---|---|---|
cwd | query | string | Include the project layers of this (trusted) directory |
verify | query | string | true probes every OAuth candidate through a real connection; false is fully offline (config and stored tokens only); omitted preserves implicit OAuth detection, probing only unpinned remote servers without stored credentials |
On success, data is an array of { name, authStatus } with the same authStatus enum as servers:inspect. Verification probes may refresh or invalidate stored credentials.
POST /api/v2/mcp/auth:begin / :complete / :cancel / :reset
The OAuth flow lifecycle for remote servers. auth:begin takes a locator body (plus the optional cwd query) and answers data { status: "authorization-required", flowId, authorizationUrl } — open the URL in a browser to grant access — or { status: "already-authorized" } when a grant already exists. The target server must use a remote transport (http / sse) and must not carry a static bearer token; static headers are allowed only when the config explicitly sets auth: "oauth".
auth:complete waits for the browser callback of a begun flow and finishes the code exchange. Its body is { flowId, timeoutMs? }: the wait defaults to 15 minutes (timeoutMs overrides it), an idle flow expires after 15 minutes regardless, and closing the HTTP connection aborts the wait. data is null on success.
auth:cancel tears down a begun flow ({ flowId }) without finishing it; unknown flows are ignored. auth:reset takes a locator body and clears the server's stored credentials — the invalidation event reaches live sessions.
40001: validation failure — including an unknownflowIdon:complete, or a server that cannot do OAuth (stdio transport, a static bearer token, or static headers withoutauth: "oauth") on:begin40408: (:begin/:reset) the locator matches nothing40929: the OAuth flow itself failed
WebSocket protocol
Connect
The only endpoint is ws://<host>:<port>/api/v1/ws; authentication happens at the upgrade request, with the Authorization: Bearer <token> header or the kimi-code.bearer.<token> subprotocol (see Authentication above). There is no handshake: once the socket opens, the connection receives global events and the client can send commands.
Heartbeat
The server sends { "type": "ping" } every 10 seconds and the client answers { "type": "pong" }. The server closes a connection it has received no frame from for 20 seconds. A client that receives nothing for 25 seconds should treat the connection as dead and reconnect.
Commands
Clients send flat JSON frames: the fields sit next to type, and every command carries a request_id. Each command gets one reply { "type": "response", "request_id", "code", "msg"?, "task"? }, where code 0 means success. A frame without a readable request_id closes the socket with code 1008; frames of unknown types are ignored.
| Frame | Fields | Description |
|---|---|---|
subscribe | { request_id, session_id, agent_ids? } | Subscribe to a session and start receiving the listed agents; agent_ids defaults to ["main"], and a later subscribe for the same session adds agents. When the session cannot be opened, the response carries the error code — 40401 when it does not exist, others such as 40930 or 40931 say why — and nothing is subscribed. Once the session is open the response is sent right away, before any frame of the subscription; agent conversations load after it. A malformed frame answers 40001 |
unsubscribe | { request_id, session_id, agent_ids? } | Stop receiving agents; omitting agent_ids, or removing the last agent, drops the whole session subscription |
task.watch | { request_id, session_id, agent_id, task_id, since_offset?, tail_bytes? } | Stream one task's output to this connection, no session subscription needed; agent_id is the agent that owns the task (the agent_id of its task frame), since_offset resumes after output the client already has, and tail_bytes caps the size of the first chunk |
task.unwatch | { request_id, session_id, agent_id, task_id } | Stop streaming that task's output |
task.stop | { request_id, session_id, agent_id, task_id } | Stop a task; an ended task just replies with its state |
task.background | { request_id, session_id, agent_id, task_id } | Stop the starting tool call from waiting so the task keeps running in the background; a task already in the background or ended just replies with its state, and a task its caller cannot stop waiting for, such as an AgentSwarm member, fails with 40933 |
On success, task.watch, task.stop, and task.background carry the task's current state in task (the task frame shape below, without type, session_id, agent_id, and event_created_at). Requests for the same task are handled one at a time in arrival order, so a task.unwatch sent while a task.watch is still pending takes effect after it; different tasks, and other commands, never wait on each other. A successful task.watch replies with code 0, then sends the last tail_bytes of output (32 KiB by default, at most 1 MiB; only the part after since_offset when it falls inside that window) as one task.output frame, with empty text when there is nothing to send, followed by live output; ended tasks can be watched too. Errors: 40001 malformed frame (including an out-of-range tail_bytes), 40401 session not found, 40406 the agent has no such task, 40933 the task cannot be moved to the background (its reply still carries the task state), 50001 internal error, 50003 output unreadable.
Events
Global events are envelopes { "type", "session_id"?, "timestamp", "payload" }, where type is the event type itself; session frames are flat { "type", "session_id", "agent_id", ... }. Two delivery scopes:
- Global events: sent to every established connection, no subscription needed —
session.meta.updated,event.session.created,event.session.archived,event.session.restored,event.session.work_changed,event.session.status_changed,event.workspace.*,event.config.*,event.model_catalog.*. - Session frames: sent only to connections subscribed to that session with
subscribe, and only for the agents they receive —turns,turns.removed,turns.failed,turn.*,item.*, andnotice(see Conversation protocol),task.list,task,agent,usage,goal,todo;interactionframes go to every subscriber of the session.task.outputgoes only to connections watching that task withtask.watch.
Several global lifecycle events keep a cross-workspace overview fresh without polling per workspace. event.session.archived fires on both the live and the cold archive path; its envelope session_id is the global watermark __global__ and the real session id rides in the payload: { "type": "event.session.archived", "workspace_id": "wd_...", "sessionId": "session_..." } (payload keys workspace_id / sessionId). event.session.restored fires when an archived session is restored, on the same paths and with the same envelope and payload keys. event.workspace.created / updated carry the full workspace object ({ id, root, name, created_at, last_opened_at, session_count } — an updated also fires when a session creation touches the workspace), and event.workspace.deleted carries { "workspace_id", "root" }. These events only cover changes made inside this server process; changes from other processes (for example a CLI writing to the same home) surface through the index reconciliation (about a minute), so overview clients should keep a low-frequency fallback poll. There is no session-deleted event.
Each agent's running tasks — shell commands in the foreground and background, subagents (swarm members included), and other asynchronous work — are synced by two flat frames, task.list and task, never replayed like interaction. When a connection starts receiving an agent's tasks (an agent listed in subscribe's agent_ids that it was not receiving yet), it first gets { type: "task.list", session_id, agent_id, tasks }, where tasks is every task the agent is running right now and replaces the client's list; after that it gets { type: "task", session_id, agent_id, event_created_at, ...task state } on every change. An agent listed before it joins the session starts directly with task frames. The task state is { task_id, kind: "shell" | "subagent" | "other", status: "running" | "ended", outcome?, phase?, detached, description?, child_agent_id?, parent_tool_call_id?, swarm_index?, command?, started_at?, ended_at?, result_summary?, error?, state_reason?, model?, thinking? }, and every frame carries the full state. detached means the tool call that started the task no longer waits for it, so it runs in the background; outcome (ended tasks only) is completed / failed / timed_out / killed / lost; phase (running subagents only) is working / suspended, with the reason in state_reason; child_agent_id is a subagent task's agent id; swarm_index is a swarm member's position in its swarm; command is a shell task's command. When a task ends, its last task frame (status: "ended") is sent and it no longer appears in task.list: the outcome of an ended task lives in the session history (tool results and task notifications), and task.watch on it replies with its exact final state. Tasks are independent of turns: undoing turns does not touch tasks. Task output streams separately as { type: "task.output", session_id, agent_id, task_id, offset, text }, where offset is the UTF-8 byte offset of text in the task's output.
An agent's own state is synced by four more flat frames, each carrying the full value and replacing the client copy; like the task frames they are never replayed. When a connection starts receiving an agent (the same rule as task.list), it first gets the current agent, usage (when the agent has a model context), goal (main agent only), and todo frames, and after that each frame again whenever its value changes; a listed agent that joins the session later announces itself with the same frames. Subscribing to a session creates its main agent if it does not exist yet, so the main agent's frames always follow the subscription. All four are { type, session_id, agent_id, event_created_at, ... }:
agent:{ status: "idle" | "running", compacting, model?, thinking?, permission_mode?, plan_mode, swarm_mode, tower_mode }.statusisrunningwhile the agent works on a turn andidlewhen it does not.compactingistruewhile the agent's context is being compacted, whether inside a turn or not. The running turn, and the retry state of a failed model request, come with the conversation frames.compactingand the three mode fields are booleans.usage:{ context_tokens, max_context_tokens? }. Only the latest value matters, so while the context grows the server sends it at most a few times per second.goal:{ goal? }, wheregoalis omitted when the session has none, or is{ objective, status: "active" | "paused" | "blocked" | "complete", completion_criterion?, turns_used, tokens_used, wall_clock_ms }.todo:{ items: [{ title, status: "pending" | "in_progress" | "done" }] }, read from the agent's todo store, so it follows undo and survives a restart.
Approvals and questions arrive as interaction frames, sent to every subscriber of the session: { type: "interaction", session_id, agent_id, event_created_at, interaction_id, kind: "approval" | "question", status: "pending" | "ended", request, response?, outcome?, ended_at?, tool_call_id? }. agent_id is the agent that raised the interaction, and tool_call_id the waiting tool call, whose tool_call item carries the same interaction_id. An approval's request is { tool_name, action, tool_input_display?, expires_at }, with expires_at 24 hours after the request was raised; a question's request is { questions }, 1–4 items { id, question, header?, body?, options, multi_select?, allow_other?, other_label?, other_description? }, each with 2–4 options of { id, label, description? } — multi_select allows several options, allow_other a free-text answer. Answer an interaction with POST /api/v1/sessions/{session_id}/interactions/{interaction_id}. When it ends — answered, or cancelled because its turn ended — a last frame carries status: "ended", ended_at, outcome (approved / rejected / cancelled / answered / dismissed), and response: { decision, scope?, feedback?, selected_label? } for an approval, { answers, method? } for an answered question, where answers maps each question's text to the answer.
Reconnect and recovery
Subscriptions do not survive a disconnect. After reconnecting, send subscribe again for each session: every agent starts again with a turns frame holding its latest page, pending interactions are resent after the response, and every agent's task.list, agent, usage, goal, and todo frames are sent again. Task output watches are dropped as well: send task.watch again with since_offset set to the end of the output you already have. Global events missed while disconnected are not replayed, so refresh the lists you keep (for example GET /api/v1/sessions) over REST.
Conversation protocol
An agent's conversation is a list of turns, each an ordered list of items. Turns and items are projected from the agent's persisted session records, so the REST page, the turns frame, and a reload after a restart show the same turns with the same ids; the only state that exists nowhere else is what a running step and a running tool are still producing. Times are ISO 8601 strings.
A turn is { turn_id, ordinal, kind, status, outcome?, end_reason?, error?, retry?, turn_number?, started_at, ended_at?, duration_ms?, usage?, steps, items }:
turn_idis unique within the session and never reused: a turn opened after an undo gets a new id even when it reuses theordinalof an undone turn.ordinalis the turn's position in the agent's current history and orders the turns; older pages are read withbefore_ordinal.turn_numberis the engine's turn number, used by the file-history endpoints.kindisagent, orsystemfor a turn that holds a singlesystemitem between agent turns: a completed context compaction, or the fork boundary that opens the conversation of a forked agent. Asystemturn is neverrunning; it arrives asturn.started,item.ended, andturn.endedback to back.statusisrunningorended; turns the agent does not run, such as a prompt a hook rejected, are alreadyended. An ended turn has anoutcome:completed;interruptedwith theend_reasonuser_cancelled(the user stopped it) oraborted(it stopped for another reason, such as the server stopping while it ran); orfailedwith theend_reasonerror,max_steps,filtered(the provider's content filter blocked the output), orblocked(a hook or a policy rejected the input).erroris{ code, message, name?, status_code?, request_id?, retryable? }on failed turns.retryis{ attempt, next_attempt, max_attempts, delay_ms, error: { name, message, status_code? } }while a failed model request of the running turn waits to be retried;attemptis the attempt that just failed. It clears when the next step starts, when a step ends, and when the turn ends.stepslists the ended steps (model requests) byindexas{ index, outcome, started_at, ended_at?, finish_reason?, usage? }. A step'soutcomeiscompleted,interrupted, orfailed;usageis{ input_other, output, input_cache_read, input_cache_creation }. The turn'susagesums its steps.
Every item has { item_id, type, step_index?, status, outcome?, created_at, debug? }. Items keep the order they appeared in, and an ended item has the outcome completed unless noted below. When the server runs with KIMI_CODE_DEBUG=1, the last item of each ended step carries debug: { finish_reason?, usage?, timing? } for the model request of that step, where timing holds first_token_ms, stream_ms, request_build_ms, server_first_token_ms, server_decode_ms, client_consume_ms, and client_blocked_ms when measured; the item arrives again as item.ended once the step ends.
user_message:{ prompt_ids, placement: "opener" | "steer", origin, text, attachments, skills, metadata? }.prompt_idslists the prompt ids the input carries — including theprompt_idof a bundled skill submission or a skill activation — so a client can retire its optimistic bubble; it is empty for inputs the system triggered.originis{ kind: "user" },{ kind: "skill", name, path?, args? },{ kind: "plugin_command", plugin_id, command, args? },{ kind: "cron", job_id, schedule, recurring, coalesced_count, stale },{ kind: "hook", hook_event },{ kind: "goal" }for a goal continuation,{ kind: "retry" }, or{ kind: "system" }.textjoins the visible text parts with newlines, without bundled skill instructions, prompt hook output, the notices that point the model at attached files, or image compression notes; it is empty for retries and internal triggers.attachmentslists the files and media of the input in order as{ kind: "image" | "video" | "audio" | "file", file_id?, url?, name?, media_type?, size?, ordinal }: uploaded files carryfile_id, inline and remote media carryurl, media the server stored is sent as adata:URL in pages, and a file attached by a server-local path carries only itsname,media_typeandsize.skillslists the skills activated with the input as{ name, path?, args? }, andmetadataechoes themetadatasent with the prompt. A CLI shell command (!) and its output arrive as raw text in auser_messagewithorigin: { kind: "system" }, each in its own ended turn.assistant_message:{ text }.reasoning:{ text, started_at, duration_ms? };duration_msis how long the reasoning streamed, present once the item has ended. Hidden reasoning is never sent.tool_call:{ item_id: tool call id, name, stage?, input?, input_text?, display?, output?, media?, error?, note?, fields?, task_id?, interaction_id?, agent_refs?, started_at, ended_at? }. While the call runs,stageisinputwhile the model streams the arguments (input_textholds them as text),awaiting_approvalwhile its approval is pending, andexecutingotherwise;outputstreams the tool's live output andfieldsis the latest handler field snapshot as a JSON object. Once ended,outputis the text of the result andmedialists its images, videos, and audio as{ kind, file_id?, url?, media_type? }; theoutcomeisfailed, witherrorinstead ofoutput, when the result is an error, andinterruptedwhen the turn ended before a result.noteis the extra text the tool attached to its result for the model, such as the size and dimensions of an image read byReadMediaFile.task_idappears once the call's work runs as a background task, whether it started in the background or was moved there later; it keeps working withtask.watchandtask.stopafter the task has lefttask.list. While a foreground call runs, find its task through the task'sparent_tool_call_id.interaction_idlinks the approval or question raised for the call, whose content comes with theinteractionframes, andagent_refslists the subagents the call started as{ agent_id, role: "child" | "member", swarm_index?, task_id? }.ExitPlanModecalls carry thedisplay{ kind: "plan_review", plan, path?, options: [{ label, description }], review: { state: "pending" | "approved" | "rejected" | "cancelled", selected_option?, feedback? } }.notification:{ notification_id, title, body, severity: "info" | "warning" | "error", notification_type, source: { kind, id }, task_id?, agent_id?, output?: { file?, preview?, bytes?, truncated? }, answer?: { answers, note? } }, a background task notification delivered into the conversation.answeris the user's reply to a background question:answersmaps each question to its answer, andnoteis an optional note.system:{ kind: "compaction", trigger: "manual" | "auto", instruction?, tokens_before?, tokens_after?, compacted_count?, summary? }for a completed context compaction, placed inside the running turn when it ran mid-turn; or{ kind: "fork_boundary", source: "btw" | "fork", forked_from: { agent_id } }, which opens the conversation of an agent forked from another one, such as a side chat, without showing the conversation it inherited. A compaction that is cancelled or fails leaves nothing in the conversation.
When a connection starts receiving an agent it gets { type: "turns", session_id, agent_id, event_created_at, turns, has_more } with the latest 10 turns, within 1 MiB like the REST pages, including the running turn and what it has produced so far. When the agent's conversation cannot be loaded, it gets { type: "turns.failed", session_id, agent_id, event_created_at, code, msg } instead — for example 40401 for an agent that is not in the session, or when an agent loaded again after it was closed fails to restore — and the agent stays subscribed. A later turns frame replaces the client's latest turns; it is sent again when the agent is loaded again after it was closed. After that:
{ type: "turns.removed", session_id, agent_id, event_created_at, turn_ids }lists the turns that left the conversation, in ordinal order: the undone turns after an undo (theremoved_turn_idsof the undo response), and every turn after a context clear. The client removes them byturn_id; the undo response and this frame may arrive in either order.turn.startedandturn.endedcarryturnwithoutitemsand replace the client copy of the turn, keeping its items;turn.endedis sent again when an ended turn is corrected.turn.updatedcarriesturn_id,steponce per ended step (write it intostepsbyindex), andretry, which replaces the turn's retry state; an absentretryclears it.item.startedanditem.endedcarryturn_idand the fullitem, and replace the client copy with the sameitem_id; an unknown item is appended to its turn. Items that do not stream, such asuser_message, only getitem.ended. A streamed item staysrunninguntil its step is persisted, then gets a singleitem.endedwith the persisted values; it does not end early when the next text, reasoning, or tool call of its step starts, so clients treat only the last block as still streaming.item.updatedcarriesturn_id,item_id, andset, the fields of a runningtool_callthat changed; merge it shallowly into the item.item.deltacarriesturn_id,item_id,field(text,input_text, oroutput), anddelta, text to append to that field.item.removedcarriesturn_idanditem_id: a running item that will not be finished, for example after a request retry restarts the step.noticeis{ level: "info" | "warning" | "error", title, detail?, code? }, a live warning or error raised by the agent, for example the codecompaction.cancelledwhen a context compaction did not finish. Notices are not part of the turns and are not sent again on subscribe.
Events may repeat what the turns frame already contains; applying them as replacements keeps the result correct.
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}, GET /api/v1/fs:content, and session :download answer failures with real 4xx / 500 statuses and the same response envelope. Session export reports errors with an HTTP 200 envelope. Ordinary JSON RPC endpoints keep the almost-always-200 convention, so clients must check the envelope code on those endpoints.
Next steps
- Using Kimi Code in the browser — start the server and use Kimi Code in a browser
- kimi command — all
kimi webcommand-line options