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

POSTapi.e2a.bot/v1/sessions

Create 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"
}
CodeMeaningWhen
201Createdsession bound + sandbox running + JWT minted
400Bad requestunknown template/agent, missing user_id, invalid llm.model, llm.base_url fails SSRF allowlist, or secret bundle key collision
401Unauthorizedmissing or invalid API key
404Not foundreferenced secret bundle does not exist
503Service unavailableno 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 promptly: Sessions expire after 60 seconds if no WebSocket connection is established. Connect to connect_url immediately after creation.

Get Session

GETapi.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"
}
CodeMeaningWhen
200OKsession exists and is accessible
401Unauthorizedmissing or invalid API key
403Forbiddensession owned by different tenant
404Not foundsession expired, reaped, or never existed

Tip: Call GET before reconnecting to avoid WebSocket upgrade failures on expired sessions.

Resume Session

POSTapi.e2a.bot/v1/sessions/resume

Resume 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"
  }
}
CodeMeaningWhen
200OKnew sandbox spun + state restored
401Unauthorizedmissing or invalid API key
403Forbiddencross-tenant resume (session owned by different tenant)
404Not foundchat_session_id never existed
502Bad gatewaysecret_bundle_not_found_at_resume — bundle was deleted

Resume recovers llm.model, llm.base_url, and secrets references automatically.

WebSocket Connection

WSrt.e2a.bot/v1/sessions/{id}/ws

Real-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..." }
});
CodeMeaningWhen
101Switching protocolsupgrade accepted, JWT valid
401Unauthorizedmissing/invalid/expired JWT
403ForbiddenJWT session_id != URL path, or wrong tenant

Frame Types

DirectionTypeDescription
client-servertask{ "type": "task", "text": "..." }
server-clientstdout{ "type": "stdout", "data": "..." }
server-clientexit{ "type": "exit", "exit_code": 0 }
server-clientsystem/cancelPre-termination warning (timeout or POST /cancel)
server-clientsystem/config_updateHot-replace: secret rotated, keys updated in-place
server-clientbinaryDesktop 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 detection
  • mouse_click, mouse_move — cursor control
  • keyboard_type, press_key — keyboard input
  • scroll — scroll viewport
  • shell_exec — run shell commands

Desktop WebSocket

WSrt.e2a.bot/v1/sessions/{id}/desktop

Dedicated 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..."
);
CodeMeaningWhen
101Switching protocolsupgrade accepted, JWT valid, cua_enabled=true
401Unauthorizedmissing or invalid JWT
403ForbiddenJWT 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:

BytesTypeFieldDescription
0-7u64 LEframe_numberMonotonic frame counter (1, 2, 3...)
8-15u64 LEtimestamp_msUnix timestamp in milliseconds
16-17u16 LEwidthFrame width in pixels
18-19u16 LEheightFrame height in pixels
20+bytesjpeg_dataJPEG-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"
    }));
  }
}