Sessions API
Agent sessions with WebSocket streaming, state persistence, and resume support. For multi-turn agent conversations.
Sessions vs Sandboxes
Sessions (/v1/sessions/*)
Agent paradigm: multi-turn conversations, WebSocket streaming via rt.e2a.bot, persistent state, resume after disconnect.
Sandboxes (/v1/sandboxes/*)
Compute primitives: exec, files, logs, batch jobs. No agent state.
Create Session
api.e2a.bot/v1/sessionsCreate an agent session. Returns a WebSocket URL for real-time interaction.
Request
{
"agent": { "kind": "deictic", "version": "b4232d5" },
"template": "cua", // "cua" | "browser" | "standard"
"llm": {
"vendor": "anthropic", // "anthropic" | "openai" | "google" | "deepseek"
"model": "claude-sonnet-4-5", // optional: override default model
"base_url": "https://proxy.example.com/v1" // optional: custom LLM endpoint
},
"secrets": ["my-llm-keys"], // secret bundle names (see Secrets API)
"app": { "id": "my-app", "set_id": "default", "capability_sets": {} },
"user_id": "user_uuid_123", // required
"ttl_seconds": 3600 // optional, default 3600, max 7200
}Response (201)
{
"chat_session_id": "ses_abc123",
"jwt": "eyJ...", // session JWT for WSS auth
"connect_url": "wss://rt.e2a.bot/v1/sessions/ses_abc123/ws",
"expires_at": "2026-05-01T13:00:00Z"
}| Code | Meaning | When |
|---|---|---|
| 201 | Created | session bound + sandbox running + JWT minted |
| 400 | Bad request | unknown template/agent, missing user_id, invalid llm.model, llm.base_url fails SSRF allowlist, or secret bundle key collision |
| 401 | Unauthorized | missing or invalid API key |
| 404 | Not found | referenced secret bundle does not exist |
| 503 | Service unavailable | no workers reachable |
secrets: Up to 10 bundle names. Keys merged; collision returns 400. Use LLM_API_KEY as the universal LLM key.
llm.model: Regex ^[a-zA-Z0-9._-]{1,128}$
llm.base_url: HTTPS only, passes SSRF allowlist (no loopback/RFC1918/link-local)
connect_url immediately after creation.Get Session
api.e2a.bot/v1/sessions/{id}Check session status and metadata. Useful for verifying a session is still alive before connecting.
Response (200)
{
"session_id": "ses_abc123",
"user_id": "user_uuid_123",
"status": "running", // "running" | "completed" | "expired"
"template": "cua",
"agent": { "kind": "deictic", "version": "b4232d5" },
"created_at": "2026-05-01T12:00:00Z",
"expires_at": "2026-05-01T13:00:00Z",
"last_active_at": "2026-05-01T12:30:00Z"
}| Code | Meaning | When |
|---|---|---|
| 200 | OK | session exists and is accessible |
| 401 | Unauthorized | missing or invalid API key |
| 403 | Forbidden | session owned by different tenant |
| 404 | Not found | session expired, reaped, or never existed |
Tip: Call GET before reconnecting to avoid WebSocket upgrade failures on expired sessions.
Resume Session
api.e2a.bot/v1/sessions/resumeResume an existing session on a fresh sandbox. Restores persisted state + config.
Request
{
"chat_session_id": "ses_abc123",
"ttl_seconds": 3600
}Response (200)
{
"chat_session_id": "ses_abc123",
"jwt": "eyJ...", // NEW JWT
"connect_url": "wss://rt.e2a.bot/v1/sessions/ses_abc123/ws",
"expires_at": "2026-05-01T14:00:00Z",
"resumed_from": {
"previous_status": "completed"
}
}| Code | Meaning | When |
|---|---|---|
| 200 | OK | new sandbox spun + state restored |
| 401 | Unauthorized | missing or invalid API key |
| 403 | Forbidden | cross-tenant resume (session owned by different tenant) |
| 404 | Not found | chat_session_id never existed |
| 502 | Bad gateway | secret_bundle_not_found_at_resume — bundle was deleted |
Resume recovers llm.model, llm.base_url, and secrets references automatically.
WebSocket Connection
rt.e2a.bot/v1/sessions/{id}/wsReal-time bidirectional channel for agent interaction. Use the JWT from session create/resume.
Connection
// Browser (use ?token= since browsers can't set Authorization on WS)
const ws = new WebSocket("wss://rt.e2a.bot/v1/sessions/ses_abc123/ws?token=eyJ...");
// Node.js
const ws = new WebSocket("wss://rt.e2a.bot/v1/sessions/ses_abc123/ws", {
headers: { "Authorization": "Bearer eyJ..." }
});| Code | Meaning | When |
|---|---|---|
| 101 | Switching protocols | upgrade accepted, JWT valid |
| 401 | Unauthorized | missing/invalid/expired JWT |
| 403 | Forbidden | JWT session_id != URL path, or wrong tenant |
Frame Types
| Direction | Type | Description |
|---|---|---|
| client-server | task | { "type": "task", "text": "..." } |
| server-client | stdout | { "type": "stdout", "data": "..." } |
| server-client | exit | { "type": "exit", "exit_code": 0 } |
| server-client | system/cancel | Pre-termination warning (timeout or POST /cancel) |
| server-client | system/config_update | Hot-replace: secret rotated, keys updated in-place |
| server-client | binary | Desktop screenshot (CUA template only) |
Hot-Replace Frame
{ "type": "system", "event": "config_update", "keys": ["LLM_API_KEY"] }Sent when you rotate a secret bundle via PUT. Agent receives new values without restart.
CUA Desktop Streaming
CUA sessions (template: "cua") boot a virtual desktop with Chromium + OmniParser. Screenshots are sent as binary WebSocket frames.
Agent tools available in CUA:
screenshot— capture framebuffer (returns binary PNG frame)locate_element— OmniParser YOLO+OCR UI detectionmouse_click,mouse_move— cursor controlkeyboard_type,press_key— keyboard inputscroll— scroll viewportshell_exec— run shell commands
Desktop WebSocket
rt.e2a.bot/v1/sessions/{id}/desktopDedicated binary channel for desktop frame streaming. Separate from the main session WebSocket to avoid mixing text and binary frames.
Connection
const desktopWs = new WebSocket( "wss://rt.e2a.bot/v1/sessions/ses_abc123/desktop?token=eyJ..." );
| Code | Meaning | When |
|---|---|---|
| 101 | Switching protocols | upgrade accepted, JWT valid, cua_enabled=true |
| 401 | Unauthorized | missing or invalid JWT |
| 403 | Forbidden | JWT session_id mismatch, or cua_enabled=false |
Binary Frame Format
Each frame is a binary WebSocket message with a 20-byte header followed by JPEG data:
| Bytes | Type | Field | Description |
|---|---|---|---|
| 0-7 | u64 LE | frame_number | Monotonic frame counter (1, 2, 3...) |
| 8-15 | u64 LE | timestamp_ms | Unix timestamp in milliseconds |
| 16-17 | u16 LE | width | Frame width in pixels |
| 18-19 | u16 LE | height | Frame height in pixels |
| 20+ | bytes | jpeg_data | JPEG-encoded frame |
Control Messages
Send JSON text frames to control streaming:
// Start streaming (sent automatically on connect)
{ "type": "desktop_stream_control", "action": "start" }
// Pause streaming (frames buffered server-side)
{ "type": "desktop_stream_control", "action": "pause" }
// Resume streaming
{ "type": "desktop_stream_control", "action": "resume" }
// Stop streaming
{ "type": "desktop_stream_control", "action": "stop" }Client Example
class DesktopViewer {
constructor(sessionId, jwt) {
this.ws = new WebSocket(
`wss://rt.e2a.bot/v1/sessions/${sessionId}/desktop?token=${jwt}`
);
this.ws.binaryType = "arraybuffer";
this.canvas = document.getElementById("desktop");
this.ctx = this.canvas.getContext("2d");
this.ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
this.renderFrame(event.data);
}
};
}
renderFrame(buffer) {
const view = new DataView(buffer);
const frameNum = view.getBigUint64(0, true);
const timestamp = view.getBigUint64(8, true);
const width = view.getUint16(16, true);
const height = view.getUint16(18, true);
const jpeg = new Uint8Array(buffer, 20);
// Resize canvas if needed
if (this.canvas.width !== width || this.canvas.height !== height) {
this.canvas.width = width;
this.canvas.height = height;
}
// Render JPEG
const blob = new Blob([jpeg], { type: "image/jpeg" });
createImageBitmap(blob).then((img) => {
this.ctx.drawImage(img, 0, 0);
});
}
pause() {
this.ws.send(JSON.stringify({
type: "desktop_stream_control", action: "pause"
}));
}
resume() {
this.ws.send(JSON.stringify({
type: "desktop_stream_control", action: "resume"
}));
}
}