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_cursorandobserved_at. A cursor is opaque and bound to the account and the list it pages. - Idempotency. A mutation takes an
Idempotency-Keyheader (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
202with an operation instead of the finished result (ask for it withPrefer: 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.