Skip to content

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:

  • OPTIONS preflight requests
  • GET /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:

json
{
  "code": 0,
  "msg": "success",
  "data": {},
  "request_id": "01JZX4A6E7M8V0R3Q0N2K2M5Q9"
}
  • code: the business outcome; 0 means success. See the error-code bands below.
  • data: the payload on success. Note that some "error" envelopes also carry a non-null data — for example, an undo that cannot run returns 40911 with data set to { reason, requestedCount, undoableCount } — so clients should check code first, then data.
  • request_id: a ULID for this request. Clients may supply one via the X-Request-Id header; invalid values are regenerated by the server.

The HTTP status is almost always 200; the business outcome lives in code. Exceptions:

SituationHTTP status
Authentication failure / rate limit401 / 429
Provider created, provider catalog imported201
Provider deleted204
Binary/streaming endpoints206 (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 errorsreal 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:

BandMeaningExamples
0Success
400xxBad request40001 validation failed (details lists each field), 40003 provider is OAuth-managed, 40006 duplicate workspace source folder
401xxAuth and readiness40101 unauthorized, 40110 no provider configured, 40113 model not resolved
404xxNot found40401 session, 40408 MCP server, 40409 file path
409xxState conflict40901 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
413xxSize or boundary exceeded41302 file read over 10 MB, 41304 path escapes the session directory
429xxRate limited42901 auth-failure ban, 42902 too many fs watches
500xxServer internal error50001 uncaught exception, 50003 persistence failure
6xxxx / 7xxxx / 8xxxxTool 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) plus page_size (1–100), responding with { items, has_more }. Used by the session list, message list, and others. Agent turns page by turn ordinal instead (before_ordinal / page_size).
  • page_token: an opaque token (bound to a fingerprint of the query conditions), used by POST /api/v1/search and GET /api/v2/sessions. Changing any query condition mid-pagination invalidates the token: v2 returns 40922, search returns 40001. GET /api/v2/sessions also offers a stateless page page-number mode as an alternative.

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.

  1. Check server status:
sh
curl -s -H "Authorization: Bearer $TOKEN" http://127.0.0.1:58627/api/v1/meta

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

  1. Create a session; metadata.cwd sets the working directory:
sh
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.

  1. 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 WebSocket client):
js
// 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],
    }),
  );
  1. Submit a prompt:
sh
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.

  1. Read history back over REST at any time:
sh
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 pathDescription
GET /api/v1/healthzLiveness probe; auth-exempt
GET /api/v1/metaServer version, capability map, server_id, experimental flags
POST /api/v1/shutdownGraceful 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:

FieldTypeDescription
server_versionstringServer version
capabilitiesobjectCapability map — websocket, file_upload, fs_query, mcp, tasks, all always true
server_idstringUnique id of this server instance
started_atstringBoot time, ISO 8601
open_in_appsarrayHost apps usable as open-in targets (finder / cursor / vscode / iterm / terminal); currently always empty
dangerous_bypass_authbooleanWhether the server was started with --dangerous-bypass-auth (clients may skip the token prompt)
backendstringEngine backend, v1 or v2; always v2 for this server
web_titlestringCustom browser tab title from --web-title; omitted when unset
experimental_flagsobjectExperimental flag id → enabled, resolved at request time
featuresarrayEngine 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 pathDescription
GET /api/v1/authAuth snapshot
POST /api/v1/oauth/loginStart the OAuth device-code login flow
GET /api/v1/oauth/loginPoll the login flow state
DELETE /api/v1/oauth/loginCancel a pending login flow
POST /api/v1/oauth/logoutLog out the managed provider
GET /api/v1/oauth/usagePlan quota and booster wallet
GET /api/v1/oauth/userinfoAccount profile
GET /api/v1/oauth/regionResolve 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.

ParameterInTypeDescription
providerbodystringManaged provider name. Default managed:kimi-code
regionbodystringmainland-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.

ParameterInTypeDescription
providerquerystringManaged 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.

ParameterInTypeDescription
providerquerystringManaged 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.

ParameterInTypeDescription
providerbodystringManaged 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".

ParameterInTypeDescription
providerquerystringManaged 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.

ParameterInTypeDescription
providerquerystringManaged 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 pathDescription
GET /api/v1/remote-controlRead the tunnel status
POST /api/v1/remote-controlStart or stop the tunnel
GET /api/v1/remote-control/devicesList the account's devices
PATCH /api/v1/remote-control/devices/{id}Rename a device
POST /api/v1/remote-control/devices/{id}/deactivateTake 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.

ParameterInTypeDescription
enabledbodybooleantrue 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.

ParameterInTypeDescription
idpathstringDevice identifier
aliasbodystringNew 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.

ParameterInTypeDescription
idpathstringDevice 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.

ParameterInTypeDescription
idpathstringDevice identifier

On success, data is { ok: true }.

Config ​

Method and pathDescription
GET /api/v1/configRead the global config (secret fields redacted)
POST /api/v1/configMerge-patch the config; broadcasts event.config.changed
POST /api/v1/client_configsFetch 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:

FieldTypeDescription
providersobjectMap of provider id → { type, base_url?, default_model?, has_api_key }
default_providerstringGlobal default provider id
default_modelstringGlobal default model alias
modelsobjectMap of model alias → model record
thinkingobjectDefault parameters for Thinking mode
plan_modebooleanPlan mode flag
yolobooleanDerived: true when default_permission_mode is yolo
default_permission_modestringDefault permission mode for new sessions
default_plan_modebooleanWhether new sessions start in Plan mode
permissionobjectInitial permission rules
hooksarrayLifecycle hooks
servicesobjectBuilt-in external service configuration
merge_all_available_skillsbooleanWhether to merge Agent Skills from all available directories
extra_skill_dirsarrayExtra skill search directories
loop_controlobjectAgent loop control parameters
backgroundobjectBackground task runtime parameters
subagentobjectSubagent configuration
secondary_modelobjectSecondary model pool for subagents
experimentalobjectExperimental flag id → enabled
telemetrybooleanWhether anonymous telemetry is enabled
auto_session_titlebooleanWhether clients may automatically generate session titles
disable_feedback_surveybooleanWhether the session rating prompt is turned off
rawobjectRaw 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:

