Developers
K2 Server API
Every K2 server exposes an HTTP API over its own your-name.k2.dev subdomain: message your agents, spawn fresh live agent sessions in any workspace, and read their answers back — from CI, from your own apps, from anywhere. The agent working the session replies with k2 respond; you read the conversation with a plain GET.
The API is agent-agnostic: workspaces configured for Claude Code, Codex, Gemini, Grok, Pi, Cursor, Hermes, or your own custom agent all speak the same endpoints. See the K2 agent contract for what a custom agent needs to participate.
K2_API=1), reachable through a Pro subdomain — API calls over the tunnel are a Pro feature. Host-session listing/resume: v0.40.30+. Capability envelope / public JWKS: v0.40.76+. Host-session kill: v0.40.79+. Work-completion reaper (no hard wall on busy agents): v0.40.81+.k2sk_… opens doors → a host-session is a durable conversation handle → the agent answers only via k2 respond / k2 respond --final (you drain with GET messages) → multi-turn apps use capability JWTs + public JWKS → stop spend with POST …/killand by not reminting caps. Three different "sessions" exist: the workspace's canonical agent (/message), API host-sessions (this page's main path), and Dedicated sandboxes (microVM).Authentication
Create an API key on the server (K2 app → Settings → API Keys, or over the owner CLI). Keys look like k2sk_… and are shown once at creation — K2 stores only a hash. Present the key on every request; the Authorization header is preferred (keeps it out of URLs and logs), ?token= works as a curl fallback.
curl https://your-name.k2.dev/v1/ping \
-H "Authorization: Bearer k2sk_XXXXXXXXXXXXXXXXXXXX"Create an API key
/cli/api-keys/createMint a k2sk_… key programmatically. This is an owner-tier operation: it needs an owner daemon token or an Owner-role connect session. An existing k2sk_ API key can never create more keys.
Gotcha: the owner token goes in the ?token= query param, not the Authorization header — this endpoint reads the query param only.
curl -X POST "https://your-name.k2.dev/cli/api-keys/create?token=<owner-token>" \
-H "Content-Type: application/json" \
-d '{
"label": "ci-runner",
"workspaces": ["my-project", "docs-site"],
"provider": "anthropic",
"capabilities": { "hostSessions": true, "canonicalMessage": false, "sandboxes": false }
}'All body fields are optional:
label — a string tag for the key. workspaces — "*" for all, or an array like ["ws-a","ws-b"]. ⚠ If you omit this, the key is fail-closed and can reach zero workspaces — you almost always must set it. provider — anthropic (default), openai, google, or xai; an unknown provider returns 400. anthropicKey — a bring-your-own LLM credential (see below). baseUrl — an override for openai-compatible endpoints. capabilities — { hostSessions, canonicalMessage, sandboxes } booleans; defaults are hostSessions on, the others off.
// 200
{
"id": "e7c1a0d4-…",
"key": "k2sk_XXXXXXXXXXXXXXXXXXXX",
"capabilities": { "hostSessions": true, "canonicalMessage": false, "sandboxes": false }
}The raw k2sk_ key is returned exactly once and is unrecoverable — K2 stores only a hash. Save it now; if you lose it, mint a new one.
CLI equivalent: k2 api-keys create --label … --workspaces "*"|a,b --provider … --anthropic-key … --allow … --deny ….
Bring your own LLM key
Pass anthropicKey(or the provider-appropriate credential) at key creation. K2 stores it per-key and, on every host-session spawn made with that key, injects it into the agent's process environment — so the agent runs on yourLLM billing, not the server's. The credential is set at create time only and is never logged.
Which environment variables get staged depends on the key's provider:
anthropic → ANTHROPIC_API_KEY
openai → OPENAI_API_KEY (+ OPENAI_BASE_URL if baseUrl is set)
google → GEMINI_API_KEY, GOOGLE_API_KEY
xai → XAI_API_KEYCapability discovery
/v1/pingLiveness plus what this server has enabled — probe this first and feature-gate your client on it.
{
"ok": true,
"api": {
"enabled": true,
"hostSessions": true, // spawn/list/resume live agent sessions
"sandboxes": "none" // "microvm" on Dedicated servers
}
}Message a workspace's agent
/v1/w/<workspace>/messageDeliver a message into the workspace's canonical agent session (the same session you see in the app), waking it if needed. <workspace> is the workspace name as shown in K2.
curl -X POST https://your-name.k2.dev/v1/w/my-project/message \
-H "Authorization: Bearer k2sk_..." \
-H "Content-Type: application/json" \
-d '{"text": "Summarize today's failing tests."}'Host sessions — spawn agents that answer back
Host sessions are fresh, API-owned agent sessions: each spawn boots the workspace's configured agent in a real terminal, briefs it that an API caller is listening, and gives it a private scoped token so its k2 respond output lands in your conversation — and only yours. One session is one conversation; for concurrent requests, spawn concurrent sessions.
/v1/w/<workspace>/host-sessionsSpawn a session with an initial prompt. The same endpoint doubles as resume: pass "session" with a previously returned id and the message is delivered into that conversation. If the session is still live it lands in the running agent (no double-boot); if it has exited, the agent is relaunched resuming the same conversation. Only claude/grok-class providers are resumable.
// request — fresh spawn
{
"prompt": "Run the test suite and report failures.",
"timeout_secs": 600
}
// request — resume an earlier conversation (field name is "session")
{ "session": "6f0c9a52-…", "prompt": "Now fix the first failure." }
// 200 — fresh spawn
{
"sessionId": "6f0c9a52-…",
"agentName": "api-owner-8f3b1c20-…",
"workspace": "my-project",
"sandbox": "none",
"stream": { "grid": "/cli/sessions/grid?session=…&token=k2st_…" }
}
// 200 — resume of a still-live session
{ "sessionId": "6f0c9a52-…", "delivered": true, "live": true, "resumed": true }agentName is api-<principal>-<uuidv4>: api-owner-<uuid> when spawned with an owner token, or api-<keyId>-<uuid> when spawned with an API key.
Lifecycle (v0.40.81+). While the agent is working, the daemon does not kill it for silence or for wall-clock age. After the agent runs k2 respond --final, the session enters a short grace window (~10s) and is then reaped. A new message into the same live session cancels grace and marks it working again. Optional timeout_secs (default 180, clamp 30..86400) is a client / JWT budget bound — not a hard wall on a busy agent. Your app should poll or subscribe with its own deadline, and use POST …/kill (below) to force-stop spend or stuck sessions.
Safety: auto-approve is on by default. Agents spawned over the API run with auto-approve unless a workspace owner opts out — api_skip_permissions is unset-means-on (a migration backfilled it). So an API caller gets an agent that can act without permission prompts. Opting a workspace out strips its auto-approve/danger flags and restores prompts.
// owner opt-out: value must be exactly "0" or "1"
// "1" = on (keep auto-approve) "0" = opt out (strip auto-approve/danger flags)
curl -X POST "https://your-name.k2.dev/cli/workspace/set?token=<owner-token>" \
-H "Content-Type: application/json" \
-d '{
"project": "/abs/path/to/my-project",
"fields": { "api_skip_permissions": "0" }
}'CLI: k2 workspace api-skip-permissions get <workspace> and k2 workspace api-skip-permissions set <workspace> on|off. It is also settable in the app's workspace Settings.
/v1/w/<workspace>/host-sessionsList this workspace's API-spawned sessions.
{
"workspace": "my-project",
"sessions": [
{ "sessionId": "6f0c9a52-…", "agentName": "api-owner-…", "live": true, "lastSeenAt": "…" }
]
}/v1/w/<workspace>/host-sessions/<id>Send a follow-up message into a live session's terminal.
// request
{ "prompt": "Now fix the first failure." }
// 200
{ "sessionId": "6f0c9a52-…", "delivered": true, "live": true }/v1/w/<workspace>/host-sessions/<id>/messages?since=<seq>Read the agent's replies. seq is monotonic per session and reads are non-destructive — keep your last latest_seq as a cursor and poll. Entries with "final": true are the agent's k2 respond --final answers. The terminal grid stream (below) is optional spectacle; the message drain is the contract.
{
"messages": [
{ "seq": 1, "text": "3 tests failing in auth.rs — details follow.", "ts": 1783460000, "final": false },
{ "seq": 2, "text": "Fixed. All 214 tests pass.", "ts": 1783460420, "final": true }
],
"latest_seq": 2
}/v1/w/<workspace>/host-sessions/<id>/killForce-stop a live host-session PTY (integrator spend control / stuck session). Same auth and workspace rules as message-live. Does not require a grid WebSocket. Empty body is fine. Requires server v0.40.79+.
curl -X POST https://your-name.k2.dev/v1/w/my-project/host-sessions/6f0c9a52-…/kill \
-H "Authorization: Bearer k2sk_…"// 200 — was live
{ "sessionId": "6f0c9a52-…", "killed": true }
// 200 — owned but already dead (idempotent)
{ "sessionId": "6f0c9a52-…", "killed": false, "reason": "not_live" }
// 404 — unknown, unowned, or wrong workspace (uniform; no existence oracle)Capability envelope (multi-turn apps)
For multi-turn or long-running agents, pass a capabilities array at spawn (and re-send on each resume/message when you remint). The daemon mints short-lived capability JWTs the agent uses to call your app — not a substitute for the k2sk_control key. Verify tokens against the server's public JWKS:
/v1/jwksPublic ES256 JWKS (no auth). Use this to verify capability JWTs your agent presents to your backend. Requires a server with the envelope path enabled (v0.40.76+; JWKS public even when some API doors are gated).
curl https://your-name.k2.dev/v1/jwksFull wire samples, remint rules, and resource namespaces: host-session capability envelope.
Streaming: the grid WebSocket
A spawn response carries stream.grid — e.g. /cli/sessions/grid?session=<uuid>&token=k2st_…— a relative path. Build the WebSocket URL from your subdomain and open it to watch the agent's terminal live:
wss://your-name.k2.dev/cli/sessions/grid?session=<uuid>&token=k2st_…The connection is authorized by the per-session k2st_ stream token, not your API key. That token is scoped to this one session and is revoked when the session exits.
Every server-to-client frame is a JSON envelope { "event": "<kind>", "payload": { … } }. On connect you receive one read-only snapshot, then incremental delta frames, plus title, label_changed, pin_changed, and child_exit events.
// snapshot — full terminal state (camelCase)
{
"event": "snapshot",
"payload": {
"paneId": "…", "cols": 120, "rows": 40,
"grid": [ [ /* CellRun[] per row, run-length encoded */ ] ],
"scrollback": [ /* rows of CellRun[] */ ],
"cursor": { "row": 3, "col": 12, "visible": true },
"version": 1, "displayOffset": 0, "altScreen": false
}
}
// a CellRun — fg/bg are 0xRRGGBB ints, or null for the theme default
{ "text": "PASS", "fg": 39423, "bg": null,
"bold": true, "italic": false, "underline": false }
// delta — apply on top of the last state
{
"event": "delta",
"payload": {
"paneId": "…", "cols": 120, "rows": 40,
"damagedRows": [ { "row": 7, "runs": [ /* CellRun[] */ ] } ],
"scrollbackAppended": [ /* rows of CellRun[] */ ],
"cursor": { "row": 8, "col": 0, "visible": true },
"version": 2
}
}Apply a delta by replacing each damagedRows entry at its row index, appending scrollbackAppended, and absorbing the new cursor and version.
Drive the session by sending text frames back over the same socket:
{ "action": "input", "text": "yes\n" }
{ "action": "resize", "cols": 100, "rows": 30 }Errors
Unknown workspaces, unknown sessions, and things you're not allowed to see are indistinguishable by design — all return the same 404 body, so the API never confirms what exists. Malformed requests return 400 with a plain error string; session-spawn quotas return 429 with an error and code.
404 { "error": "no such workspace" }Sandboxes (Dedicated)
Dedicated servers add a hardened microVM sandbox API — the same session model, but each agent boots inside its own isolated Linux virtual machine, built for untrusted workloads and agent fleets. Capability shows as "sandboxes": "microvm" on /v1/ping. Full sandbox documentation ships alongside Dedicated availability.