Skip to content
last verified against v0.1.26 verified 2026-10-16

API

Everything the ks client does goes through the customer API at https://ctl.keepstate.ai/api/v2, and you can call it directly: projects, sessions, agents, tasks, approvals, results, keys, advisers, Cruise proposals and more. The service documents the API itself; this page tells you how to use it.

The contract

curl -s https://ctl.keepstate.ai/api/v2/openapi.json

The contract is OpenAPI 3.1, rendered from the same route table that serves the API, so a route in it is a route the service has. The service's statement of what it can do, with every capability available or not and the reason, is at GET /api/v2/capabilities. Neither needs sign-in.

Authentication

Every call carries a bearer token: Authorization: Bearer ksk_…. The token is the one ks login stored for you, in a file only you can read under your config directory. Treat it as a password. The console's browser sign-in does not reach /api/v2.

Rules every call follows

  • One envelope. Every answer is the v2 envelope, and every list carries items, next_cursor and observed_at. A cursor is opaque and bound to the account and the list it pages.
  • Idempotency. A mutation takes an Idempotency-Key header (at most 200 characters) and leaves an operation record. When a reply is lost, read the record (GET /api/v2/operations/{id}, by its id or by your key) instead of sending again.
  • Accepted as an operation. A route may answer 202 with an operation instead of the finished result (ask for it with Prefer: respond-async); read the operation until it ends.
  • Revisions. A change to a record names the revision it was prepared against (expected_revision); a record that moved is refused rather than overwritten.
  • Strict bodies. Request bodies are decoded strictly: an unknown field is refused.
  • Plans, then digests. A destructive or binding step (a deletion, a fork, a migration, a Cruise proposal's approval) first shows a plan or a preview, and is carried out by naming what you were shown.

Errors

An error is the same envelope with an error object: a stable code, a type, a message, whether it is retryable, whether any work started (work_started: no, yes or unknown), and the next_action to take. Read work_started before you retry: when it is yes or unknown, read the operation record first.

Project roles

A project's members hold one of three roles: owner, team or reader (/api/v2/projects/{id}/members). Each route in the contract names the least role it needs in x-ks-role.

Protocol versions

Declare the protocol your client speaks in the X-KS-Protocol header; the header is stated in GET /api/v2/capabilities (protocol), not as a parameter in the contract. The service's own rule: an operation newer than a declared protocol is refused by name (ks_client_too_old), never answered in a shape the client cannot read; a client that declares nothing is served as before. The current protocol and the oldest one served are in GET /api/v2/capabilities.

The journal

Each session keeps an ordered journal of everything that happened in it: the agents' conversations, tasks, approvals, results and lifecycle. GET /api/v2/sessions/{id}/events reads it after a sequence number or a cursor; GET /api/v2/sessions/{id}/events/stream follows it as server-sent events (hello, event, heartbeat, end). A heartbeat means the connection is alive, not that a model is working. The ks app is built on this stream.

What is not public

The engine's own daemon routes (session lifecycle on the fleet hosts, the gateway) are not a user contract and are not documented here; they are KeepState's to run. The older /api routes the first clients used stay under the legacy policy the contract names. Anything the contract does not list is not supported.

Spotted an error on this page? Tell us, and we will correct it.

Report it
← previous CLI reference next → Troubleshooting