ParameterInTypeDescription
providersbodyobjectMap of provider id → provider table
default_providerbodystringGlobal default provider id
default_modelbodystringGlobal default model alias
modelsbodyobjectMap of model alias → model record
thinkingbodyobjectDefault parameters for Thinking mode
plan_modebodybooleanPlan mode flag
yolobodybooleantrue maps to default_permission_mode: "yolo"; false is ignored
default_permission_modebodystringmanual / yolo / auto
default_plan_modebodybooleanWhether new sessions start in Plan mode
permissionbodyobjectInitial permission rules
hooksbodyarrayLifecycle hooks
servicesbodyobjectBuilt-in external service configuration
merge_all_available_skillsbodybooleanWhether to merge Agent Skills from all available directories
extra_skill_dirsbodyarrayExtra skill search directories
loop_controlbodyobjectAgent loop control parameters
backgroundbodyobjectBackground task runtime parameters
subagentbodyobjectSubagent configuration
secondary_modelbodyobjectSecondary model pool for subagents
experimentalbodyobjectExperimental flag id → enabled
telemetrybodybooleanWhether anonymous telemetry is enabled
auto_session_titlebodybooleanWhether clients may automatically generate session titles
disable_feedback_surveybodybooleanWhether 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.

ParameterInTypeDescription
namebodystringRequired. 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 pathDescription
GET /api/v1/modelsList configured model aliases
POST /api/v1/models/{model_id}:set_defaultSet the global default model
GET /api/v1/providersList providers
POST /api/v1/providersCreate 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}:refreshRefresh one provider's model metadata
POST /api/v1/providers:{action}Collection actions: refresh / refresh_oauth / import_catalog / import_registry
GET /api/v1/catalog/providersBrowse 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.

ParameterInTypeDescription
model_idpathstringRequired. 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 path
  • 40413: 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:

FieldTypeDescription
idstringProvider id
typestringWire protocol: kimi / openai / openai_responses / anthropic / google-genai / vertexai
base_urlstringAPI base URL, when set
default_modelstringThe provider's default model alias, when set
has_api_keybooleanWhether a credential is stored
statusstringconnected when an API key or cached OAuth token exists, unconfigured otherwise (error is reserved in the schema)
modelsarrayThe 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.

ParameterInTypeDescription
idbodystringRequired. Provider id — letters, digits, -, _, and spaces; must start with a letter or digit
typebodystringRequired. Wire protocol: kimi / openai / openai_responses / anthropic / google-genai / vertexai
api_keybodystringAPI key, stored in config.toml
base_urlbodystringAPI base URL; must not contain an environment variable placeholder (${...})
default_modelbodystringThe provider's default model; must be one of models[].model
modelsbodyarrayRequired. At least one entry, no duplicate model values; entry shape below

Each models[] entry declares one alias whose id becomes id/model:

FieldTypeDescription
modelstringRequired. Upstream model name
max_context_sizeintegerRequired. Context window in tokens, ≥ 1
display_namestringDisplay name
capabilitiesarrayCapability flags such as thinking or image_in
max_output_sizeintegerMax output tokens, ≥ 1
support_effortsarraySupported Thinking-mode effort levels
adaptive_thinkingbooleanAdaptive thinking toggle

On success, data is the created provider item (same shape as a GET /api/v1/providers item).

  • 40921: a provider with this id already 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.

ParameterInTypeDescription
provider_idpathstringRequired. 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.

ParameterInTypeDescription
provider_idpathstringRequired. Current provider id
new_idbodystringRename 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
typebodystringRequired. Wire protocol: kimi / openai / openai_responses / anthropic / google-genai / vertexai
api_keybodystringTri-state, see above
base_urlbodystringAPI base URL; must not contain an environment variable placeholder (${...})
default_modelbodystringThe provider's default model; must be one of models[].model
modelsbodyarrayRequired. 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 alias
  • 40003: provider is OAuth-managed — log out via POST /api/v1/oauth/logout instead
  • 40412: provider not found
  • 40921: new_id is 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.

ParameterInTypeDescription
provider_idpathstringRequired. 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 via POST /api/v1/oauth/logout instead
  • 40412: 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.

ParameterInTypeDescription
provider_idpathstringRequired. 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 path
  • 40412: 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.

ParameterInTypeDescription
catalog_idbodystringRequired. Directory entry id from GET /api/v1/catalog/providers
idbodystringOverride the catalog id as the local provider id. Same id rules as POST /api/v1/providers
api_keybodystringAPI key for the imported provider
base_urlbodystringOverride 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_id missing or another body validation failure
  • 40003: the target provider exists and is OAuth-managed
  • 40004: the entry cannot be imported (rejected, requires a base_url, has no importable models, or its id is unusable as a provider id)
  • 40417: no directory entry with that catalog_id
  • 50004: 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.

ParameterInTypeDescription
urlbodystringRequired. URL of the registry's api.json
api_keybodystringBearer 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: url missing or another body validation failure
  • 40003: a listed provider exists and is OAuth-managed
  • 40005: 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.

ParameterInTypeDescription
catalog_idpathstringRequired. 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 that catalog_id
  • 50004: 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 pathDescription
POST /api/v1/sessionsCreate a session (requires workspace_id or metadata.cwd)
GET /api/v1/sessionsList 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}/profileRead the session profile
POST /api/v1/sessions/{session_id}/profileUpdate title, metadata, agent config
POST /api/v1/sessions/{session_id}/title/generateGenerate 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}/childrenList child sessions
POST /api/v1/sessions/{session_id}/childrenCreate a child session (fork with a tag)
GET /api/v1/sessions/{session_id}/goalCurrent goal snapshot (null when none)
PATCH /api/v1/sessions/{session_id}/goalEdit the current goal's objective in place
GET /api/v1/sessions/{session_id}/warningsSession-level warnings
GET /api/v1/sessions/{session_id}/environmentRead the main agent's environment binding
GET /api/v1/sessions/{session_id}/environmentsList the environments registered for the session workspace
POST /api/v1/sessions/{session_id}/exportExport 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/changesList the files one turn changed
GET /api/v1/sessions/{session_id}/file-history/contentRead a file as it was at the start or end of a turn
POST /api/v1/sessions/{session_id}/ratingAsk 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.

