Skip to content

Session API (REST)

POST /api/session is the canonical way for a backend to create a Coframe Agent session and start its first agent turn — no SPA, no persistent connection. One request creates the session, persists the initial transcript, starts exactly one turn, and returns everything needed to follow it. Prefer the REST SDK if you’re in TypeScript; this page is the underlying wire contract for everyone else.

Send your API key with every request (including status polling):

Authorization: Bearer <api-key>

X-Jarvis-Api-Key: <api-key> is accepted as an alternative header. Unauthenticated requests receive 401.

POST /api/session
Content-Type: application/json
FieldTypeRequiredDescription
messagesMessage[]yesInitial transcript; the last user message seeds the first turn.
targetUrlstring (URL)noPage the edit targets.
targetPageUrlsstring[]noAdditional target pages.
metadataobjectnoOpaque caller metadata stamped onto the session.
idempotencyKeystringnoReplays return the original session instead of creating a duplicate.
userIdstringnoStable caller identity for turn attribution.
source"web" | "api" | "slack" | "cli"noAttribution stamped onto lifecycle events. Defaults to api.
callbackobjectnoWebhook registration — see below.
outputSchemaJSON Schema objectnoAsk for a structured deliverable: every turn ends with a JSON value matching your schema, read from agent/status.deliverable — see Structured sessions.
tools{ include?: string[], exclude?: string[], externalTurnExclude?: string[] }noPer-session tool policy — see Structured sessions. externalTurnExclude removes tools only on turns nobody started from the session page (the API-started first turn, agent-API messages, the agent’s own continuations); a person typing in the editor gets them back.
systemPromptstring (≤ 20,000 chars)noCaller instructions appended to the Mark-17 harness prompt under a ## Session Instructions heading. Never replaces the harness prompt.
mcpServers{ [id]: { url, type?, headers? } }noMCP servers the agent may call this session — see MCP servers.
capabilities{ browser?: boolean, sandbox?: boolean }noResources the agent may use; both default to true — see Capabilities.
subAgentsbooleannoLet the agent delegate to sub-agents (spawn_agents and the other delegation tools). Defaults to false: an API-created session never spawns unless you send true. A caller that also sends top-level source: "web" (the SPA’s marker) is instead governed by the PostHog sub-agents flag for its own identity — the flag must target that caller, so this is not an escalation path. A non-boolean is rejected with 400 subAgents: must be a boolean. Children of an enabled session inherit it — along with the session’s mcpServers (including the jarvis pin and server headers), tools and capabilities, but not its outputSchema (a child reports through submit_result) nor its systemPrompt (a child’s instructions are its objective); the setting is fixed at creation.
delegation{ maxLiveChildren?: integer ≥ 0, maxLiveBrowsers?: integer ≥ 0 }noPer-session concurrency caps for sub-agents, next to subAgents. Each field defaults to the deployment value (DELEGATION_MAX_LIVE_CHILDREN / DELEGATION_MAX_LIVE_BROWSERS, both 4 in production), which is also its ceiling: a value above it is rejected with 400 delegation.maxLiveBrowsers: 5 exceeds the deployment ceiling DELEGATION_MAX_LIVE_BROWSERS=4 — never clamped — and maxLiveBrowsers must not exceed the children cap the session runs under (400 delegation.maxLiveBrowsers (3) must be ≤ maxLiveChildren (2)). Unknown keys, non-integers and negatives are 400s too. The caps are fixed at creation, apply to every spawn_agents admission (browser: "own" children count against maxLiveBrowsers), are inherited by the session’s children, and are reported as facts in agent/status.delegation.maxLiveChildren / .maxLiveBrowsers. If a deployment later lowers a ceiling, the lower value applies to live sessions too. Has no effect unless sub-agents are enabled for the session.

A Message is { role: "user" | "assistant", parts: [{ type: "text", text: string }] }.

Each entry connects one remote MCP server for every turn of the session. Its tools appear to the agent as mcp_<id>__<tool>, alongside the built-in tools (and Jarvis tools, when the caller is a Jarvis user).

