Skip to content

Protocol and API ​

Everything a client needs is JSON over HTTP plus one Server-Sent Events stream. The definitions live in @kestrel/protocol; the server is a reference implementation.

Base URL: http://<host>:7331. All API paths are under /api.

Authentication ​

Every /api/* request is authenticated according to auth.mode. With server.token (mode token) the credential is a single operator token; with accounts it is a per-user token that also selects the tenant. See Authentication and tenants. In both cases the header is:

http
Authorization: Bearer <token>

Event streams, which cannot set headers in a browser EventSource, may use a query parameter instead:

GET /api/sessions/<id>/events?token=<token>

GET /api/health is public so load balancers can probe the process without credentials. Static console files are public; the API they call is not.

Endpoints ​

MethodPathPurpose
GET/api/healthLiveness, version, uptime, plugin and session counts.
GET/api/bootstrapModels, tools, plugins and feature flags for a new client.
GET/api/configSanitized runtime configuration (no secrets).
GET/api/pluginsPlugin list with state and errors.
GET/api/auth/meCaller, tenant and authentication mode.
GET/api/usersUsers in the platform (admin only).
POST/api/usersCreate a user; the token is returned once (admin only).
POST/api/users/:id/tokenRotate a user's token (admin only).
DELETE/api/users/:idDelete a user (admin only).
GET/api/tenantsTenants (admin only).
POST/api/tenantsCreate a tenant (admin only).
GET/api/sessionsSession summaries with cumulative usage, scoped to the caller's tenant.
POST/api/sessionsCreate a session.
GET/api/sessions/:idSession detail, including the transcript.
PATCH/api/sessions/:idRename, change model or system prompt while idle.
DELETE/api/sessions/:idDelete a session and its persisted transcript.
POST/api/sessions/:id/messagesStart a run. Returns 202 immediately.
GET/api/sessions/:id/eventsSSE stream for one session.
GET/api/eventsSSE stream for every session the caller may see.
POST/api/sessions/:id/cancelAbort the active run.
POST/api/sessions/:id/approvals/:approvalIdApprove or decline a tool call.
GET/api/usageSession count and cumulative tokens, scoped to the caller's tenant.

Create a session ​

bash
curl -sS -X POST http://127.0.0.1:7331/api/sessions \
  -H 'content-type: application/json' \
  -d '{"title":"Refactor auth","model":"gpt-4o-mini","workspace":"/srv/app"}'
json
{
  "session": {
    "id": "0f0d...",
    "title": "Refactor auth",
    "createdAt": "2026-01-01T10:00:00.000Z",
    "updatedAt": "2026-01-01T10:00:00.000Z",
    "status": "idle",
    "model": "gpt-4o-mini",
    "messageCount": 0,
    "workspace": "/srv/app",
    "messages": [],
    "systemPrompt": "..."
  }
}

All fields except title are optional. workspace must be resolvable by the server.

Sessions created by delegate or delegate_many carry delegation metadata on every summary and detail response:

json
{ "parentId": "9a1b…", "depth": 1, "role": "researcher" }

parentId is absent for sessions a human started, which keeps the root list a tree with a single optional edge per node. depth and role are present whenever parentId is.

Send a message ​

bash
curl -sS -X POST http://127.0.0.1:7331/api/sessions/$ID/messages \
  -H 'content-type: application/json' \
  -d '{"content":"Find the failing test and fix it."}'
# {"runId":"..."}

The response is 202 Accepted and does not wait for the model. Subscribe to the event stream before or after sending; events are buffered in memory and replayed on reconnect.

One run per session at a time. A second message while a run is active returns 409 session_busy; cancel first or wait for run.finished.

Approve a tool call ​

bash
curl -sS -X POST \
  http://127.0.0.1:7331/api/sessions/$ID/approvals/$APPROVAL_ID \
  -H 'content-type: application/json' \
  -d '{"approved":true}'

The approval id arrives in an approval.requested event. Decisions are idempotent only while the call is pending; a late decision returns 404 approval_not_found.

Server-Sent Events ​

Each frame is:

id: 42
data: {"seq":42,"at":"2026-01-01T10:00:03.120Z","event":{"type":"message.delta","sessionId":"...","messageId":"...","channel":"text","delta":"Hel"}}
  • id equals the envelope seq. Send it back as Last-Event-ID (or ?lastEventId=) to replay missed events after a reconnect.
  • The server emits retry: 3000 and a heartbeat comment every 15 seconds.
  • Replay is per process and bounded; it is a reconnection aid, not an audit log.

Event types ​

TypePayload
session.updatedSession summary after a title, model or status change.
run.startedrunId; the run is now active.
message.startedAssistant message skeleton.
message.deltamessageId, channel (text or reasoning), delta.
message.completedFinal message with content, reasoning and tool calls.
tool.startedThe call about to execute.
tool.finishedResult, duration, error flag and optional structured data.
approval.requestedCall, reason and approval id.
approval.resolvedDecision.
run.finishedTerminal status (completed, cancelled, max_steps) and usage for that run.
run.failedError message; the session is marked error.
session.deletedThe session no longer exists.
plugin.changedA plugin changed state at runtime.
noticeNon-fatal informational message.

A user message is appended synchronously and emitted as message.completed before run.started. Clients may render it optimistically, then reconcile with the event carrying the server id.

Minimal client ​

ts
const session = await fetch(`${base}/api/sessions`, { method: "POST" }).then((r) => r.json());
const events = new EventSource(`${base}/api/sessions/${session.session.id}/events`);

events.onmessage = (message) => {
  const { event } = JSON.parse(message.data);
  switch (event.type) {
    case "message.delta":
      if (event.channel === "text") process.stdout.write(event.delta);
      break;
    case "run.finished":
      console.log(`\n[${event.status}]`);
      break;
    case "approval.requested":
      void decide(event.approval.id, true);
  }
};

await fetch(`${base}/api/sessions/${session.session.id}/messages`, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ content: "Hello" }),
});

Errors ​

Errors use one envelope:

json
{
  "error": {
    "code": "session_busy",
    "message": "This session already has an active run.",
    "details": {}
  }
}
StatusCodeWhen
400invalid_json, invalid_message, invalid_bodyMalformed or missing fields.
401unauthorizedMissing or wrong bearer token.
403forbiddenStatic path traversal attempt.
404session_not_found, approval_not_found, not_foundUnknown resource or route.
405method_not_allowedWrong verb for an existing path.
409session_busyA run is already active.
413payload_too_large, message_too_largeBody over the configured limit.
500internal_errorUnexpected server error; details are in the log.

Internal errors never leak stack traces to clients.

Limits ​

LimitDefault
Request body10 MB (server.bodyLimitBytes)
Message length200,000 characters
Tool result sent to the model60,000 characters
Store writesDebounced, atomic rename
Event replay bufferBounded per process

Versioning ​

/api/health and /api/bootstrap report protocol: "1.0". The protocol follows semantic versioning: additive event types and optional fields are minor changes; removals or semantic changes are major.

Released under the MIT License.