FieldTypeDescription
idstringSession id (session_...)
workspace_idstringOwning workspace id
titlestringSession title; "" when untitled
created_at / updated_atstringCreation and last-update times, ISO 8601
archivedbooleanWhether the session is archived (hidden from the default session list)
archived_atstringArchive time, ISO 8601; present only when archived
busybooleanAny agent has an active turn or background task
main_turn_activebooleanThe main agent has an active turn
pending_interactionstringnone / approval / question — an unanswered interaction is waiting
last_turn_reasonstringMain agent's latest turn outcome: completed / cancelled / failed
last_promptstringMost recent user prompt text, when present
metadataobjectCustom metadata; always carries cwd (the session's working directory)
agent_configobjectProjected 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
usageobjectToken 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_rulesarraySession permission rules; currently always []
message_countintegerMessage 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.

ParameterInTypeDescription
workspace_idbodystringRequired when metadata.cwd is absent. Registered workspace id; the session is created at that workspace's root
metadatabodyobjectCustom 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
titlebodystringInitial title (at least 1 character); the session is untitled otherwise
agent_configbodyobjectPartial 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: neither workspace_id nor metadata.cwd given, metadata.cwd does not match the workspace root, or agent_config sets tower_mode: true (details lists the field); also when agent_config.model is not configured, agent_config.profile is unknown, or the model does not support agent_config.thinking
  • 40113: no model is available while agent_config sets profile or thinking
  • 40409: the working directory does not exist or is not a directory
  • 40410: no registered workspace with that workspace_id
  • 40913–40918, 40920: agent_config could 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.

ParameterInTypeDescription
before_idquerystringOnly sessions older than this id; mutually exclusive with after_id
after_idquerystringOnly sessions newer than this id; mutually exclusive with before_id
page_sizequeryinteger1–100. When paging applies, the default is 20; see the note above for the unpaginated default behavior
busyquerybooleanKeep only busy (or only idle) sessions
include_archivequerybooleanInclude archived sessions alongside live ones. Default false
archived_onlyquerybooleanKeep only archived sessions; mutually exclusive with include_archive; implies cursor paging even without page_size
exclude_emptyquerybooleanDrop sessions that carry no user prompt
workspace_idquerystringRestrict to one workspace (aliases are resolved)

On success, data is { items, has_more } where each item is the session object.

  • 40001: validation failure — for example before_id combined with after_id, or archived_only combined with include_archive
  • 40410: unknown workspace_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.

ParameterInTypeDescription
session_idpathstringRequired. 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}.

ParameterInTypeDescription
session_idpathstringRequired. 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
titlebodystringNew title (at least 1 character); becomes a custom title
metadatabodyobjectKeys merged into the session's custom metadata
agent_configbodyobjectPartial main-agent config; fields below, all optional

Each agent_config field is applied immediately to the main agent:

FieldTypeDescription
modelstringModel alias id; an empty string is ignored
thinkingstringThinking-mode effort level
permission_modestringmanual / yolo / auto
plan_modebooleanEnter or exit Plan mode
swarm_modebooleanEnter or exit swarm mode
goal_objectivestringCreate a goal with this objective
goal_controlstringpause / 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
forcebodybooleanRegenerate even when a custom or generated title exists. Default false
sourcebodystringTitle input: user_prompts (default) / first_turn / digest

On success, data is { title } — the title now applied to the session.

  • 40401: session not found
  • 40923: generation unavailable — the flag is off, there is no managed OAuth login or no prompt content yet, an existing title without force, 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.

ParameterInTypeDescription
titlebodystringTitle for the fork (at least 1 character). Default Fork: <source title>
metadatabodyobjectCustom metadata for the fork
turn_indexbodyintegerPersistent 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_index is invalid, does not exist, or selects the active turn
  • 40901: 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.

ParameterInTypeDescription
instructionbodystringExtra 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.

ParameterInTypeDescription
turn_idbodystringThe 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_idbodystringThe 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 exist
  • 40901: the agent is working (a running turn, queued input, or a running compaction) — wait for it to finish, then retry
  • 40911: 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
before_idquerystringOnly children older than this id; mutually exclusive with after_id
after_idquerystringOnly children newer than this id; mutually exclusive with before_id
page_sizequeryinteger1–100. Default 100
busyquerybooleanKeep 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
titlebodystringTitle for the child (at least 1 character). Default Child: <source title>
metadatabodyobjectCustom 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.

ParameterInTypeDescription
session_idpathstringRequired. 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
objectivebodystringRequired. 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 found
  • 40914: no current goal
  • 40915: the goal is complete
  • 40917: the objective is empty
  • 40918: 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.

ParameterInTypeDescription
session_idpathstringRequired. 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.

ParameterInTypeDescription
session_idpathstringRequired. 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.

ParameterInTypeDescription
session_idpathstringRequired. 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
web_logbodystringClient log text to include in the archive, at most 256 KB UTF-8
desktopbodybooleanAlso 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
file_idpathstringRequired. 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
turn_numberqueryintegerRequired. 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_number missing or invalid
  • 40401: 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
turn_numberqueryintegerRequired. The turn's turn_number
pathquerystringRequired. File path, as listed by file-history/changes
phasequerystringstart (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_number or path missing or invalid
  • 40401: 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
opbodystringRequired. evaluate / shown / respond / abandon
turn_idbodystringRequired with evaluate: the turn_id of the turn that just completed
appearance_idbodystringRequired with shown / respond / abandon: the id returned by evaluate
responsebodystringRequired 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 pathDescription
GET /api/v1/sessions/{session_id}/messagesPage 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}/turnsPage 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
before_idquerystringOnly messages older than this message id; mutually exclusive with after_id
after_idquerystringOnly messages newer than this message id; mutually exclusive with before_id
page_sizequeryinteger1–100. Default 50
rolequerystringKeep 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 example before_id combined with after_id
  • 40401: session not found