{
"mcpServers": {
"acme": {
"url": "https://mcp.acme.com/mcp",
"type": "http",
"headers": { "Authorization": "Bearer <token>" }
}
},
"tools": { "exclude": ["mcp_acme__delete_*"] }
}
  • url must be https; put credentials in headers, never in the URL. type is "http" (Streamable HTTP, default) or "sse".
  • Ids match [a-z0-9][a-z0-9_-]{0,31}. At most 10 servers, 20 headers each.
  • jarvis is special: "jarvis": { "url": "https://<your-jarvis-api>/api/mcp" } does not add a server — it pins the built-in Jarvis MCP server (tools mcp_jarvis__*) to that endpoint instead of the deployment’s default, so a session launched from a Jarvis preview talks to that preview. The session’s own Jarvis token is the credential: headers (and type: "sse") on jarvis are rejected with a 400.
  • Headers are stored server-side on the session and only sent to that server — they are never returned by any API, written to the transcript, or exposed to browser clients.
  • Like outputSchema and tools, the server list, systemPrompt and capabilities are fixed at creation: later intakes for the same session cannot change them.
  • tools.exclude applies to MCP tools too, by exact name or a trailing-* prefix (mcp_acme__* drops a whole server). So does tools.externalTurnExclude, on the turns it covers.
  • A server that is unreachable or slower than 15 s to list its tools is skipped for that turn; the turn still runs.

capabilities turns session resources off, for callers that only need the agent and their own tools:

  • browser: false — the Kernel browser is never started, and the browser and preview tools (browser_execute, screenshot, dom_query, set_previews, …) are removed.
  • sandbox: false — no sandbox container is started, and the workspace tools (read_file, run_shell, apply_variant, run_tests, skills, …) are removed. The browser runs in the sandbox, so this also turns the browser off; { "sandbox": false, "browser": true } is a 400. The session never starts a workspace, so its startupStatus stays uninitialized; turns run normally.
  • Removed tools can’t be re-added with tools.include. Like the other profile fields, capabilities is fixed at creation.
{
"success": true,
"sessionId": "session-1c8816418724",
"requestId": "req-658e8124-6da0-4f4e-8c9d-583607248d86",
"runId": "run_mrrllafh_vpswfpvp",
"shareUrl": "/edit/session-1c8816418724",
"statusUrl": "/api/sessions/session-1c8816418724/requests/req-658e8124-...",
"streamUrl": "/api/sessions/session-1c8816418724/requests/req-658e8124-.../stream"
}
  • shareUrl — open in a browser to watch or join the session.
  • statusUrl — poll (same auth header). Returns { success, sessionId, requestId, runId, status } where status is "pending" | "running" | "completed"; unknown request IDs return 404.
  • streamUrl — Server-Sent Events of the run’s domain events.

Invalid input returns 400 { success: false, error, code: "invalid_request" } with a field-scoped error message, and no session is created.

Terminal window
curl -s -X POST "https://<editor-host>/api/session" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [{ "role": "user", "parts": [{ "type": "text", "text": "Make the hero CTA green." }] }],
"targetUrl": "https://example.com/landing",
"idempotencyKey": "my-experiment-1234",
"callback": { "url": "https://api.example.com/hooks/editor", "secret": "whsec_..." }
}'

Instead of polling or holding the SSE stream open, register a callback and Coframe Agent pushes run lifecycle events to your endpoint:

FieldTypeRequiredDescription
urlstringyeshttps (or http) URL to POST events to. Loopback, private-range, and link-local hosts are rejected.
secretstringnoEnables HMAC-SHA256 signing of every delivery.
eventsstring[]noSubset of ["run.started", "run.completed"]. Default: all.

Each event is POSTed as JSON:

{
"event": "run.completed",
"sessionId": "session-1c8816418724",
"requestId": "req-658e8124-6da0-4f4e-8c9d-583607248d86",
"runId": "run_mrrllafh_vpswfpvp",
"timestamp": "2026-07-19T09:33:05.669Z",
"data": { "kind": "run.completed", "...": "the full domain event" }
}

With headers:

Content-Type: application/json
X-Coframe-Event: run.completed
X-Coframe-Delivery: whd-<uuid>
X-Coframe-Signature: sha256=<hmac-hex> (only when a secret was registered)

The signature is HMAC-SHA256 of the exact raw request body, keyed by your secret. Always verify against the raw bytes — re-serializing the parsed JSON will not match.

import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody: string, secret: string, signatureHeader: string): boolean {
const expected = `sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`;
return timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}
import hmac, hashlib
def verify(raw_body: bytes, secret: str, signature_header: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header)
  • At-least-once. Deduplicate by the X-Coframe-Delivery id (or by (runId, event)).
  • Deliveries are durable — they survive server restarts and redeploys.
  • Failed attempts retry with exponential backoff (5 attempts total; each attempt times out after 10 seconds), then the delivery is dropped.
  • Respond 2xx quickly from your endpoint; do slow work asynchronously.