Bearer API key
Long-lived key for sandbox/session operations. Create via POST /v1/api-keys after OTP auth. Format: e2a_live_… Send as Authorization: Bearer e2a_live_…
Complete contract for the e2a.bot platform — two API surfaces (sandboxes for compute primitives; sessions for the agent paradigm), tenant onboarding, template catalog, usage, WebSocket framing, desktop streaming, and sandbox lifecycle.
Shared vocabulary used across every endpoint and protocol on this page.
Bearer API key
Long-lived key for sandbox/session operations. Create via POST /v1/api-keys after OTP auth. Format: e2a_live_… Send as Authorization: Bearer e2a_live_…
Bearer JWT
Short-lived token for account operations (create API keys, billing). Obtained from POST /v1/auth/verify-otp. Use API key for most operations.
Templates
Pre-baked rootfs + kernel combinations. standard (minimal Linux), browser (Chromium headless), cua (Xvfb + fluxbox + Chromium + OmniParser for agent desktop control), canvas (HTML canvas rendering). Template selection is irreversible per sandbox.
Agents
Optional one-shot or interactive agent runtimes bundled into the rootfs. deictic (first-party Rust, needs explicit model), claude-code (Anthropic CLI, model must start with claude), codex (OpenAI CLI, model must start with gpt or o). Set llm_key when agent is present.
States
creating → configuring → booting → running → stopping → destroyed. running can branch to paused and back. Any state can emergency-transition to destroyed (reaper, manual DELETE, spot reclaim).
Hosts
Two public hostnames. api.e2a.bot — control plane (auth, API keys, sandboxes, sessions, secrets, billing). rt.e2a.bot — runtime WSS for agent sessions (wss://rt.e2a.bot/v1/sessions/{id}/ws). All endpoints use HTTPS; WebSocket uses WSS.
Secrets (BYO LLM keys)
Secret bundles store your LLM API keys in AWS Secrets Manager under your tenant namespace. Create bundles via POST /v1/tenants/me/secrets, then reference them by name in session create (secrets: ["my-bundle"]). Keys are injected into the sandbox at session start. Use LLM_API_KEY as the universal key name regardless of LLM vendor. Bundles support hot-replace: rotate keys via PUT without restarting running sessions — agents receive new values via WebSocket config_update frame.
Two-API distinction
e2a.bot exposes two API surfaces with distinct customer mental models. /v1/sandboxes/* is for compute primitives — generic isolated execution (exec, files, logs, batch jobs, CI/CD); no agent paradigm. /v1/sessions/* is for the agent paradigm — multi-agent conversations, WebSocket streaming over rt.e2a.bot, persistent state, resume after disconnect. Both use the same Bearer API key auth; only the use-case shape differs. Customers needing stateful agent conversations MUST use /v1/sessions/* — the /v1/sandboxes/{id}/ws WebSocket route was removed; agent WSS belongs exclusively to /v1/sessions/{id}/ws.
api.e2a.bot/v1/sandboxes/* — generic isolated execution. Use this for one-shot batch jobs, CI/CD, scripts, file I/O, log tailing. Stateless; no agent paradigm. For multi-agent conversations + WSS + state + resume, use the Sessions API below. Every route authenticates with a Bearer API key. Note: /v1/sandboxes/{id}/ws was removed in 2026-04 — agent WS belongs exclusively to /v1/sessions/{id}/ws on rt.e2a.bot.
/v1/sandboxesBearer API keyCreate a new sandbox. Optionally attaches an agent + task for one-shot execution, or leaves the sandbox idle for WebSocket-driven control.
Request
{
"template": "standard", // required: "standard" | "browser" | "cua" | "canvas"
"agent": "claude-code", // optional: "deictic" | "claude-code" | "codex"
"llm_key": "sk-ant-...", // required iff agent is set
"task": "summarize README.md", // optional: one-shot prompt; absent ⇒ agent waits on WS
"model": "claude-sonnet-4-5", // required for deictic; prefix-validated for claude-code/codex
"session_id": "abcd-1234", // optional: codex resume uuid
"wire_version": 2, // optional, default 2; set 1 only for legacy single-agent
"idle_timeout_seconds": 60, // optional, default 60
"max_lifetime_seconds": 7200, // optional, default + hard cap 7200
"workspace_enabled": false, // optional, default false
"app_id": "my-app", // required iff workspace_enabled
"capset_id": "default", // required iff workspace_enabled
"app_json": { // optional; nested capability_sets schema (see notes)
"name": "My App",
"capability_sets": [{
"name": "general",
"agents": [{
"name": "primary",
"capability_name": "general",
"capability_description": "general-purpose worker",
"tools": ["read_file", "write_file", "shell_exec"]
}]
}]
},
"persistence_url": "https://chat.example.com/persist", // optional; HTTPS-only, public-IP only
"persistence_auth_header": "Bearer sk_…", // optional; verbatim Authorization header
"env_vars": { "MY_VAR": "value" }, // optional; reserved keys rejected
"metadata": { "tag": "prod" } // optional free-form
}Response
{
"vm_id": "sbx_7f3k9m2x",
"state": "running",
"host_port": 30090,
"endpoint": "http://worker-ip:9090" // optional; orchestrator's self-reported node endpoint
}Status codes
| 201 | Created | sandbox provisioned and reached Running |
| 400 | Bad request | missing required field, reserved env-var key, agent without llm_key, flat-shape app_json, multi-agent app_json on wire_version=1, or persistence_url failing the SSRF allowlist |
| 401 | Unauthorized | missing or invalid Bearer API key |
| 409 | Conflict | no capacity on any worker |
| 501 | Not implemented | unknown agent type or template |
| 503 | Service unavailable | no workers registered or reachable |
Notes
/v1/sandboxesBearer API keyList all sandboxes owned by the authenticated user. Fans out across all worker nodes and merges results.
Response
// Full success — returns JSON array of VmResponse objects
[
{ "vm_id": "sbx_7f3k9m2x", "state": "running", "vcpu": 1, "cpu_fraction": 0.5, ... },
...
]
// Partial failure — wrapped object with partial flag
{
"sandboxes": [ { ... } ],
"partial": true
}Status codes
| 200 | OK | all workers responded (or degraded with partial:true) |
| 401 | Unauthorized | missing or invalid Bearer API key (any worker returning 401 short-circuits) |
| 503 | Service unavailable | no workers registered |
Notes
/v1/sandboxes/{id}Bearer API keyGet details for a specific sandbox, including agent status when an agent is attached.
Response
{
"vm_id": "sbx_7f3k9m2x",
"state": "running",
"vcpu": 1,
"cpu_fraction": 0.5,
"memory_mib": 512,
"rootfs": "standard.ext4",
"metadata": { "tag": "prod", "user_id": "usr_abc" },
"agent_exit_code": 0, // optional; present once agent exits
"agent_status": "completed" // optional; "running" | "completed" | "failed"
}Status codes
| 200 | OK | sandbox found and caller owns it |
| 401 | Unauthorized | missing or invalid Bearer API key |
| 404 | Not found | no such sandbox, or cross-tenant access attempt (no existence leak) |
Notes
/v1/sandboxes/{id}Bearer API keyDestroy a sandbox. Synchronous — returns once Firecracker, TAP, and port mappings are torn down. Workspace data (if enabled) is flushed to S3 before teardown.
Response
{
"vm_id": "sbx_7f3k9m2x",
"state": "destroyed"
}Status codes
| 200 | OK | sandbox destroyed |
| 401 | Unauthorized | missing or invalid Bearer API key |
| 404 | Not found | no such sandbox or cross-tenant access attempt |
Notes
/v1/sandboxes/{id}/execBearer API keyExecute a shell command inside the sandbox via SSH. Blocking — response returns once the command exits or the request context is cancelled.
Request
{
"command": "echo hello && ls /workspace"
}Response
{
"output": "hello\ntotal 0\n"
}Status codes
| 200 | OK | command executed |
| 400 | Bad request | missing or empty command |
| 401 | Unauthorized | missing or invalid Bearer API key |
| 404 | Not found | no such sandbox or cross-tenant access attempt |
/v1/sandboxes/{id}/logsBearer API keyRetrieve the tail of the agent's stderr log. Primary way to collect one-shot task output once agent_status transitions to completed or failed.
Query parameters
| Name | Type | Req | Default | Description |
|---|---|---|---|---|
| stream | enum | no | stderr | Which log stream to return. Phase 2 only implements stderr; stdout and all return 501. |
| lines | integer | no | 100 | Number of trailing lines to return. Mutually exclusive with bytes. Max 10000; values above are clamped. |
| bytes | integer | no | — | Number of trailing bytes to return instead of lines. Mutually exclusive with lines. Max 1048576 (1 MiB); values above are clamped. |
Response
{
"content": "...",
"truncated": false,
"total_bytes": 12345
}Status codes
| 200 | OK | stream fetched |
| 400 | Bad request | lines and bytes both set; zero value; non-integer; unknown stream value |
| 401 | Unauthorized | missing or invalid Bearer API key |
| 404 | Not found | no such sandbox or cross-tenant access attempt |
| 501 | Not implemented | stream=stdout or stream=all (reserved for future) |
Notes
/v1/sandboxes/{id}/filesBearer API keyDownload a file from inside the sandbox. Binary-safe (base64 round-trip internally).
Query parameters
| Name | Type | Req | Default | Description |
|---|---|---|---|---|
| path | string | yes | — | Absolute path inside the VM. Must start with /; .. components are rejected. |
Response
// Content-Type: application/octet-stream // Body: raw file bytes
Status codes
| 200 | OK | file read |
| 400 | Bad request | missing path, relative path, or .. component present |
| 401 | Unauthorized | missing or invalid Bearer API key |
| 404 | Not found | no such sandbox or file does not exist |
/v1/sandboxes/{id}/filesBearer API keyUpload a file into the sandbox.
Query parameters
| Name | Type | Req | Default | Description |
|---|---|---|---|---|
| path | string | yes | — | Absolute path inside the VM. Parent directory must exist. |
Request
// Content-Type: application/octet-stream // Body: raw file bytes
Response
{
"status": "uploaded"
}Status codes
| 200 | OK | file written |
| 400 | Bad request | invalid path or missing body |
| 401 | Unauthorized | missing or invalid Bearer API key |
| 404 | Not found | no such sandbox |
/v1/sandboxes/{id}/cancelBearer API keyBroadcast a cancel signal to all active WebSocket subscribers for this sandbox. Does NOT destroy the VM — use DELETE for that or wait for TTL.
Request
// Body is optional; defaults to { "reason": "user_request" } when empty.
{
"reason": "user_request" // "credits_exhausted" | "user_request" | "admin_action" | "timeout"
}Response
{
"vm_id": "sbx_7f3k9m2x",
"cancelled": true, // true if any WS subscriber received the broadcast
"reason": "user_request"
}Status codes
| 200 | OK | broadcast attempted (cancelled may be false if no WS subscribers) |
| 401 | Unauthorized | missing or invalid Bearer API key |
| 404 | Not found | no such sandbox or cross-tenant access attempt |
| 409 | Conflict | sandbox is not in the Running state |
Notes
api.e2a.bot/v1/sessions/* (control plane) + wss://rt.e2a.bot/v1/sessions/{id}/ws (runtime WSS). Use this for multi-agent conversations, persistent state across reconnects, and resume after disconnect. Bearer API key authenticates create/resume; the session JWT returned at create time authenticates the WSS upgrade.
/v1/sessionsBearer API keyCreate a new agent session. Spins a sandbox bound to a freshly-minted chat_session_id and returns a session JWT plus a connect_url for the WSS upgrade. State writes during the session persist; reconnects + resume are supported.
Request
{
"agent": { "kind": "deictic", "version": "b4232d5" },
"template": "cua", // required: "cua" | "browser" | "standard"
"llm": {
"vendor": "anthropic", // required: "anthropic" | "openai" | "google" | "deepseek"
"model": "claude-sonnet-4-5", // optional: override default model for vendor
"base_url": "https://proxy.example.com/v1" // optional: custom LLM endpoint (HTTPS only)
},
"secrets": ["my-llm-keys"], // optional: secret bundle names to inject (see Secrets API)
"app": { "id": "my-app", "set_id": "default", "capability_sets": {} },
"user_id": "user_uuid_123", // required; flat tenant model = your tenant_id
"ttl_seconds": 3600 // optional, default 3600, hard cap 7200
}Response
{
"chat_session_id": "ses_abc123",
"jwt": "eyJ...", // session JWT — exp ≈ ttl_seconds; HS256
"connect_url": "wss://rt.e2a.bot/v1/sessions/ses_abc123/ws",
"expires_at": "2026-04-28T13:00:00Z"
}Status codes
| 201 | Created | session bound + sandbox running + JWT minted |
| 400 | Bad request | unknown template, unknown agent.kind, missing user_id, invalid llm.model regex, llm.base_url fails SSRF allowlist, or secret bundle key collision |
| 401 | Unauthorized | missing or invalid Bearer API key |
| 404 | Not found | referenced secret bundle does not exist |
| 503 | Service unavailable | no orchestrator workers reachable |
Notes
/v1/sessions/resumeBearer API keyResume an existing chat_session_id on a fresh sandbox. The original VM may be terminated; resume re-spins a new VM bound to the same chat_session_id, restores persisted state, and returns a new session JWT + connect_url.
Request
{
"chat_session_id": "ses_abc123",
"ttl_seconds": 3600
}Response
{
"chat_session_id": "ses_abc123",
"jwt": "eyJ...", // NEW JWT — fresh exp
"connect_url": "wss://rt.e2a.bot/v1/sessions/ses_abc123/ws",
"expires_at": "2026-04-28T13:00:00Z",
"resumed_from": {
"previous_status": "completed" // pre-resume state.status; "unknown" when state predates the field
}
}Status codes
| 200 | OK | new sandbox spun + state-storage TTL intact |
| 401 | Unauthorized | missing or invalid Bearer API key |
| 403 | Forbidden | cross-tenant resume — chat_session_id exists but is owned by a different tenant; audit log carries cross_tenant_resume_denied with owner_tenant_id |
| 404 | Not found | chat_session_id never existed under any tenant |
| 502 | Bad gateway | secret_bundle_not_found_at_resume — a secret bundle referenced by the original session was deleted between create and resume |
Notes
/v1/sessions/{id}/wsSession JWT (Authorization: Bearer or ?token= query)Runtime WSS endpoint on rt.e2a.bot. Upgrade with the JWT returned by POST /v1/sessions or POST /v1/sessions/resume. Bidirectional frames stream agent control, stdio, and (for cua templates) desktop screenshots. Browser clients should use ?token=<jwt> since browsers can't set Authorization on WS upgrades.
Status codes
| 101 | Switching protocols | upgrade accepted; JWT validates + chat_session_id matches URL + tenant matches |
| 401 | Unauthorized | missing/invalid/expired JWT, OR session unknown (anti-oracle — does not leak existence) |
| 403 | Forbidden | JWT's chat_session_id ≠ URL path id, OR JWT's tenant_id ≠ session's owner tenant_id |
Notes
Non-REST interactions — WebSocket framing for the agent paradigm, CUA desktop streaming, and the one-shot agent task workflow.
GET wss://rt.e2a.bot/v1/sessions/{id}/wsReal-time bidirectional channel for the agent paradigm. Text frames carry JSON control messages; binary frames carry desktop screenshots (CUA only). e2a-api validates the session JWT (issued at POST /v1/sessions or /v1/sessions/resume) + cross-tenant tenant_id check + path-binding check, then forwards the upgrade to the worker over an internal-secret-gated channel. Customers pass the JWT via Authorization: Bearer header OR ?token= query (browsers can't set Authorization on WS upgrades).
Session JWT claims
| Claim | Type | Req | Default | Description |
|---|---|---|---|---|
| user_id | string | yes | — | From the Bearer API key; baked into the session JWT. |
| app_id | string | yes | e2a-sandbox | Runtime-required identity field. |
| set_id | string | no | default | Runtime capability-set scope. |
| chat_session_id | string (UUID) | yes | — | Fresh per-upgrade UUID. Used by the runtime to partition one WS attach from another. |
| exp | unix seconds | yes | now + 60s | Session JWT TTL. Short-lived by design — the orchestrator re-mints on each upgrade. |
| cua_enabled | boolean | no | false | Signals the runtime to activate CUA tool context (screenshot, mouse/keyboard). |
Frames
Agent control + stdio stream. Schema is defined by the agent running inside the VM (deictic, claude-code, or codex). The orchestrator does not parse them — it passes text frames through and optionally intercepts for metering.
{ "type": "exec", "command": "ls -la" } // client → server
{ "type": "stdout", "data": "total 68\n" } // server → client
{ "type": "exit", "exit_code": 0 } // server → clientRaw image bytes (PNG/JPEG). Only emitted when cua_enabled is true. No wrapper frame — the payload is the image. Clients render directly.
Server-originated keepalive every 30 seconds to prevent idle NAT / load-balancer timeouts. Clients should reply with pong; most WebSocket libraries do this automatically.
Pre-termination warning, broadcast once ~10 seconds before max_lifetime_seconds expiry when at least one WS client is attached. Also emitted when POST /v1/sandboxes/{id}/cancel is called.
{ "type": "system", "event": "cancel", "reason": "timeout" }Hot-replace notification. Emitted when a secret bundle referenced by this session is rotated via PUT /v1/tenants/me/secrets/{name}. The agent runtime receives the updated key values and swaps them in-place without session restart. keys lists the updated key names.
{ "type": "system", "event": "config_update", "keys": ["LLM_API_KEY"] }Orderly disconnect. 1001 on SIGTERM or explicit DELETE of the sandbox; 1011 when the upstream runtime is unreachable at upgrade time.
code=1001, reason="server shutting down" code=1001, reason="sandbox destroyed" code=1011, reason="upstream unavailable"
Notes
CUA sessions (template: "cua") boot an Xvfb virtual display with fluxbox + Chromium + a local OmniParser UI-detection server. When an agent calls the screenshot tool, the runtime captures the framebuffer and emits the image as a binary WebSocket frame. Mouse and keyboard control flow through text tool-call frames in the opposite direction.
Flow
Notes
For fire-and-forget agent runs (scrape, summarize, generate), pass agent + task + llm_key at create time and poll until complete. The sandbox self-destructs via idle timeout or max lifetime after the agent exits; no explicit DELETE needed unless you want to free resources sooner.
Flow
Notes
States, TTL rules, and cancel semantics that govern every sandbox.
| creating | Resource allocation, port + IP assignment. VM has no compute yet. |
| configuring | Rootfs copy, kernel + drive attach, network config prepared. |
| booting | Firecracker process spawned; init + systemd coming up. |
| running | Ready. SSH + WebSocket accept connections; agent (if any) running. |
| paused | Firecracker Ctrl-Alt-Del / pause equivalent. running ⇄ paused is resumable. |
| stopping | Graceful teardown in progress. Workspace flush, process cleanup. |
| destroyed | Terminal. Resources released. Sandbox record remains briefly for the List endpoint. |
Valid transitions: creating → configuring → booting → running; running ⇄ paused; running → stopping → destroyed; any state can emergency-transition to destroyed.
| Field | Type | Default | Behavior |
|---|---|---|---|
| idle_timeout_seconds | u64 | 60 | When active_ws_connections == 0 for this many seconds, the reaper destroys the sandbox. Timer starts only after the VM reaches Running (boot time does not count). |
| max_lifetime_seconds | u64 | 7200 (hard cap) | Regardless of WS activity, the sandbox is destroyed this many seconds after creation. Hard-capped at 7200 — values above are clamped. At max_lifetime - 10s, if any WS client is attached, a one-time pre-termination warning is broadcast. |
The reaper evaluates rules every 10 seconds. Evaluation order: hard max lifetime → pre-expiry warning window → idle timeout.
POST /v1/sandboxes/{id}/cancel — broadcasts a cooperative {type:"system", event:"cancel", reason:"…"} message over the WebSocket. The agent decides how to react. Does not destroy the VM. 409 if the sandbox is not running.DELETE /v1/sandboxes/{id} — forcibly tears down Firecracker, TAP, and port mappings. Active WS proxies receive a 1001 close with reason "sandbox destroyed".{event:"cancel", reason:"timeout"} warning ~10 seconds before the destroy.api.e2a.bot — auth (OTP), API keys, dashboard, billing, and workspaces. Every route after verify-otp authenticates with a Bearer JWT.
Self-serve tenant onboarding for individual customers. Email + OTP flow issues a tenant_id + initial Bearer API key in one shot. The api_key is shown only at verify-time; subsequent reads return only the prefix. Distinct from the legacy /v1/auth/* group below — /v1/tenants is the path forward; /v1/auth/* remains for existing users on api.e2a.bot.
/v1/tenantsNoneBegin signup for a new tenant. Sends a one-time passcode (OTP) to the email. Per memory project_e2a_bot_sni_dpi: customer-facing api.e2a.bot is the canonical host for /v1/tenants; do not use api.e2a.bot for new accounts.
Request
{ "email": "you@example.com" }Response
{ "message": "OTP sent" }Status codes
| 200 | OK | OTP dispatched via SES |
| 400 | Bad request | missing or malformed email |
| 429 | Too many requests | OTP rate-limit per email (3/hour) OR per-IP rate-limit on the auth-rate-limited prefix |
| 500 | Internal | SES send failed (e.g., domain not verified for a real-domain test) |
Notes
/v1/tenants/verifyNoneVerify the OTP and provision the tenant. Returns tenant_id + initial Bearer API key in one response. The api_key is shown ONCE — store it.
Request
{ "email": "you@example.com", "code": "123456" }Response
{
"tenant_id": "usr_eab8d564...", // cf. tnt_xxx is the canonical contract shape; usr_ prefix tracked as a separate cleanup ticket
"key_id": "key_e6ba43557ec8628c",
"prefix": "e2a_92ca",
"api_key": "e2a_92caf7cf...64-hex-chars", // SHOWN ONCE — store at this point
"plan_tier": "free",
"created_at": "2026-04-28T09:40:32Z",
"token": "eyJ..." // JWT — short-lived; used for /v1/tenants/me reads
}Status codes
| 200 | OK | OTP valid + tenant + key created |
| 401 | Unauthorized | OTP invalid / expired / wrong code |
Notes
/v1/tenants/meBearer API keyGet the caller's tenant info.
Response
{
"tenant_id": "usr_eab8d564...",
"email": "you@example.com",
"created_at": "2026-04-28T09:40:32Z",
"plan": "free"
}Status codes
| 200 | OK | key valid + tenant resolved |
| 401 | Unauthorized | missing or invalid Bearer API key |
/v1/tenants/me/api-keysBearer API keyIssue an additional API key under the same tenant. The api_key field is returned ONCE — store it now.
Response
{
"key_id": "key_<uuid>",
"prefix": "e2a_xxxx",
"api_key": "e2a_<64-hex>", // SHOWN ONCE
"plan_tier": "free",
"created_at": "..."
}Status codes
| 200 | OK | key issued |
| 401 | Unauthorized | missing or invalid Bearer API key |
/v1/tenants/me/api-keysBearer API keyList all API keys for the tenant. NEVER returns api_key values — only key_id, prefix, plan_tier, revoked, created_at.
Response
{
"keys": [
{
"key_id": "key_abc",
"prefix": "e2a_92ca",
"plan_tier": "free",
"revoked": false,
"created_at": "..."
}
]
}Status codes
| 200 | OK | list returned (may be empty) |
| 401 | Unauthorized | missing or invalid Bearer API key |
/v1/tenants/me/api-keys/{key_id}Bearer API keyRevoke an API key. Subsequent requests using the revoked key return 401. The list endpoint continues to show revoked keys (with revoked: true) for audit.
Status codes
| 204 | No content | key revoked |
| 401 | Unauthorized | missing or invalid Bearer API key |
| 404 | Not found | key_id does not belong to caller's tenant |
Discover the (app_id, capability_set) combinations valid for POST /v1/sessions. At MVP only deictic is in the catalog; codex + claude-code are post-MVP additions.
/v1/templatesBearer API keyReturn the agent template catalog. Catalog cross-validates against POST /v1/sessions accept-list — every catalog entry MUST be acceptable to /v1/sessions (TC-0009 contract).
Response
{
"templates": [
{
"app_id": "deictic",
"name": "Deidict (default agent)",
"description": "General-purpose multi-agent orchestration; recommended default.",
"capability_sets": [
{ "set_id": "default", "name": "Default", "tools": ["file_read", "file_write", "shell", "search"] }
]
}
]
}Status codes
| 200 | OK | catalog returned |
| 401 | Unauthorized | missing or invalid Bearer API key |
| 429 | Too many requests | tenant rate-limit hit (TC-0024) |
/v1/templates/{app_id}Bearer API keyDetail for a specific template by app_id. Returns the same shape as the catalog list, scoped to one entry.
Status codes
| 200 | OK | template found |
| 401 | Unauthorized | missing or invalid Bearer API key |
| 404 | Not found | unknown app_id; audit emits template_not_found |
Read your own current-period and historical usage. Wraps the internal billing accumulator with a customer-safe shape. M2-trimmed: cpu_seconds + sandbox_count + credits_used_usd; memory_gib_seconds + disk_gib_seconds + estimated_cost_usd are tracked post-MVP.
/v1/usageBearer API keyCurrent-period (this month) usage + credit balance for the caller's tenant. Per-tenant scope only — no cross-tenant leakage. Eventual-consistency window: ≤5min between sandbox activity + reflected here.
Response
{
"tenant_id": "usr_abc",
"period": "2026-04-01..2026-04-30",
"infra_usage": {
"cpu_seconds": 12345,
"sandbox_count": 5,
"credits_used_usd": 2.34
},
"credits": {
"balance_usd": 87.66,
"auto_topup_enabled": false
}
}Status codes
| 200 | OK | usage computed |
| 401 | Unauthorized | missing or invalid Bearer API key |
Notes
/v1/usage/historyBearer API keyHistorical usage windows. Query params from + to (ISO-8601 dates) bracket the window; default last 30 days.
Query parameters
| Name | Type | Req | Default | Description |
|---|---|---|---|---|
| from | ISO-8601 date | no | — | Start of window (inclusive). Default: 30 days ago. |
| to | ISO-8601 date | no | — | End of window (exclusive). Default: today. |
Response
{
"entries": [
{
"period_start": "2026-04-01",
"period_end": "2026-04-01",
"infra_usage": { "cpu_seconds": 12345, "sandbox_count": 0, "credits_used_usd": 0 },
"session_count": 0
}
]
}Status codes
| 200 | OK | windows returned (may be empty array) |
| 400 | Bad request | invalid from/to date format or to <= from |
| 401 | Unauthorized | missing or invalid Bearer API key |
Email + OTP flow on the legacy api.e2a.bot host. Exchange the OTP for a short-lived JWT used by every other api.e2a.bot route. New customers should use the modern /v1/tenants flow on api.e2a.bot above.
/v1/auth/registerNoneRegister a new account. Sends a one-time passcode (OTP) to the email.
Request
{ "email": "you@example.com" }Response
{ "message": "OTP sent" }Status codes
| 201 | Created | OTP dispatched |
| 400 | Bad request | missing or malformed email |
| 429 | Too many requests | rate limit per email/IP |
/v1/auth/loginNoneStart login for an existing account. Sends a one-time passcode (OTP) to the email.
Request
{ "email": "you@example.com" }Response
{ "message": "OTP sent" }Status codes
| 201 | Created | OTP dispatched |
| 400 | Bad request | missing or malformed email |
| 429 | Too many requests | rate limit per email/IP |
/v1/auth/verify-otpNoneVerify the OTP sent by register/login. Returns a JWT used for all other api.e2a.bot routes.
Request
{ "email": "you@example.com", "code": "123456" }Response
{
"token": "eyJ...", // JWT — short-lived
"user_id": "usr_abc",
"email": "you@example.com"
}Status codes
| 200 | OK | OTP valid, token issued |
| 401 | Unauthorized | OTP invalid or expired |
Create, list, and revoke Bearer API keys. Secrets are shown only once at creation.
/v1/api-keysBearer JWTCreate a new API key. The full secret is returned once; subsequent reads only return the prefix.
Response
{
"key_id": "key_abc",
"key": "e2a_live_...", // full secret — only returned here, once
"prefix": "e2a_live_abc"
}Status codes
| 201 | Created | key issued |
/v1/api-keysBearer JWTList all API keys for the authenticated user. Secrets are not returned.
Response
{
"api_keys": [
{
"key_id": "key_abc",
"prefix": "e2a_live_abc",
"revoked": false,
"created_at": "2026-04-18T12:34:56Z",
"plan_tier": "free"
}
]
}Status codes
| 200 | OK | list returned |
/v1/api-keys/{id}Bearer JWTRevoke an API key. Irreversible; revoked keys stop authenticating immediately.
Response
{ "status": "revoked" }Status codes
| 200 | OK | key revoked |
| 404 | Not found | no such key_id for this user |
Manage secret bundles for BYO LLM keys. Bundle keys are injected into sessions at create time and can be hot-replaced without restart.
/v1/tenants/me/secretsBearer API keyCreate a new secret bundle. Values are stored in AWS Secrets Manager under your tenant namespace.
Request
{
"name": "my-llm-keys", // regex: ^[a-z0-9][a-z0-9-]{0,62}$
"values": {
"LLM_API_KEY": "sk-ant-..." // LLM_API_KEY is the universal key name
}
}Response
{
"name": "my-llm-keys",
"key_names": ["LLM_API_KEY"], // values are NEVER returned
"created_at": "2026-05-01T12:00:00Z"
}Status codes
| 201 | Created | bundle stored |
| 400 | Bad request | invalid name regex, reserved key name, value exceeds 64KB, or bundle exceeds 50 keys |
| 409 | Conflict | bundle with this name already exists |
| 429 | Too many requests | tenant quota exceeded (50 bundles) |
Notes
/v1/tenants/me/secretsBearer API keyList all secret bundles. Values are NEVER returned.
Response
{
"bundles": [
{
"name": "my-llm-keys",
"key_names": ["LLM_API_KEY"],
"created_at": "2026-05-01T12:00:00Z",
"last_rotated_at": "2026-05-01T14:00:00Z"
}
]
}Status codes
| 200 | OK | list returned |
/v1/tenants/me/secrets/{name}Bearer API keyGet metadata for a specific bundle. Values are NEVER returned.
Response
{
"name": "my-llm-keys",
"key_names": ["LLM_API_KEY"],
"created_at": "2026-05-01T12:00:00Z",
"last_rotated_at": "2026-05-01T14:00:00Z"
}Status codes
| 200 | OK | bundle found |
| 404 | Not found | no such bundle |
/v1/tenants/me/secrets/{name}Bearer API keyRotate (replace) all values in an existing bundle. Triggers hot-replace to all running sessions using this bundle via WebSocket config_update frame.
Request
{
"values": {
"LLM_API_KEY": "sk-ant-new-key..." // new value
}
}Response
{
"name": "my-llm-keys",
"key_names": ["LLM_API_KEY"],
"last_rotated_at": "2026-05-01T15:00:00Z"
}Status codes
| 200 | OK | bundle rotated + broadcast sent |
| 400 | Bad request | invalid key name, reserved key, value too large |
| 404 | Not found | no such bundle |
Notes
/v1/tenants/me/secrets/{name}Bearer API keyDelete a secret bundle. Running sessions continue with their current values; future resume calls return 502 secret_bundle_not_found_at_resume.
Response
{ "status": "deleted" }Status codes
| 200 | OK | bundle deleted |
| 404 | Not found | no such bundle |
Notes
Usage and sandbox-history reads for account monitoring.
/v1/dashboard/overviewBearer JWTUsage summary — sandbox count, LLM tokens, CPU-seconds, tool-call count.
Response
{
"sandbox_count": 42,
"input_tokens": 150000,
"output_tokens": 50000,
"cpu_seconds": 3600,
"billable_tool_calls": 12
}Status codes
| 200 | OK | overview computed |
/v1/dashboard/sandboxesBearer JWTPaginated list of recent sandboxes with timing and billing metadata.
Query parameters
| Name | Type | Req | Default | Description |
|---|---|---|---|---|
| limit | integer | no | 20 | Max items per page. |
| offset | integer | no | 0 | Skip this many items. |
Response
{
"sandboxes": [ { ... } ],
"total": 42
}Status codes
| 200 | OK | page returned |
/v1/dashboard/usage/dailyBearer JWTPer-day usage breakdown for a date range.
Query parameters
| Name | Type | Req | Default | Description |
|---|---|---|---|---|
| from | string (YYYY-MM-DD) | yes | — | Start date, inclusive. |
| to | string (YYYY-MM-DD) | yes | — | End date, inclusive. |
Response
{
"entries": [
{ "day": "2026-04-01", "input_tokens": 10000, "output_tokens": 3000, "cpu_seconds": 300 }
]
}Status codes
| 200 | OK | entries returned |
Credit balance, Stripe checkout, transactions, and auto-topup configuration.
/v1/billing/balanceBearer JWTCurrent credit balance in cents (smallest currency unit).
Response
{ "credit_balance": 84750 }Status codes
| 200 | OK | balance returned |
/v1/billing/checkoutBearer JWTCreate a Stripe Checkout session to add credits. Returns a redirect URL.
Request
{ "credit_amount": 1000 }Response
{ "url": "https://checkout.stripe.com/..." }Status codes
| 200 | OK | session created |
| 400 | Bad request | invalid or below-minimum amount |
/v1/billing/transactionsBearer JWTPaginated billing transactions (top-ups, deductions, refunds).
Query parameters
| Name | Type | Req | Default | Description |
|---|---|---|---|---|
| limit | integer | no | 20 | Max items per page. |
| offset | integer | no | 0 | Skip this many items. |
Response
{
"transactions": [
{ "id": "txn_...", "amount": -25, "description": "Sandbox usage", "timestamp": "..." }
]
}Status codes
| 200 | OK | page returned |
/v1/billing/auto-topupBearer JWTRead the current auto-topup configuration.
Response
{
"enabled": true,
"threshold_amount": 100,
"topup_amount": 500
}Status codes
| 200 | OK | config returned |
/v1/billing/auto-topupBearer JWTUpdate the auto-topup configuration.
Request
{
"enabled": true,
"threshold_amount": 100,
"topup_amount": 500
}Response
{ "status": "updated" }Status codes
| 200 | OK | config persisted |
| 400 | Bad request | invalid amounts |
S3-backed persistent storage scoped to (user, app_id, capset_id). Mounted into sandboxes with workspace_enabled.
/v1/workspacesBearer JWTList all workspaces for the authenticated user.
Response
{
"workspaces": [
{ "app_id": "my-app", "capset_id": "default", "size_bytes": 1048576 }
]
}Status codes
| 200 | OK | list returned |
/v1/workspaces/{appID}/{capsetID}Bearer JWTList files inside a workspace.
Query parameters
| Name | Type | Req | Default | Description |
|---|---|---|---|---|
| prefix | string | no | — | Filter to keys beginning with this prefix. |
| limit | integer | no | 20 | Max items per page. |
| token | string | no | — | Pagination cursor returned by a previous call. |
Response
{
"files": [
{ "path": "output.json", "size": 1024, "modified": "2026-04-20T10:00:00Z" }
],
"next_token": "..." // absent when no more pages
}Status codes
| 200 | OK | page returned |
/v1/workspaces/{appID}/{capsetID}/files/*Bearer JWTDownload a single file from a workspace. Returns a presigned S3 URL to redirect to, or the raw bytes depending on negotiation.
Response
{
"url": "https://s3.../presigned...",
"expires_in": 3600
}Status codes
| 200 | OK | URL or bytes returned |
| 404 | Not found | no such file at this path |
/v1/workspaces/{appID}/{capsetID}/files/*Bearer JWTUpload a file into a workspace via a presigned S3 URL.
Response
{
"url": "https://s3.../presigned...",
"expires_in": 3600
}Status codes
| 200 | OK | presigned URL issued |
/v1/workspaces/{appID}/{capsetID}/files/*Bearer JWTDelete a single file from a workspace.
Response
{ "status": "deleted" }Status codes
| 200 | OK | file deleted |
| 404 | Not found | no such file at this path |
/v1/workspaces/{appID}/{capsetID}Bearer JWTDelete an entire workspace. All files under the (app_id, capset_id) scope are removed.
Response
{ "status": "deleted" }Status codes
| 200 | OK | workspace deleted |
| 404 | Not found | no such workspace |
/v1/workspaces/{appID}/{capsetID}/usageBearer JWTStorage usage for a workspace — size and file count.
Response
{
"total_size_mib": 4.2,
"file_count": 17
}Status codes
| 200 | OK | usage returned |
Need help? Read the quickstart guide →