GET /api/v1/sessions/{session_id}/messages/{message_id} ​

Reads one message from the same history by id.

ParameterInTypeDescription
session_idpathstringRequired. Session id
message_idpathstringRequired. 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 found
  • 40403: 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
agent_idpathstringRequired. Agent id; a plain id without path separators
before_ordinalqueryintegerOnly 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_sizequeryinteger1–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 failure
  • 40401: 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 pathDescription
GET /api/v1/sessions/{session_id}/promptsActive and queued prompts
POST /api/v1/sessions/{session_id}/promptsSubmit a prompt (content-part array, optional model / permission-mode overrides)
POST /api/v1/sessions/{session_id}/steerSteer message content directly into the active turn
POST /api/v1/sessions/{session_id}/prompts:steerSteer queued prompts into the active turn
POST /api/v1/sessions/{session_id}/prompts/{prompt_id}:abortAbort a running prompt
POST /api/v1/sessions/{session_id}/prompts/{prompt_id}:steerSteer one queued prompt

GET /api/v1/sessions/{session_id}/prompts ​

Reads the main agent's prompt queue snapshot.

ParameterInTypeDescription
session_idpathstringRequired. 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
contentbodyarrayRequired. Non-empty array of content parts; variants below
agent_idbodystringTarget agent. Default the main agent
prompt_idbodystringClient-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
skillsbodyarrayBundled 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
modelbodystringModel alias to switch the agent to
thinkingbodystringThinking-mode effort level
permission_modebodystringmanual / yolo / auto
swarm_modebodybooleanEnter or exit swarm mode
disabled_toolsbodyarrayTool 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:

PartFieldsDescription
texttextPlain text
image / videosourceMedia 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)
filefile_id, name, media_type, sizeA 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 unknown model, an unsupported thinking level, or a profile field (choose the agent profile with agent_config.profile when creating the session)
  • 40110: no provider configured yet — finish login first
  • 40111: 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_id when known)
  • 40401: session not found
  • 40407: a referenced file_id does not exist (or does not match the part's media kind)
  • 40415: a skills entry names an unknown skill
  • 40912: the skill exists but cannot be activated by the user
  • 40927: prompt_id is 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
prompt_idsbodyarrayRequired. Non-empty array of queued prompt ids

On success, data is { steered: true, prompt_ids }.

  • 40001: validation failure
  • 40401: session not found
  • 40402: 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: ...).

ParameterInTypeDescription
session_idpathstringRequired. Session id
prompt_idpathstringRequired. Prompt id

On success, data is { aborted: true }.

  • 40401: session not found
  • 40402: no prompt with that id
  • 40903: the prompt already completed; data carries { 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
prompt_idpathstringRequired. Queued prompt id

On success, data is { steered: true, prompt_ids: [prompt_id] }.

  • 40401: session not found
  • 40402: 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 pathDescription
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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
interaction_idpathstringRequired. The interaction_id of the interaction frame
kindbodystringRequired. approval or question

An approval takes these fields:

ParameterInTypeDescription
decisionbodystringRequired. approved / rejected / cancelled
scopebodystringWith approved, session (the only value) also remembers the approval rule for the rest of the session
feedbackbodystringFree-form feedback handed back to the agent
selected_labelbodystringThe 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:

ParameterInTypeDescription
answersbodyobjectMap of question item id (from the frame's request.questions) to an answer object; variants below
dismissedbodybooleantrue dismisses the question without answering it
methodbodystringHow the answer was produced: enter / space / number_key / click

Each answer is an object discriminated by kind:

KindFieldsDescription
singleoption_idOne chosen option
multioption_idsSeveral chosen options (at least 1)
othertextA free-text answer
multi_with_otheroption_ids, other_textOptions 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 (details lists each field), or a kind that does not match the interaction
  • 40401: session not found
  • 40404: no interaction with that id in the session
  • 40902: 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 pathDescription
GET /api/v1/sessions/{session_id}/skillsPer-session skill catalog
GET /api/v1/workspaces/{workspace_id}/skillsSession-less skill catalog for a workspace
POST /api/v1/sessions/{session_id}/skills/{skill_name}:activateActivate a skill (starts a turn)
GET /api/v1/toolsList tools of the effective agent
GET /api/v1/mcp/serversList MCP servers
POST /api/v1/mcp/servers/{mcp_server_id}:restartRestart 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.

ParameterInTypeDescription
session_idpathstringRequired. 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.

ParameterInTypeDescription
workspace_idpathstringRequired. 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: ...).

ParameterInTypeDescription
session_idpathstringRequired. Session id
skill_namepathstringRequired. Name of the skill to activate
pathbodystringOptional exact SKILL.md path from the session catalog, used to select a same-name variant. Must match an entry returned by the catalog
argsbodystringFree-form arguments handed to the skill, like the text after a slash command
prompt_idbodystringClient-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
attachmentsbodyarrayMedia 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 suffix
  • 40401: session not found (or not activated)
  • 40407: a referenced attachment file does not exist
  • 40415: no skill with that name
  • 40912: the skill exists but its type cannot be activated by the user
  • 40927: prompt_id is 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.

ParameterInTypeDescription
session_idquerystringSession 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.

ParameterInTypeDescription
mcp_server_idpathstringRequired. MCP server id (its configured name)

On success, data is { restarting: true }.

  • 40001: missing or unknown action suffix
  • 40408: 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 pathDescription
GET /api/v1/capabilitiesList built-in capabilities with readiness status
GET /api/v1/capabilities/{capability_id}Read one capability's status
POST /api/v1/capabilities/{capability_id}:installStart a capability install (background; poll GET for progress)
GET /api/v1/pluginsList installed plugins
GET /api/v1/plugins/{plugin_id}Read an installed plugin and its declared capabilities
POST /api/v1/pluginsInstall a custom plugin or a marketplace entry at an expected version
POST /api/v1/plugins/previewInspect a marketplace entry's capabilities without installing it
GET /api/v1/plugins/marketplaceMarketplace 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.

ParameterInTypeDescription
capability_idpathstringRequired. 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.

ParameterInTypeDescription
capability_idpathstringRequired. Capability id

On success, data is the capability status object documented under GET /api/v1/capabilities above.

  • 40001: missing or unknown action suffix
  • 40418: no capability with that id
  • 40924: an install of this capability is already running
  • 40925: 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 resolves source, 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 declare sha256sum, expectedSha256 is required and must match the catalog. Custom entries without a version may omit expectedVersion; local-directory and GitHub entries do not require expectedSha256.

On success, data is the plugin summary documented under GET /api/v1/plugins above.

  • 40001: validation failure — for example source is neither a URL nor an absolute path, or the plugin failed to load
  • 40409: the local path does not exist
  • 40934: 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 invalid
  • 40419: no marketplace entry with that id
  • 40934: 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: ...).

