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.
Authentication
Section titled “Authentication”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.
Start a session
Section titled “Start a session”POST /api/sessionContent-Type: application/json| Field | Type | Required | Description |
|---|---|---|---|
messages | Message[] | yes | Initial transcript; the last user message seeds the first turn. |
targetUrl | string (URL) | no | Page the edit targets. |
targetPageUrls | string[] | no | Additional target pages. |
metadata | object | no | Opaque caller metadata stamped onto the session. |
idempotencyKey | string | no | Replays return the original session instead of creating a duplicate. |
userId | string | no | Stable caller identity for turn attribution. |
source | "web" | "api" | "slack" | "cli" | no | Attribution stamped onto lifecycle events. Defaults to api. |
callback | object | no | Webhook registration — see below. |
A Message is { role: "user" | "assistant", parts: [{ type: "text", text: string }] }.
Response — 201
Section titled “Response — 201”{ "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 }wherestatusis"pending" | "running" | "completed"; unknown request IDs return404.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.
Example
Section titled “Example”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_..." } }'Webhook callbacks
Section titled “Webhook callbacks”Instead of polling or holding the SSE stream open, register a callback and
Coframe Agent pushes run lifecycle events to your endpoint:
| Field | Type | Required | Description |
|---|---|---|---|
url | string | yes | https (or http) URL to POST events to. Loopback, private-range, and link-local hosts are rejected. |
secret | string | no | Enables HMAC-SHA256 signing of every delivery. |
events | string[] | no | Subset of ["run.started", "run.completed"]. Default: both. |
Delivery format
Section titled “Delivery format”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/jsonX-Coframe-Event: run.completedX-Coframe-Delivery: whd-<uuid>X-Coframe-Signature: sha256=<hmac-hex> (only when a secret was registered)Verifying signatures
Section titled “Verifying signatures”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)Delivery semantics
Section titled “Delivery semantics”- At-least-once. Deduplicate by the
X-Coframe-Deliveryid (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
2xxquickly from your endpoint; do slow work asynchronously.