ParameterInTypeDescription
plugin_idpathstringRequired. Installed plugin id

On success, data is { ok: true }.

  • 40001: missing or unknown action suffix
  • 40419: 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.

ParameterInTypeDescription
plugin_idpathstringRequired. Installed plugin id

On success, data is { ok: true }.

  • 40001: missing or unknown action suffix
  • 40419: no installed plugin with that id

POST /api/v1/plugins/{plugin_id}:remove ​

Removes an installed plugin; the dispatch contract matches :enable above.

ParameterInTypeDescription
plugin_idpathstringRequired. Installed plugin id

On success, data is { ok: true }.

  • 40001: missing or unknown action suffix
  • 40419: 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 pathDescription
GET /api/v1/workspacesList registered workspaces
POST /api/v1/workspacesRegister 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}/primaryPromote a source folder to the primary directory
GET /api/v1/workspaces/project-config-dirsRead 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}/trustRead the trust state
POST /api/v1/workspaces/{workspace_id}/trustGrant trust
POST /api/v1/workspaces/{workspace_id}/untrustRevoke 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.

FieldTypeDescription
idstringWorkspace id: wd_<slug>_<hash12> for implicit registration (CLI/VS Code entering a directory), wm_<uuid> when created via POST /api/v1/workspaces
rootstringAbsolute path of the primary directory
namestringDisplay name, 1–100 characters; defaults to the primary directory's base name
kindstringWorkspace 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_dirsstring[]Registry-managed extra source folders as absolute paths (excludes project local.toml and ephemeral sources)
created_atstringRegistration time, ISO 8601
last_opened_atstringLast time the workspace was opened or re-registered, ISO 8601
session_countintegerNumber 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.

ParameterInTypeDescription
rootbodystringRequired. Absolute path of an existing directory, used as the primary directory
namebodystringDisplay name, 1–100 characters. Default the primary directory's base name
additional_dirsbodystring[]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_keybodystringClient-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: root is missing or not an absolute path, or another request validation failure (details lists the field)
  • 40006: additional_dirs contains duplicates or the primary directory itself. msg is Duplicate source folder: <actual path>; details is [{ "path": "additional_dirs.N", "directory": "<resolved absolute path>" }], where path locates the request field and directory is the duplicate directory
  • 40409: root or a path in additional_dirs does 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.

ParameterInTypeDescription
workspace_idpathstringRequired. Workspace id
namebodystringNew display name, 1–100 characters
additional_dirsbodystring[]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 (details lists each field)
  • 40006: duplicate entries or an entry equal to the primary directory; msg, details.path, and details.directory have the same meaning as for POST /api/v1/workspaces
  • 40409: a path in additional_dirs does not exist or is not a directory
  • 40410: 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.

ParameterInTypeDescription
workspace_idpathstringRequired. Workspace id
pathbodystringRequired. 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: path is not in the dir set
  • 40409: path does not exist or is not a directory
  • 40410: 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.

ParameterInTypeDescription
rootquerystringRequired. Absolute path of the project primary directory

On success, data is { additional_dirs: string[] }.

  • 40001: root is missing or empty

DELETE /api/v1/workspaces/{workspace_id} ​

Unregisters a workspace. Only the registry entry is removed — the on-disk directory is untouched.

ParameterInTypeDescription
workspace_idpathstringRequired. 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.

ParameterInTypeDescription
workspace_idpathstringRequired. 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.

ParameterInTypeDescription
workspace_idpathstringRequired. 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.

ParameterInTypeDescription
workspace_idpathstringRequired. 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 pathDescription
POST /api/v1/workspace/fs:listSession-less workspace directory listing
POST /api/v1/workspace/fs:searchSession-less workspace search (the body carries the workspace reference)
POST /api/v1/workspace/fs:suggestSession-less file-completion candidates (for @ file mentions)
POST /api/v1/fs:suggestFile-completion candidates across absolute root directories, without a session or workspace
GET /api/v1/sessions/{session_id}/fs/{path}:downloadDownload a session file (binary, see below)
GET /api/v1/fs:browseList host directories (folder picker)
GET /api/v1/fs:homeThe user's home directory and recent workspaces
GET /api/v1/fs:contentRaw bytes of any host file (gated only by the token — be careful when exposing the port)
POST /api/v1/fs:mkdirCreate 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
pathbodystringDirectory to list, relative to the session work directory. Default .
depthbodyintegerRecursion depth, 1–10. Default 1
limitbodyintegerMaximum entries, 1–1000. Default 200
show_hiddenbodybooleanInclude dotfiles. Default false
follow_gitignorebodybooleanSkip gitignored paths. Default true
exclude_globsbodystring[]Additional globs to skip
allow_ignored_globsbodystring[]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
sortbodystringtype_first (default) / name_asc / name_desc / mtime_desc / size_desc
include_git_statusbodybooleanAttach 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 failure
  • 40401: session not found
  • 40409: path not found (including a path that 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
pathbodystringRequired. File path, relative to the session work directory
offsetbodyintegerByte offset to start at. Default 0
lengthbodyintegerBytes to read, 1–10485760 (10 MiB). Default 1048576 (1 MiB)
encodingbodystringauto (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 failure
  • 40401: session not found
  • 40409: path not found
  • 40906: path is a directory
  • 40907: binary file requested with encoding: "utf-8"
  • 41302: file exceeds the 10 MiB read ceiling
  • 41304: 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
pathsbodystring[]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 failure
  • 40401: session not found

POST /api/v1/sessions/{session_id}/fs:stat ​

Stats one path in the session workspace.

ParameterInTypeDescription
session_idpathstringRequired. Session id
pathbodystringRequired. Path to stat, relative to the session work directory

On success, data is the entry object described under fs:list.

  • 40001: body validation failure
  • 40401: session not found
  • 40409: path not found
  • 41304: 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
pathsbodystring[]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 failure
  • 40401: session not found

POST /api/v1/sessions/{session_id}/fs:mkdir ​

Creates a directory inside the session workspace.

ParameterInTypeDescription
session_idpathstringRequired. Session id
pathbodystringRequired. Directory to create, relative to the session work directory
recursivebodybooleanCreate missing parent directories. Default false

On success, data is the created directory's entry object (as described under fs:list).

  • 40001: body validation failure
  • 40401: session not found
  • 40409: 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id, or a workspace reference
querybodystringRequired. Search text; "" lists the top level
limitbodyintegerMaximum hits, 1–200. Default 50
include_globsbodystring[]Only paths matching one of these globs
exclude_globsbodystring[]Skip paths matching these globs
follow_gitignorebodybooleanSkip 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 failure
  • 40401: 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
patternbodystringRequired. Text or regex to search for
regexbodybooleanTreat pattern as a regular expression. Default false
case_sensitivebodybooleanDefault true
include_globsbodystring[]Only files matching one of these globs
exclude_globsbodystring[]Skip files matching these globs
follow_gitignorebodybooleanSkip gitignored paths. Default true
max_filesbodyintegerFiles to scan at most, 1–10000. Default 200
max_matches_per_filebodyintegerMatches kept per file, 1–10000. Default 50
max_total_matchesbodyintegerMatches kept overall, 1–100000. Default 5000
context_linesbodyintegerContext 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 failure
  • 40401: session not found
  • 41305: 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
rootbodystringWorkspace directory to scope to. When omitted, status covers the whole Git worktree; a relative value is resolved from the session work directory
pathsbodystring[]Restrict the status to these paths. Relative paths are resolved from root when provided, otherwise from the session work directory
include_pull_requestbodybooleanInclude 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 failure
  • 40401: session not found
  • 40908: 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
rootsbodystring[]Required. Workspace directories to inspect, 1–32 entries
include_pull_requestbodybooleanInclude 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 failure
  • 40401: session not found

POST /api/v1/sessions/{session_id}/fs:pull_request ​

Reads the pull request associated with a session workspace directory.

ParameterInTypeDescription
session_idpathstringRequired. Session id
rootbodystringWorkspace 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 failure
  • 40401: session not found
  • 40908: 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
rootsbodystring[]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 failure
  • 40401: session not found

POST /api/v1/sessions/{session_id}/fs:diff ​

Returns the unified git diff of one file in a session workspace directory.

ParameterInTypeDescription
session_idpathstringRequired. Session id
pathbodystringRequired. File to diff, relative to root
rootbodystringWorkspace 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 failure
  • 40401: session not found
  • 40908: 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
pathbodystringRequired. File to open, relative to the session work directory
linebodyintegerLine number to jump to where the handler supports it (positive integer)

On success, data is { opened: true }.

  • 40001: body validation failure
  • 40401: session not found
  • 40409: path not found
  • 41304: 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
app_idbodystringRequired. Target application: finder / cursor / vscode / iterm / terminal
pathbodystringRequired. File or directory to open, relative to the session work directory
linebodyintegerLine number to jump to where the application supports it (positive integer)

On success, data is { opened: true }.

  • 40001: body validation failure
  • 40401: session not found
  • 40409: path not found
  • 41304: path escapes the session workspace
  • 50001: 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
pathbodystringRequired. File to reveal, relative to the session work directory

On success, data is { revealed: true }.

  • 40001: body validation failure
  • 40401: session not found
  • 40409: path not found
  • 41304: 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.

ParameterInTypeDescription
session_idpathstringRequired. Session id
pathpathstringRequired. Workspace-relative file path plus the :download suffix
allow_external_symlinksquerystringtrue to download through symlinks outside the workspace. Default false
  • 40001: missing or empty path
  • 40401: session not found
  • 40409: path not found
  • 41304: 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.

ParameterInTypeDescription
workspacebodystringRequired. Registered workspace id or absolute root (registered on the spot)
pathbodystringDirectory to list, relative to the workspace root. Default .
depthbodyintegerRecursion depth, 1–10. Default 1
limitbodyintegerMaximum entries, 1–1000. Default 200
show_hiddenbodybooleanInclude dotfiles. Default false
follow_gitignorebodybooleanSkip gitignored paths. Default true
exclude_globsbodystring[]Additional globs to skip
allow_ignored_globsbodystring[]Globs to show even when gitignored
allow_external_symlinksbodybooleanFollow symlinked directories whose targets are outside the workspace. Default false
sortbodystringtype_first (default) / name_asc / name_desc / mtime_desc / size_desc

On success, data has the same shape as fs:list.

  • 40001: body validation failure
  • 40409: path not found or not a directory
  • 40410: workspace not found and not a usable absolute path
  • 40411: permission denied by the host filesystem
  • 41304: path escapes the workspace through a symlink and allow_external_symlinks is 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.

ParameterInTypeDescription
workspacebodystringRequired. Registered workspace id or absolute root (registered on the spot)
querybodystringRequired. Search text; "" lists the top level
limitbodyintegerMaximum hits, 1–200. Default 50
include_globsbodystring[]Only paths matching one of these globs
exclude_globsbodystring[]Skip paths matching these globs
follow_gitignorebodybooleanSkip 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 failure
  • 40410: 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.

ParameterInTypeDescription
workspacebodystringRequired. Registered workspace id or absolute root (registered on the spot)
querybodystringRequired. Partial path text to complete
limitbodyintegerMaximum candidates, 1–200. Default 50; fd-backed searches return at most 20
follow_gitignorebodybooleanSkip gitignored paths when true. Default false
show_hiddenbodybooleanInclude dotfiles. Default false; fd-backed searches always include them
streambodybooleanReturn NDJSON preview/final events when true. Default false
include_globsbodystring[]Only paths matching one of these globs
exclude_globsbodystring[]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 failure
  • 40410: 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:

ParameterInTypeDescription
rootsbodystring[]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 path
  • 40409: a root does not exist or is not a directory (msg names it)

GET /api/v1/fs:browse ​

Lists the subdirectories of one host directory — the backend of the folder picker.

ParameterInTypeDescription
pathquerystringAbsolute 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: path is not absolute
  • 40409: path not found
  • 40411: 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.

ParameterInTypeDescription
pathquerystringRequired. Absolute file path
  • 40001: path is not absolute, or not a regular file
  • 40409: path not found
  • 40411: permission denied
  • 40906: 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.

ParameterInTypeDescription
pathbodystringRequired. Absolute directory path

On success, data is { path }.

  • 40001: path is not absolute
  • 40409: parent path not found
  • 40411: permission denied
  • 40919: path already exists

File uploads ​

Method and pathDescription
POST /api/v1/filesMultipart 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).

ParameterInTypeDescription
filebodybinaryRequired. The multipart file part
namebodystringStored display name. Default the uploaded filename
expires_in_secbodynumberSeconds 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 no file field

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.

ParameterInTypeDescription
file_idpathstringRequired. 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.

ParameterInTypeDescription
file_idpathstringRequired. 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 pathDescription
GET /api/v1/gui/store/lengthNumber of stored keys
GET /api/v1/gui/store/getItemRead a value by key
POST /api/v1/gui/store/setItemWrite a value by key
POST /api/v1/gui/store/removeItemDelete a value by key
POST /api/v1/gui/store/clearDelete 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).

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

ParameterInTypeDescription
keybodystringRequired. Key to write, 1–256 characters
valuebodystringRequired. Value to store

On success, data is null.

POST /api/v1/gui/store/removeItem ​

Deletes one value (mirrors localStorage.removeItem).

ParameterInTypeDescription
keybodystringRequired. 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 pathDescription
POST /api/v1/searchCross-session full-text search; mode is terms (default) or literal (exact substring); page_token pagination
GET /api/v1/connectionsList live WebSocket connections
GET /api/v2/sessionsNext-generation session list, see below
POST /api/v2/sessions:archiveBatch-archive sessions, see below
POST /api/v2/sessions:restoreBatch-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.

ParameterInTypeDescription
querybodystringRequired. Search text
modebodystringterms (default) / literal
opbodystringTerm combiner in terms mode: AND (default) / OR
containerbodyobjectRestrict the search to { session_id?, agent_id? }
rolebodystringRestrict to user / assistant / title hits
start_timebodyintegerOnly hits at or after this time (epoch milliseconds)
end_timebodyintegerOnly hits at or before this time (epoch milliseconds)
sortbodystringscore (default) / time_desc / time_asc; ignored by literal mode, which always returns newest-first
page_sizebodyintegerHits per page, 1–50. Default 20
page_tokenbodystringToken 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:

ParameterDescription
workspace.idFilter by workspace; repeatable
activity.statusFilter by activity status: running / approval / question / failed / idle; repeatable
meta.updated_afterOnly sessions updated after this time (epoch milliseconds)
meta.updated_beforeOnly sessions updated before this time (epoch milliseconds)
meta.archivedtrue / false (default) / all
meta.has_prompttrue keeps only sessions that carry a user prompt, false keeps only empty ones (the exclude_empty equivalent of GET /api/v1/sessions)
viewflat (default) / by_workspace, see below
group.page_sizeSessions 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)
sortmeta.updated_at_desc (default) / meta.updated_at_asc / meta.created_at_desc
includeComma-separated extra field groups; currently only git (branch and PR info, deduplicated per environment and working directory and cached for 60 seconds)
fieldsComma-separated item projection; currently only id,archived, trimming each item to { id, archived } (select-all-matching flows). Not combinable with include=git (40001)
page_size1–100, default 50; up to 10000 with the id,archived projection. Under view=by_workspace it counts groups per page
page_tokenPagination token from the previous page
pageStateless 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:

json
{
  "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.

json
{
  "code": 0,
  "msg": "success",
  "data": {
    "results": [
      { "id": "session_a", "ok": true },
      { "id": "session_b", "ok": false, "error": { "code": 40401, "message": "session session_b does not exist" } }
    ],
    "succeeded": 1,
    "failed": 1
  },
  "request_id": "req_..."
}

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 pathDescription
GET /api/v2/mcp/serversList every known MCP server
GET /api/v2/mcp/servers/{name}Get one server by runtime name
POST /api/v2/mcp/serversAdd 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:testProbe a real connection to one server
POST /api/v2/mcp/servers:inspectLocator-addressed catalog with a batched connection probe
GET /api/v2/mcp/auth-statusesPer-server OAuth state over the catalog
POST /api/v2/mcp/auth:beginBegin an interactive OAuth flow
POST /api/v2/mcp/auth:completeAwait the browser callback and finish the code exchange
POST /api/v2/mcp/auth:cancelTear down a begun OAuth flow
POST /api/v2/mcp/auth:resetClear 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.

ParameterInTypeDescription
namepathstringRequired (get only). Runtime name of the server
cwdquerystringInclude 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) or plugin (a plugin manifest)

  • origin: where the entry is defined — a file path or a plugin id

  • mutable: only user-level entries are mutable; plugin and project-layer entries are read-only

  • config: 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 values

  • plugin: { id, name }, present on plugin entries

  • 40001: validation failure

  • 40408: 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-only
  • 40408: (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.

ParameterInTypeDescription
namebodystringRuntime name of a registry entry
serverbodyobjectInline server config to probe as-is
cwdbodystringProject 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 servers
  • 40408: 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.

ParameterInTypeDescription
targetsbodyarrayLocators narrowing the catalog; omitted inspects all servers
cwdbodystringInclude 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 failure
  • 40408: a targets locator 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.

ParameterInTypeDescription
cwdquerystringInclude the project layers of this (trusted) directory
verifyquerystringtrue 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 unknown flowId on :complete, or a server that cannot do OAuth (stdio transport, a static bearer token, or static headers without auth: "oauth") on :begin
  • 40408: (:begin / :reset) the locator matches nothing
  • 40929: 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.

FrameFieldsDescription
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.*, and notice (see Conversation protocol), task.list, task, agent, usage, goal, todo; interaction frames go to every subscriber of the session. task.output goes only to connections watching that task with task.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 }. status is running while the agent works on a turn and idle when it does not. compacting is true while 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. compacting and 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? }, where goal is 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_id is unique within the session and never reused: a turn opened after an undo gets a new id even when it reuses the ordinal of an undone turn. ordinal is the turn's position in the agent's current history and orders the turns; older pages are read with before_ordinal. turn_number is the engine's turn number, used by the file-history endpoints.
  • kind is agent, or system for a turn that holds a single system item between agent turns: a completed context compaction, or the fork boundary that opens the conversation of a forked agent. A system turn is never running; it arrives as turn.started, item.ended, and turn.ended back to back.
  • status is running or ended; turns the agent does not run, such as a prompt a hook rejected, are already ended. An ended turn has an outcome: completed; interrupted with the end_reason user_cancelled (the user stopped it) or aborted (it stopped for another reason, such as the server stopping while it ran); or failed with the end_reason error, max_steps, filtered (the provider's content filter blocked the output), or blocked (a hook or a policy rejected the input). error is { code, message, name?, status_code?, request_id?, retryable? } on failed turns.
  • retry is { 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; attempt is the attempt that just failed. It clears when the next step starts, when a step ends, and when the turn ends.
  • steps lists the ended steps (model requests) by index as { index, outcome, started_at, ended_at?, finish_reason?, usage? }. A step's outcome is completed, interrupted, or failed; usage is { input_other, output, input_cache_read, input_cache_creation }. The turn's usage sums 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_ids lists the prompt ids the input carries — including the prompt_id of a bundled skill submission or a skill activation — so a client can retire its optimistic bubble; it is empty for inputs the system triggered. origin is { 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" }. text joins 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. attachments lists 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 carry file_id, inline and remote media carry url, media the server stored is sent as a data: URL in pages, and a file attached by a server-local path carries only its name, media_type and size. skills lists the skills activated with the input as { name, path?, args? }, and metadata echoes the metadata sent with the prompt. A CLI shell command (!) and its output arrive as raw text in a user_message with origin: { kind: "system" }, each in its own ended turn.
  • assistant_message: { text }.
  • reasoning: { text, started_at, duration_ms? }; duration_ms is 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, stage is input while the model streams the arguments (input_text holds them as text), awaiting_approval while its approval is pending, and executing otherwise; output streams the tool's live output and fields is the latest handler field snapshot as a JSON object. Once ended, output is the text of the result and media lists its images, videos, and audio as { kind, file_id?, url?, media_type? }; the outcome is failed, with error instead of output, when the result is an error, and interrupted when the turn ended before a result. note is the extra text the tool attached to its result for the model, such as the size and dimensions of an image read by ReadMediaFile. task_id appears once the call's work runs as a background task, whether it started in the background or was moved there later; it keeps working with task.watch and task.stop after the task has left task.list. While a foreground call runs, find its task through the task's parent_tool_call_id. interaction_id links the approval or question raised for the call, whose content comes with the interaction frames, and agent_refs lists the subagents the call started as { agent_id, role: "child" | "member", swarm_index?, task_id? }. ExitPlanMode calls carry the display { 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. answer is the user's reply to a background question: answers maps each question to its answer, and note is 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 (the removed_turn_ids of the undo response), and every turn after a context clear. The client removes them by turn_id; the undo response and this frame may arrive in either order.

  • turn.started and turn.ended carry turn without items and replace the client copy of the turn, keeping its items; turn.ended is sent again when an ended turn is corrected.

  • turn.updated carries turn_id, step once per ended step (write it into steps by index), and retry, which replaces the turn's retry state; an absent retry clears it.

  • item.started and item.ended carry turn_id and the full item, and replace the client copy with the same item_id; an unknown item is appended to its turn. Items that do not stream, such as user_message, only get item.ended. A streamed item stays running until its step is persisted, then gets a single item.ended with 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.updated carries turn_id, item_id, and set, the fields of a running tool_call that changed; merge it shallowly into the item.

  • item.delta carries turn_id, item_id, field (text, input_text, or output), and delta, text to append to that field.

  • item.removed carries turn_id and item_id: a running item that will not be finished, for example after a request retry restarts the step.

  • notice is { level: "info" | "warning" | "error", title, detail?, code? }, a live warning or error raised by the agent, for example the code compaction.cancelled when 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 pathDescriptionRange (206)ETag / 304
GET /api/v1/files/{file_id}Download an uploaded fileYesNo (sends an etag header but ignores If-None-Match)
GET /api/v1/sessions/{session_id}/fs/{path}:downloadDownload a session workspace fileYesYes
GET /api/v1/fs:contentRaw bytes of any host file (gated only by the token — be careful when exposing the port)YesYes
POST /api/v1/sessions/{session_id}/exportExport the session with diagnostics (zip stream)NoNo

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 ​