{"openapi":"3.1.0","info":{"title":"agent-next sandbox gateway","version":"0.0.25","description":"E2B-compatible multi-tenant front door for agent sandboxes. Point the stock e2b SDKs at it with E2B_DOMAIN=<domain> and E2B_API_KEY=<tenant key>; the gateway owns API keys, per-key quotas, sandbox tenancy, lifecycle metering and idle auto-pause.\n\nAuth: every tenant route takes the tenant API key in the `X-API-Key` header (the e2b SDK sends it from E2B_API_KEY). /healthz, /v1/waitlist, /v1/claim, /v1/tiers, the GitHub signup redirects and the discovery docs (/openapi.json, /llms.txt, /llms-full.txt) are unauthenticated.\n\nErrors use the E2B wire envelope {\"code\": <http status>, \"message\": \"...\"} with an optional machine-readable `error_code`; 429s that are safe to retry carry an integer `Retry-After` header.\n\nThis document covers the public tenant API only; routes outside it are not part of the E2B-compatible surface."},"servers":[{"url":"https://api.{domain}","description":"control plane; api.<domain> and the data plane <port>-<sandboxID>.<domain> are both derived from the gateway domain (E2B_DOMAIN)","variables":{"domain":{"default":"sbx.agent-next.com","description":"GATEWAY_DOMAIN of the deployment"}}}],"tags":[{"name":"sandboxes","description":"sandbox lifecycle (E2B v1 and v2 dialects)"},{"name":"snapshots","description":"pause-time disk snapshots and fork"},{"name":"templates","description":"tenant-scoped template builds from an OCI image"},{"name":"usage","description":"self-serve metering and credit balance"},{"name":"signup","description":"unauthenticated waitlist signup and key claim"},{"name":"meta","description":"liveness and agent-facing discovery docs"},{"name":"billing","description":"card verification (free -> verified tier) and the Stripe webhook"},{"name":"mcp","description":"sandbox-mcp: remote MCP (JSON-RPC streamable-HTTP) over the sandbox planes"}],"paths":{"/sandboxes/{sandbox_id}/files/{name}":{"put":{"x-gateway-feature":"gpu","tags":["sandboxes"],"operationId":"uploadGpuLeaseFile","summary":"Upload raw bytes into a GPU lease OUTPUT_DIR","description":"Available only when the local accel plane or GPU node mode is enabled, for owned gl-* leases with container-backed public execution. A GPU lease has no network unless it was created with accel.network \"internet\". Streams the raw body in chunks, creates parent directories inside OUTPUT_DIR, and atomically replaces the file after completion. The name is relative to the job's OUTPUT_DIR (<workdir>/out): the job reads it there, and GET /sandboxes/{id}/files[/{name}] lists and downloads it back. Same path validation as GPU downloads: no absolute paths, empty/dot/dot-dot components, backslashes, NUL, components over 255 characters, names over 1024 characters, or more than five components. Symlinked roots, parents and existing destinations are refused. Both front and node enforce GATEWAY_GPU_MAX_UPLOAD_BYTES (default 512 MiB) on actual streamed bytes, regardless of Content-Length. Paused, frozen or transitioning leases return 409 without resuming; dead, unknown and foreign leases return 404. Leases have no dedicated disk quota; disk-write failure returns 507.","parameters":[{"$ref":"#/components/parameters/SandboxId"},{"name":"name","in":"path","required":true,"description":"Workdir-relative file path; nested components may be separated by /.","schema":{"type":"string","minLength":1,"maxLength":1024}}],"requestBody":{"required":true,"content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"responses":{"200":{"description":"Atomic upload completed","content":{"application/json":{"schema":{"type":"object","required":["name","size","sha256"],"properties":{"name":{"type":"string"},"size":{"type":"integer","minimum":0,"description":"Bytes written"},"sha256":{"type":"string","pattern":"^[0-9a-f]{64}$"}}}}}},"400":{"description":"Invalid relative file name"},"401":{"description":"Missing or revoked API key"},"402":{"description":"Credit exhausted"},"403":{"description":"Tenant suspended or not entitled"},"404":{"description":"Unknown, foreign or dead lease, or unsafe file path"},"409":{"description":"Lease paused, frozen or transitioning"},"413":{"description":"Upload exceeds the front or node byte cap"},"502":{"description":"Invalid or interrupted node upload receipt"},"503":{"description":"Node unreachable or node build lacks file uploads"},"507":{"description":"Could not write upload"}}}},"/sandboxes":{"get":{"tags":["sandboxes"],"operationId":"listSandboxes","summary":"List the calling tenant's sandboxes","description":"Proxied to the upstream, then intersected with the caller's owned set — another tenant's row can never appear. `envdAccessToken`/`trafficAccessToken` are stripped from every item (bulk surfaces carry no credentials). `state` is coerced into the SDK enum (running|paused) and `domain` rewritten to the gateway domain. The tenant-visible list is paginated gateway-side in deterministic (created_at, sandboxID) order: `order` (default desc), `limit` 1-100 (default 100) and the `nextToken` continuation cursor are enforced on both dialects; any upstream pagination headers are stripped and the gateway's own X-Next-Token / X-Total-Running are emitted instead. `metadata` filters the result (url-encoded k=v pairs, AND-ed against the stored create metadata) so a tenant can list what it owns; per-item `metadata` is filled from the stored copy when the upstream echo omits it, and `PUT /sandboxes/{sandbox_id}/metadata` merges lifecycle tags into that stored copy.","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"metadata","in":"query","required":false,"schema":{"type":"string"},"description":"url-encoded k=v pairs (metadata=team%3Dalpha); every pair must match the stored create metadata — list what you own"},{"$ref":"#/components/parameters/ListOrder"},{"$ref":"#/components/parameters/ListLimit"},{"$ref":"#/components/parameters/NextToken"}],"responses":{"200":{"description":"one page of owned sandboxes","headers":{"X-Next-Token":{"schema":{"type":"string"},"description":"present only when more pages remain; pass back verbatim as `nextToken` (opaque HMAC-signed cursor, bound to the tenant and the order direction)"},"X-Total-Running":{"schema":{"type":"integer"},"description":"the tenant's total running sandbox count; present because no `state` filter excluded running"}},"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ListedSandbox"}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"502":{"$ref":"#/components/responses/BadGateway"},"504":{"$ref":"#/components/responses/GatewayTimeout"}}},"post":{"tags":["sandboxes"],"operationId":"createSandbox","summary":"Create a sandbox (E2B v1 dialect)","description":"Passes auth, per-tenant running-quota (429 + Retry-After), credit (402), suspension (403), create-rate (429 + Retry-After) and node-capacity/host-disk admission (503 + Retry-After) gates, then forwards upstream. `templateID` resolves per tenant: own alias/id, a platform template, an owned snapshot id, or 404; omitted/`base` maps to the deployment's default template when one is configured. An omitted `timeout` defaults to 300 s, capped by SANDBOX_MAX_TIMEOUT_S; an omitted `secure` is injected `true` on secure-capable upstreams unless the deployment disabled it. A size request must take effect or fail (#427): `resourceClass` — or a `cpuCount`/`memoryMB` pair exactly matching a class spec — rewrites `templateID` to the template's SIZED_TEMPLATES variant and drops the sizing fields; a class with no variant for that template is a 400 naming the classes it does have, and any other raw pair is a 400 — unless the deployment set UPSTREAM_HONORS_CREATE_SIZING, which forwards raw values bounded by the node's template caps. SDK users cannot send `resourceClass`, so a `templateID` of the form `<alias>:<size>` (`agent-full:standard`) is the same request in the SDK dialect: it resolves to the variant that class would pick, is refused (disabled class 400, unknown size 404 naming the sizes that alias does offer, a disagreeing `resourceClass` 400) exactly as the field is, and is advertised by GET /templates; a ref that resolves on its own (a tenant's `name:tag` alias, a template id, an owned snapshot id) keeps resolving verbatim. A tenant's own template keeps its built size (a matching request is a no-op); a snapshot id never maps. The response reports the applied spec's `cpuCount`/`memoryMB` — the variant's upstream templateID never leaves the gateway. `diskSizeMB` stays bounded by the node's disk cap.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewSandbox"}}}},"responses":{"201":{"description":"created sandbox; `envdAccessToken` is the upstream's token or a gateway-minted one — send it as `X-Access-Token` on envd-port data-plane calls","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Sandbox"}}},"headers":{"X-Sandbox-Timeout-Applied":{"schema":{"type":"number"},"description":"present only for a key with a max_timeout_s cap: the keep-alive seconds applied to this request after clamping to min(SANDBOX_MAX_TIMEOUT_S, the key's cap)"}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/CreditExhausted"},"403":{"$ref":"#/components/responses/Suspended"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/QuotaExceeded"},"499":{"$ref":"#/components/responses/ClientDisconnected"},"502":{"$ref":"#/components/responses/BadGateway"},"503":{"$ref":"#/components/responses/Capacity"},"504":{"$ref":"#/components/responses/GatewayTimeout"}},"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}]}},"/v2/sandboxes":{"get":{"tags":["sandboxes"],"operationId":"listSandboxesV2","summary":"List the calling tenant's sandboxes (v2 list params)","description":"Same tenancy filtering and gateway-side pagination as GET /sandboxes. On a v1-only upstream the gateway also applies the v2 filters itself after fetching: `state`, `template`, `metadata`, `startedAfter`, `order` — an unparseable value is a 400. On a v2-capable upstream those params are relayed and the upstream validates, but `limit` and `nextToken` are always the gateway's own (none of the proxied upstreams paginate).","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"state","in":"query","required":false,"schema":{"type":"string"},"description":"comma-separated subset of `running`,`paused`; anything else is a 400 when the gateway filters"},{"name":"template","in":"query","required":false,"schema":{"type":"string"},"description":"upstream template id (compares ids, not tenant aliases)"},{"name":"metadata","in":"query","required":false,"schema":{"type":"string"},"description":"url-encoded k=v pairs; every pair must match the stored create metadata"},{"name":"startedAfter","in":"query","required":false,"schema":{"type":"string"},"description":"epoch seconds or ISO-8601 timestamp; unparseable is a 400"},{"$ref":"#/components/parameters/ListOrder"},{"$ref":"#/components/parameters/ListLimit"},{"$ref":"#/components/parameters/NextToken"}],"responses":{"200":{"description":"one page of owned sandboxes after filtering","headers":{"X-Next-Token":{"schema":{"type":"string"},"description":"present only when more pages remain; pass back verbatim as `nextToken`"},"X-Total-Running":{"schema":{"type":"integer"},"description":"the tenant's total running count; present when running sandboxes were requested (no `state` filter, or one including running)"}},"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ListedSandbox"}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"502":{"$ref":"#/components/responses/BadGateway"},"504":{"$ref":"#/components/responses/GatewayTimeout"}}},"post":{"tags":["sandboxes"],"operationId":"createSandboxV2","summary":"Create a sandbox (E2B v2 dialect, e2b SDK >= 2.51)","description":"The same auth/quota/admission/metering pipeline as POST /sandboxes. Forwarded verbatim to a v2-capable upstream; on a v1-only upstream it is translated to the v1 create and fields the v1 upstream cannot express are refused with 400 naming them: `network`, `mcp`, `iam`, `volumeMounts`, unknown fields, non-boolean `autoPause`/`secure`/`autoPauseMemory`, `autoPauseMemory: false`, a malformed `autoResume`. Both `allow_internet_access` and `allowInternetAccess` are accepted inbound; an explicit internet allow paired with the canonical deny-all network block (`networkOff: true`-equivalent) is a 400 under EGRESS_POLICY_ENABLED — the pair has no single meaning. `cpuCount`/`memoryMB`/`resourceClass` follow the same take-effect-or-400 sizing contract as POST /sandboxes (a SIZED_TEMPLATES variant applies the size; otherwise a 400) — including the `<alias>:<size>` templateID ref.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewSandboxV2"}}}},"responses":{"201":{"description":"created sandbox (v2 shape)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxV2"}}},"headers":{"X-Sandbox-Timeout-Applied":{"schema":{"type":"number"},"description":"present only for a key with a max_timeout_s cap: the keep-alive seconds applied to this request after clamping to min(SANDBOX_MAX_TIMEOUT_S, the key's cap)"}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/CreditExhausted"},"403":{"$ref":"#/components/responses/Suspended"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/QuotaExceeded"},"499":{"$ref":"#/components/responses/ClientDisconnected"},"502":{"$ref":"#/components/responses/BadGateway"},"503":{"$ref":"#/components/responses/Capacity"},"504":{"$ref":"#/components/responses/GatewayTimeout"}},"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}]}},"/v2/sandboxes/{sandbox_id}/connect":{"post":{"tags":["sandboxes"],"operationId":"connectSandboxV2","summary":"Connect to a sandbox, resuming it when paused (v2 dialect)","description":"Same connect flow as POST /sandboxes/{id}/connect with the v2 request/response dialect: forwarded verbatim to a v2-capable upstream, translated to the v1 connect body otherwise (an omitted `timeout` becomes 300 s). A paused sandbox passes the running-quota/credit start gate and queues for a start slot like a resume.","security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/SandboxId"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectSandboxV2"}}}},"responses":{"200":{"description":"the sandbox, running (v2 shape)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxV2"}}},"headers":{"X-Sandbox-Timeout-Applied":{"schema":{"type":"number"},"description":"present only for a key with a max_timeout_s cap: the keep-alive seconds applied to this request after clamping to min(SANDBOX_MAX_TIMEOUT_S, the key's cap)"}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/CreditExhausted"},"403":{"$ref":"#/components/responses/Suspended"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/QuotaExceeded"},"499":{"$ref":"#/components/responses/ClientDisconnected"},"502":{"$ref":"#/components/responses/BadGateway"},"503":{"$ref":"#/components/responses/Capacity"},"504":{"$ref":"#/components/responses/GatewayTimeout"}}}},"/sandboxes/metrics":{"get":{"tags":["sandboxes"],"operationId":"bulkSandboxMetrics","summary":"Bulk runtime metrics for owned sandboxes","description":"E2B's bulk-metrics path. `sandbox_ids` is filtered to the calling tenant's own ids before relaying and the response is intersected with the owned set on the way back — foreign ids silently vanish from both directions. A request naming only foreign ids answers {\"sandboxes\": []} without an upstream call.","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"sandbox_ids","in":"query","required":true,"schema":{"type":"string"},"description":"comma-separated sandbox ids"}],"responses":{"200":{"description":"per-sandbox metrics rows for owned ids","content":{"application/json":{"schema":{"type":"object","required":["sandboxes"],"properties":{"sandboxes":{"type":"array","items":{"$ref":"#/components/schemas/SandboxMetrics"}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"502":{"$ref":"#/components/responses/BadGateway"},"504":{"$ref":"#/components/responses/GatewayTimeout"}}}},"/sandboxes/{sandbox_id}":{"get":{"tags":["sandboxes"],"operationId":"getSandbox","summary":"Sandbox details","description":"Proxied upstream after the ownership check; a 404 upstream also records the sandbox dead locally. The owner-facing body carries the current `envdAccessToken` (re-injected when gateway-minted).","security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/SandboxId"}],"responses":{"200":{"description":"the sandbox","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Sandbox"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"502":{"$ref":"#/components/responses/BadGateway"},"504":{"$ref":"#/components/responses/GatewayTimeout"}}},"delete":{"tags":["sandboxes"],"operationId":"deleteSandbox","summary":"Kill a sandbox","description":"Upstream delete under the kill bound and the sandbox lock; an upstream 404 is the desired end state — the sandbox is recorded dead either way. The upstream response is relayed verbatim (204 on success).","security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/SandboxId"}],"responses":{"204":{"description":"killed"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"502":{"$ref":"#/components/responses/BadGateway"},"504":{"$ref":"#/components/responses/GatewayTimeout"}}}},"/sandboxes/{sandbox_id}/pause":{"post":{"tags":["sandboxes"],"operationId":"pauseSandbox","summary":"Pause a running sandbox","description":"Draws a pause slot, re-checks state under the sandbox lock, enforces the per-tenant paused quota (429 `quota_paused_exceeded` + Retry-After 5), then forwards upstream. Re-asserting pause on an already-paused sandbox marks it user-paused; whether data-plane traffic wakes it is the create's `autoResume` (default false — 502 not-found; `connect` always resumes).","security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/SandboxId"}],"responses":{"204":{"description":"paused (upstream status relayed)"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/QuotaExceeded"},"502":{"$ref":"#/components/responses/BadGateway"},"504":{"$ref":"#/components/responses/GatewayTimeout"}}}},"/sandboxes/{sandbox_id}/resume":{"post":{"tags":["sandboxes"],"operationId":"resumeSandbox","summary":"Resume a paused sandbox","description":"A resume adds a running sandbox: suspended (403), abuse-paused (403), quota (429), credit (402) and the capacity/disk admission gates (503 + Retry-After) all apply. An empty body is accepted (E2B shape): the sandbox timeout is filled in, invalid JSON answers 400. An upstream 409 'already running' is reconciled to a 200-class answer, not an error. The response carries the current `envdAccessToken`.","security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/SandboxId"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResumeSandbox"}}}},"responses":{"200":{"description":"the sandbox, running","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Sandbox"}}},"headers":{"X-Sandbox-Timeout-Applied":{"schema":{"type":"number"},"description":"present only for a key with a max_timeout_s cap: the keep-alive seconds applied to this request after clamping to min(SANDBOX_MAX_TIMEOUT_S, the key's cap)"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/CreditExhausted"},"403":{"$ref":"#/components/responses/Suspended"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/QuotaExceeded"},"499":{"$ref":"#/components/responses/ClientDisconnected"},"502":{"$ref":"#/components/responses/BadGateway"},"503":{"$ref":"#/components/responses/Capacity"},"504":{"$ref":"#/components/responses/GatewayTimeout"}}}},"/sandboxes/{sandbox_id}/connect":{"post":{"tags":["sandboxes"],"operationId":"connectSandbox","summary":"Connect to a sandbox, resuming it when paused","description":"The e2b SDK's connect path. On a paused sandbox this is a start: the same capacity/disk admission, running-quota and credit gates as resume apply. On a running sandbox the upstream call is serialized with the other token writers and the response carries the current `envdAccessToken`. The request body is optional: a missing body or non-JSON Content-Type is accepted and an omitted `timeout` defaults to the sandbox's stored keep-alive (the create/resume timeout, 300 s by default). An unknown or killed id answers 404 'sandbox <id> not found'.","security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/SandboxId"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResumeSandbox"}}}},"responses":{"200":{"description":"the sandbox, running","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Sandbox"}}},"headers":{"X-Sandbox-Timeout-Applied":{"schema":{"type":"number"},"description":"present only for a key with a max_timeout_s cap: the keep-alive seconds applied to this request after clamping to min(SANDBOX_MAX_TIMEOUT_S, the key's cap)"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/CreditExhausted"},"403":{"$ref":"#/components/responses/Suspended"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/QuotaExceeded"},"499":{"$ref":"#/components/responses/ClientDisconnected"},"502":{"$ref":"#/components/responses/BadGateway"},"503":{"$ref":"#/components/responses/Capacity"},"504":{"$ref":"#/components/responses/GatewayTimeout"}}}},"/sandboxes/{sandbox_id}/timeout":{"post":{"tags":["sandboxes"],"operationId":"setSandboxTimeout","summary":"Set the sandbox keep-alive timeout","description":"Proxied upstream for a running sandbox. A paused sandbox answers 409 without an upstream call (AgentENV would 500); an upstream 'non-running state' 5xx is translated to the same 409.","security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/SandboxId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetTimeout"}}}},"responses":{"204":{"description":"timeout updated (upstream status relayed)","headers":{"X-Sandbox-Timeout-Applied":{"schema":{"type":"number"},"description":"present only for a key with a max_timeout_s cap: the keep-alive seconds applied to this request after clamping to min(SANDBOX_MAX_TIMEOUT_S, the key's cap)"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"502":{"$ref":"#/components/responses/BadGateway"},"504":{"$ref":"#/components/responses/GatewayTimeout"}}}},"/sandboxes/{sandbox_id}/snapshots":{"post":{"tags":["snapshots"],"operationId":"createSnapshot","summary":"Snapshot a sandbox's disk","description":"Snapshotting is incremental upstream (~9 s/GB), so the create does not ride one long request: the gateway records a pending snapshot under a minted id and runs the upstream call on its own budget. Within a short inline window the finished answer is relayed (201, the snapshot object); past it the answer is still 201 with the pending snapshot and `status: \"pending\"` — 201 is what the e2b SDK's create_snapshot parses, and pending-then-poll is E2B's own async build pattern. Poll the GET lists for `status` `ready`/`failed` (the SDK's SnapshotInfo drops the field — an SDK client probes readiness by attempting the restore, which 409s while pending); a `pending` snapshot is not yet a valid `templateID` (409). The snapshot id is recorded against the calling tenant because the upstream list has no per-org filter. A paused source is left to the upstream to decide, except an abuse-paused one (403).","security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/SandboxId"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSnapshot"}}}},"responses":{"201":{"description":"the new snapshot — ready when the inline window covered the create, `status: \"pending\"` otherwise (poll the lists)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Snapshot"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Suspended"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"description":"too many snapshots already pending for this key (Retry-After)"},"502":{"$ref":"#/components/responses/BadGateway"},"504":{"$ref":"#/components/responses/GatewayTimeout"}}},"get":{"tags":["snapshots"],"operationId":"listSandboxSnapshots","summary":"List this sandbox's snapshots (tenant-owned records)","description":"Served from the gateway's own snapshot records (the upstream list cannot filter per org), so a snapshot created out-of-band upstream is invisible here. Items carry a `status` (pending/ready/failed) — the poll surface for an async create (#420). `name`/`limit` (1-100, default 100) filter the set; `nextToken` is not implemented on this route and no `x-next-token` is emitted.","security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/SandboxId"},{"$ref":"#/components/parameters/SnapshotName"},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"description":"the tenant's snapshots for this sandbox","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Snapshot"}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/snapshots":{"get":{"tags":["snapshots"],"operationId":"listSnapshots","summary":"List the calling tenant's snapshots","description":"Served from the gateway's records like the sandbox-scoped list; `sandboxID` narrows it to one sandbox. Items carry a `status` (pending/ready/failed). A `ready` snapshot id is a valid `templateID` on sandbox create (E2B's restore path) for the owning tenant.","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"sandboxID","in":"query","required":false,"schema":{"type":"string"},"description":"restrict the list to one sandbox"},{"$ref":"#/components/parameters/SnapshotName"},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"description":"the tenant's snapshots","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Snapshot"}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/snapshots/{snapshot_id}":{"delete":{"tags":["snapshots"],"operationId":"deleteSnapshot","summary":"Delete one of the tenant's snapshots","description":"Deletes the calling tenant's own snapshot: the delete is forwarded upstream in the namespace a snapshot id lives in (DELETE /templates/{id}, carrying the upstream id the tenant never holds) and the tenant's record is dropped, so the lists stop serving it. The identifier may be the snapshotID or one of its names (the same match GET /snapshots?name= applies). A foreign or unknown id answers the same 404 with no upstream call; an upstream 409/502 is forwarded and keeps the record. The e2b SDK's delete_snapshot instead calls DELETE /templates/{snapshotID}, which resolves the same way.","security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/SnapshotId"}],"responses":{"204":{"description":"deleted"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"502":{"$ref":"#/components/responses/BadGateway"}}}},"/sandboxes/{sandbox_id}/fork":{"post":{"tags":["sandboxes"],"operationId":"forkSandbox","summary":"Fork a sandbox into N clones (E2B v4 shape)","description":"Each fork is a new sandbox passing the create gate (running quota 429, credit 402, create rate limit) and recorded as the tenant's own. The upstream forks one sandbox per call, so `count` fans out into per-fork upstream calls; a first-fork upstream refusal is the whole request's answer. A paused source forks from its pause checkpoint: the upstream refuses to fork or snapshot a paused sandbox, so the gateway resumes it, snapshots it, pauses it again and creates from that snapshot behind the same call (`count` honored; the source ends paused under its original pause label but runs, and is metered, for the checkpoint, and needs a free running-sandbox slot like a resume). When the checkpoint cannot be taken it is a 409 and the source is paused again.","security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"},{"$ref":"#/components/parameters/SandboxId"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForkSandbox"}}}},"responses":{"201":{"description":"one entry per requested fork — `sandbox` on success, `error` once the per-fork pipeline failed (later forks share the verdict)","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ForkResult"}}}},"headers":{"X-Sandbox-Timeout-Applied":{"schema":{"type":"number"},"description":"present only for a key with a max_timeout_s cap: the keep-alive seconds applied to this request after clamping to min(SANDBOX_MAX_TIMEOUT_S, the key's cap)"}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/CreditExhausted"},"403":{"$ref":"#/components/responses/Suspended"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/QuotaExceeded"},"499":{"$ref":"#/components/responses/ClientDisconnected"},"502":{"$ref":"#/components/responses/BadGateway"},"503":{"$ref":"#/components/responses/Capacity"},"504":{"$ref":"#/components/responses/GatewayTimeout"}}}},"/sandboxes/{sandbox_id}/prompt":{"post":{"tags":["sandboxes"],"operationId":"postPrompt","summary":"Queue a headless agent task on a sandbox (managed prompt)","description":"Runs agent-prompt <harness> -- <task> inside the sandbox via the envd data plane; envs apply to that run only and are never stored. Start-gated like create (402 credit, 403 suspended, 429 rate / live-prompt cap); done requires an observed exit 0. Observe with GET /sandboxes/{sandbox_id}/events.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["task"],"properties":{"task":{"type":"string","maxLength":65536},"harness":{"type":"string","maxLength":32,"default":"codex"},"envs":{"type":"object","additionalProperties":{"type":"string"},"maxItems":32},"timeout_s":{"type":"number","minimum":1,"maximum":3600,"default":600}}}}}},"responses":{"202":{"description":"prompt queued","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"invalid body (task/harness/envs/timeout_s)"},"401":{"description":"missing or invalid API key"},"402":{"$ref":"#/components/responses/CreditExhausted"},"403":{"$ref":"#/components/responses/Suspended"},"404":{"description":"sandbox not found (or not yours)"},"429":{"$ref":"#/components/responses/QuotaExceeded"}}}},"/sandboxes/{sandbox_id}/prompts/{prompt_id}":{"get":{"tags":["sandboxes"],"operationId":"getPromptStatus","summary":"One managed prompt's state","security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"prompt status","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"prompt or sandbox not found (or not yours)"}}}},"/sandboxes/{sandbox_id}/events":{"get":{"tags":["sandboxes"],"operationId":"listPromptEvents","summary":"The sandbox's managed-prompt event log (ordered, cursor-paginated)","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"after","in":"query","schema":{"type":"integer","minimum":0},"description":"return only events with seq > after"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":500,"default":100}}],"responses":{"200":{"description":"events page","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/sandboxes/{sandbox_id}/network":{"get":{"x-gateway-feature":"egress-policy","tags":["sandboxes"],"operationId":"getNetwork","summary":"Read back the sandbox's stored egress policy","description":"Available only when the deployment enables EGRESS_POLICY_ENABLED (absent otherwise — the path then falls through to the generic sandbox subpath proxy). Gateway-local: the gateway answers from the allowOut/denyOut lists it stored at create time and on PUT network — no upstream call, so a paused sandbox answers too, and an out-of-band upstream mutation could diverge from this view (source says whose it is). No stored policy answers `{}` — no stored policy, not a promise of open: an explicit `allow_internet_access: false` (or its camelCase twin on the v2 create and fork dialects, which document it) stores the deny-all it means, but the camelCase spelling on the v1 create dialect is not that dialect's field — forwarded verbatim, ignored upstream, stored as nothing.","security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/SandboxId"}],"responses":{"200":{"description":"the stored egress policy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NetworkPolicy"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"post":{"tags":["sandboxes"],"operationId":"updateNetworkPost","summary":"E2B network controls — not supported","description":"E2B's update_network route; the beta does not ship it, so it answers a clear 501 (after auth) instead of a 404/405.","security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/SandboxId"}],"responses":{"401":{"$ref":"#/components/responses/Unauthorized"},"501":{"$ref":"#/components/responses/NotImplemented"}}},"put":{"tags":["sandboxes"],"operationId":"updateNetworkPut","summary":"Replace a running sandbox's egress rules","description":"Our extension beyond the E2B wire (AgentENV deployments; an nvx upstream answers 405): the upstream replaces the sandbox's egress rules with the body's `allowOut`/`denyOut` lists — omitting a field clears it, so `{}` reopens all egress. `allowOut` entries are CIDRs, bare IPs, domains (`*.example.com`) or the presets `@packages` (language/OS package + container registries) and `@trusted` (@packages + git hosts, the Hugging Face hub, big-three cloud SDK endpoints, the gateway's own API/MCP hosts): the gateway expands presets into concrete host lists before the hop, an unknown `@name` answers 400, and the <=64-entry/<=253-byte caps are checked after expansion. A paused sandbox answers 409; the response is the upstream's.","security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/SandboxId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NetworkUpdate"}}}},"responses":{"204":{"description":"egress rules replaced (the upstream's answer)"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"502":{"$ref":"#/components/responses/BadGateway"},"504":{"$ref":"#/components/responses/GatewayTimeout"}}},"patch":{"tags":["sandboxes"],"operationId":"updateNetworkPatch","summary":"E2B network controls — not supported","description":"See POST.","security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/SandboxId"}],"responses":{"401":{"$ref":"#/components/responses/Unauthorized"},"501":{"$ref":"#/components/responses/NotImplemented"}}}},"/sandboxes/{sandbox_id}/ports":{"get":{"tags":["sandboxes"],"operationId":"listSandboxPorts","summary":"List the sandbox's data-plane port registry","description":"Every registered port of this sandbox, sorted by port (our extension beyond the E2B wire). Each entry carries the port's `public` flag, `title`, access `token` (HMAC-derived per sandbox+port — the row itself never stores credential material) and the data-plane `url` it unlocks. A port only appears here after PUT /sandboxes/{id}/ports/{port}: non-envd ports serve open by default (E2B parity) — a `public:false` row is what engages the token gate.","security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/SandboxId"}],"responses":{"200":{"description":"the registered ports","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SandboxPort"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/sandboxes/{sandbox_id}/ports/{port}":{"put":{"tags":["sandboxes"],"operationId":"putSandboxPort","summary":"Set a data-plane port's public flag and title","description":"Our extension beyond the E2B wire: registers (or updates) the port's `public` flag and optional `title`. Non-envd ports serve open by default — E2B parity, `network.allowPublicTraffic` defaults true — so `public:false` is the opt-in that engages the gate: the port's <port>-<sandbox_id>.<domain> URL then answers 401 without a credential (the port's `token` as `?_gw_token=<token>` or `Authorization: Bearer <token>`, or the sandbox's `trafficAccessToken` as the E2B `e2b-traffic-access-token` header; a recognized gateway credential is consumed on every non-envd forward, open port or gated). A sandbox created with `network.allowPublicTraffic=false` is gated on EVERY non-envd port regardless of this flag. The response carries the derived `token` and the `url`; the token is HMAC-derived, never stored. The envd port (its own fixed token gate) and out-of-range port values answer 400. Gateway-local metadata: no upstream call, so a paused sandbox's registry still updates.","security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/SandboxId"},{"$ref":"#/components/parameters/PortPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxPortUpdate"}}}},"responses":{"200":{"description":"the registered port","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxPort"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/sandboxes/{sandbox_id}/metadata":{"put":{"tags":["sandboxes"],"operationId":"updateSandboxMetadata","summary":"Merge lifecycle tags into a sandbox's stored metadata","description":"Our extension beyond the E2B wire: merges the body's tags into the metadata stored at create (the create copy is kept, the merge never replaces). The stored copy is the list `?metadata=` filter's authority, so a merge is visible to filtered lists immediately. Gateway-local state: no upstream call, so a paused sandbox is tagged the same as a running one. Answers 200 with the merged metadata. A non-object body, a body carrying the reserved `__gwIdemOp` key, or a merge that would push the stored metadata past 16 KiB is a 400 (refused, never truncated).","security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/SandboxId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"tags to merge into the stored metadata (free-form keys, JSON-safe values)"}}}},"responses":{"200":{"description":"the merged metadata","content":{"application/json":{"schema":{"type":"object","properties":{"metadata":{"type":"object"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/sandboxes/{sandbox_id}/{rest}":{"get":{"tags":["sandboxes"],"operationId":"sandboxSubpath","summary":"Read-only sandbox sub-path (SDK logs, metrics, …)","description":"Read-only fallback for SDK calls like /sandboxes/{id}/logs or /metrics. After the ownership check the cleaned sub-path is proxied upstream; dot segments, empty segments, ';', '%' and backslashes are refused 400 rather than normalized. The upstream's status and body are relayed.","security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/SandboxId"},{"name":"rest","in":"path","required":true,"schema":{"type":"string"},"description":"sub-path under /sandboxes/{sandbox_id}/ (multi-segment)"}],"responses":{"200":{"description":"whatever the upstream sub-path returns (relayed)"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"502":{"$ref":"#/components/responses/BadGateway"},"504":{"$ref":"#/components/responses/GatewayTimeout"}}}},"/templates":{"get":{"tags":["templates"],"operationId":"listTemplates","summary":"List the tenant's templates plus platform templates","description":"Tenant-scoped, served from the gateway's records (never a raw upstream proxy — that leaked every tenant's templates). Every platform template's SIZED_TEMPLATES variants are listed too, as `<alias>:<size>` rows (`agent-full:standard`) carrying the class spec — the refs an SDK create can use to pick a size, restricted to the node's enabled classes.","security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"the tenant's templates followed by platform templates","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TemplateListItem"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v3/templates":{"post":{"tags":["templates"],"operationId":"registerTemplate","summary":"Register a template (name = alias)","description":"Creates the tenant's template row (status `waiting`) and its build id; start the build with POST /v2/templates/{templateID}/builds/{buildID}. `name` is 1-128 chars of [A-Za-z0-9_-] (name:tag and `tags` are refused). `cpuCount`/`memoryMB` bound every sandbox created from the template (deployment caps; defaults 4 / 8192). Rate limits: 403 over the live-template cap, 429 + Retry-After over the hourly build budget.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterTemplate"}}}},"responses":{"202":{"description":"registered; build pending a start call","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateRegistered"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Suspended"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/QuotaExceeded"}}}},"/v2/templates/{template_id}/builds/{build_id}":{"post":{"tags":["templates"],"operationId":"startTemplateBuild","summary":"Start the OCI image import for a registered template","description":"`fromImage` is the only source: a public OCI reference whose registry host passes the deployment allowlist (SSRF guard; localhost/private IPs refused even if the regex would allow them). `fromTemplate` and `fromImageRegistry` answer 501 `build steps not supported yet; use from_image`. `steps` (RUN/ENV/WORKDIR/USER) is accepted only on a deployment that sets TEMPLATE_STEPS_ENABLED, then validated and run on the upstream build node; else the same 501. A second start on a claimed build is a 409.","security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TemplateId"},{"$ref":"#/components/parameters/BuildId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StartBuild"}}}},"responses":{"202":{"description":"build started"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Suspended"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"501":{"$ref":"#/components/responses/NotImplemented"}}}},"/templates/{template_id}/builds/{build_id}/status":{"get":{"tags":["templates"],"operationId":"templateBuildStatus","summary":"Build status (polled by the SDK's Template build)","description":"The tenant's own build row; a `building` row with an upstream id is refreshed from the upstream before answering.","security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TemplateId"},{"$ref":"#/components/parameters/BuildId"}],"responses":{"200":{"description":"build state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildStatus"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/templates/aliases/{alias}":{"get":{"tags":["templates"],"operationId":"resolveTemplateAlias","summary":"Resolve a template alias to a templateID","description":"The tenant's own alias or a platform alias; everything else is the same 404 (foreign aliases are not revealed). A listed `<alias>:<size>` ref resolves to itself.","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"alias","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"the resolved template","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateAlias"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/templates/{template_id}":{"delete":{"tags":["templates"],"operationId":"deleteTemplate","summary":"Delete a tenant template","description":"Deletes the upstream template too when one was imported; an upstream 409 (template in use) and 5xx are mapped to 409/502. A path id that is not one of the tenant's templates but is one of its snapshots (id or name) deletes that snapshot instead — this is the route the e2b SDK's delete_snapshot calls (DELETE /templates/{snapshotID}); a foreign or unknown id answers the same 404.","security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/TemplateId"}],"responses":{"204":{"description":"deleted"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"502":{"$ref":"#/components/responses/BadGateway"}}},"get":{"tags":["templates"],"operationId":"getTemplate","summary":"Get one template with its builds","description":"Tenant-scoped detail (TemplateWithBuilds = listing row + buildID/buildStatus/buildCount); 404 for another tenant's or an unknown id. A listed `<alias>:<size>` ref resolves to its synthetic row.","security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"TemplateWithBuilds","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateListItem"}}}},"404":{"$ref":"#/components/responses/NotFound"},"401":{"$ref":"#/components/responses/Unauthorized"}}},"patch":{"tags":["templates"],"operationId":"retagTemplate","summary":"Retag a template's alias","description":"Alias retag only (compat/e2b v0.4 #15). Body: {\"aliases\": [\"name\"]}; returns the updated template row.","security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"updated template row","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateListItem"}}}},"404":{"$ref":"#/components/responses/NotFound"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/v0/usage":{"get":{"tags":["usage"],"operationId":"getUsage","summary":"Usage report + lifetime credit balance (gateway-only route)","description":"Windowed per-group usage reconstructed from the sandbox lifecycle events, plus the lifetime credit summary (the budget does not reset). `group_by` is `sandbox` or `metadata.<key>`; anything else is a 400.","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"from","in":"query","required":false,"schema":{"type":"string"},"description":"window start: epoch seconds or ISO-8601 (default 0)"},{"name":"to","in":"query","required":false,"schema":{"type":"string"},"description":"window end: epoch seconds or ISO-8601 (default now)"},{"name":"group_by","in":"query","required":false,"schema":{"type":"string","default":"sandbox"},"description":"`sandbox` or `metadata.<key>`"}],"responses":{"200":{"description":"usage groups and the credit summary","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageReport"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v0/account":{"get":{"tags":["usage"],"operationId":"getAccount","summary":"Caller's key facts: tenant, limits, tier (gateway-only route)","description":"Read-only; identifiers by prefix only, never the secret or its hash. Browser-callable from SITE_ORIGIN.","security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Account"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/keys/rotate":{"post":{"tags":["usage"],"operationId":"rotateKey","summary":"Replace the caller's API key (gateway-only route)","description":"Authenticated by the current key. The tenant keeps its credit, usage, sandboxes, snapshots and webhooks; the old secret stops working in the same commit and the new one is returned once. A second rotation with the already-replaced key answers 401. Browser-callable from SITE_ORIGIN.","security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"the new key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RotatedKey"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/waitlist":{"post":{"tags":["signup"],"operationId":"joinWaitlist","summary":"Waitlist signup (no auth)","description":"Per-client-IP rate-limited (429) and body-capped (413). The 202 is identical for a new and an already-stored email — it never reveals whether an address is known — but its `claim_token` only redeems for the first signup of an address; only the token's sha256 is stored. The body may also be form-encoded (`application/x-www-form-urlencoded`, e.g. `curl -d email=...` or a plain HTML form post, no JavaScript needed); a form post that sends `Accept: text/html` (a browser) gets a self-contained HTML page with the claim token and the claim command instead of JSON, for the 202 and for the 400/413/429 refusals.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaitlistJoin"}},"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/WaitlistJoin"}}}},"responses":{"202":{"description":"accepted (or indistinguishably accepted)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaitlistJoined"}},"text/html":{"schema":{"type":"string"}}}},"400":{"description":"invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}},"text/html":{"schema":{"type":"string"}}}},"413":{"description":"request body over the cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}},"text/html":{"schema":{"type":"string"}}}},"429":{"description":"per-client-IP rate limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}},"text/html":{"schema":{"type":"string"}}}}}}},"/v1/claim":{"post":{"tags":["signup"],"operationId":"claimKey","summary":"Redeem a waitlist claim token for the tenant key (no auth)","description":"Once an operator approved the signup, the first claim mints the key and returns the secret exactly once (`issued`); later claims answer `claimed` with the key id only. Pending/rejected signups answer 200 with their status. Unknown tokens also answer 200 {\"status\": \"pending\"}, identical to an unapproved claim, so repeat signup tokens do not disclose whether an email is stored. The body may be form-encoded (`curl -d claim_token=...`); the answer is always JSON.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimRequest"}},"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/ClaimRequest"}}}},"responses":{"200":{"description":"claim outcome","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"413":{"$ref":"#/components/responses/TooLarge"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/healthz":{"get":{"tags":["meta"],"operationId":"healthz","summary":"Liveness","description":"The process is up and serving requests. This is a liveness probe only: readiness (DB, upstream and admission checks) is not part of the public surface.","security":[],"responses":{"200":{"description":"alive","content":{"application/json":{"schema":{"type":"object","required":["status","version"],"properties":{"status":{"type":"string","enum":["ok"]},"version":{"type":"string","description":"Release version (pyproject.toml project.version)"}}}}}}}}},"/openapi.json":{"get":{"tags":["meta"],"operationId":"openapiSpec","summary":"This OpenAPI document","security":[],"responses":{"200":{"description":"the spec, parsed once at startup and served as JSON","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/llms.txt":{"get":{"tags":["meta"],"operationId":"llmsTxt","summary":"Short agent onboarding (llms.txt)","security":[],"responses":{"200":{"description":"curated quickstart text","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/llms-full.txt":{"get":{"tags":["meta"],"operationId":"llmsFullTxt","summary":"Full endpoint reference (llms-full.txt)","security":[],"responses":{"200":{"description":"every public route in plain text","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/auth.md":{"get":{"tags":["meta"],"operationId":"authMd","summary":"Agent registration contract (auth.md)","security":[],"responses":{"200":{"description":"the cold-start registration contract, markdown","content":{"text/markdown":{"schema":{"type":"string"}}}}}}},"/.well-known/oauth-protected-resource":{"get":{"tags":["meta"],"operationId":"oauthProtectedResource","summary":"RFC 9728 discovery with an agent_auth extension","security":[],"responses":{"200":{"description":"resource + agent_auth block pointing at /auth.md","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/v2/templates/{template_id}":{"patch":{"tags":["templates"],"operationId":"retagTemplate","summary":"Retag a template's alias","description":"Alias retag only (compat/e2b v0.4 #15). Body: {\"aliases\": [\"name\"]}; returns the updated template row.","security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"updated template row","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateListItem"}}}},"404":{"$ref":"#/components/responses/NotFound"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/v2/templates":{"get":{"tags":["templates"],"operationId":"listTemplatesV2","summary":"List templates (v2 shape)","description":"Same tenant scoping as GET /templates but each row omits buildID/buildStatus/buildCount (compat/e2b v0.4 #13).","security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"array of Template rows","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TemplateListItem"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/sandboxes/{sandbox_id}/refreshes":{"post":{"tags":["sandboxes"],"operationId":"refreshSandbox","summary":"Renew a sandbox's timeout","description":"Renews the running sandbox's timeout (compat/e2b v0.4 #17). Ownership like pause; a refresh never pauses and counts as liveness for the idle reaper. Body mirrors the timeout route ({\"timeout\": seconds}). A key with a max_timeout_s cap has `duration` clamped to it.","security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"refreshed (upstream response forwarded)","headers":{"X-Sandbox-Timeout-Applied":{"schema":{"type":"number"},"description":"present only for a key with a max_timeout_s cap: the keep-alive seconds applied to this request after clamping to min(SANDBOX_MAX_TIMEOUT_S, the key's cap)"}}},"404":{"$ref":"#/components/responses/NotFound"},"401":{"$ref":"#/components/responses/Unauthorized"},"502":{"$ref":"#/components/responses/BadGateway"},"504":{"$ref":"#/components/responses/GatewayTimeout"}}}},"/events/webhooks":{"post":{"tags":["webhooks"],"operationId":"createWebhook","summary":"Register a lifecycle webhook","description":"compat/e2b v0.5 #20. Body: {name, url, events[], signatureSecret, enabled?} — events are sandbox.lifecycle.{created,killed,updated,paused,resumed,checkpointed,credit_warning}. Answers 201 with the registration. The url is validated (SSRF guard): https only on port 443 and it must resolve only to public unicast addresses (loopback, the 169.254.169.254 metadata service, RFC1918/CGNAT private and the IPv6 ULA range fc00::/7 are refused 400; a resolve slower than 5 s is refused 400 too). At most WEBHOOK_MAX_PER_KEY (default 20) endpoints per key; the create over the cap answers 409.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"201":{"description":"The registered webhook","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"get":{"tags":["webhooks"],"operationId":"listWebhooks","summary":"List the calling tenant's webhooks","security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Webhook registrations","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/events/webhooks/{webhook_id}":{"get":{"tags":["webhooks"],"operationId":"getWebhook","summary":"Fetch one webhook registration","security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"The webhook","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"Not owned or unknown","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"patch":{"tags":["webhooks"],"operationId":"patchWebhook","summary":"Update name/url/events/enabled/signatureSecret","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"The updated webhook","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"Not owned or unknown","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"delete":{"tags":["webhooks"],"operationId":"deleteWebhook","summary":"Delete a webhook (answers 200, per the E2B docs example)","security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"{deleted: true}","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"Not owned or unknown","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/events/webhooks/{webhook_id}/deliveries":{"get":{"tags":["webhooks"],"operationId":"listWebhookDeliveries","summary":"Delivery attempt log for one webhook","description":"One row per attempt (up to 3 per event, at-least-once): delivery_id (stable across the delivery's retries — the receiver dedupe key), attempt_id (new per attempt), event_id, event_type, ts, attempt, status, ok.","security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Delivery rows","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"Not owned or unknown","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/events/webhooks/{webhook_id}/rotate":{"post":{"tags":["webhooks"],"operationId":"rotateWebhookSecret","summary":"Rotate the signing secret","description":"Server-mints a new whsec_-style signing secret and returns it once, in signatureSecret. The replaced secret stays accepted for a grace window (WEBHOOK_ROTATION_GRACE_SECONDS, default 300s): deliveries dual-sign x-agent-next-signature 'v1=<new>,v1=<prev>' until it closes, so receivers should accept either token while they update their stored secret. The grace window covers only x-agent-next-signature — the legacy e2b-signature is a single value signed with the active secret and is never dual-signed, so migrate a legacy receiver to x-agent-next-signature BEFORE rotating. PATCH signatureSecret also replaces the secret but takes effect immediately with no grace.","security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"The webhook registration plus the new signing secret (signatureSecret, shown once)","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"Not owned or unknown","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/events/webhooks/{webhook_id}/stats":{"get":{"tags":["webhooks"],"operationId":"webhookStats","summary":"Aggregate delivery counters for one webhook","security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"{delivery_attempts, ok_attempts, failed_attempts, events_delivered}","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"Not owned or unknown","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/health":{"get":{"tags":["meta"],"operationId":"health","summary":"E2B-style liveness","description":"Plain-text 200, the path api.e2b.app serves.","security":[],"responses":{"200":{"description":"alive","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/mcp":{"post":{"tags":["mcp"],"operationId":"sandboxMcpPermanent","summary":"sandbox-mcp: permanent public endpoint (alias of POST /mcp/v1)","description":"The permanent public MCP endpoint on the dedicated mcp.<apex> host (OpenAI treats the MCP origin as immutable once submitted). One handler with POST /mcp/v1 mounted twice — identical auth, gates and tools; see that operation for the full contract. The dedicated host proxies only this path, the OAuth protected-resource metadata and /.well-known/openai-apps-challenge.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["jsonrpc","method"],"properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{},"method":{"type":"string","enum":["initialize","tools/list","tools/call"]},"params":{"type":"object"}}}}}},"responses":{"200":{"description":"JSON-RPC response (result or error) for the request","content":{"application/json":{"schema":{"type":"object","properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{},"result":{"type":"object"},"error":{"type":"object","properties":{"code":{"type":"integer"},"message":{"type":"string"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/mcp/v1":{"post":{"tags":["mcp"],"operationId":"sandboxMcp","summary":"sandbox-mcp: remote MCP endpoint (JSON-RPC over streamable HTTP)","description":"One remote MCP server over the existing sandbox planes: initialize -> tools/list -> tools/call (POST only, no server-initiated stream). The tools map onto the REST handlers and the data plane: sandboxes_list, sandbox_templates, sandbox_create, sandbox_exec, sandbox_exec_output, sandbox_files_read, sandbox_files_write, sandbox_preview_url, sandbox_workspace, sandbox_kill. Exec/files run the data-plane handler, so the envd auth check, suspension, admission/auto-resume and activity accounting are the data plane's own. Every tools/call result carries an in-band `meters` block (the key's credit summary). Auth: the tenant API key in `X-API-Key` or `Authorization: Bearer <key>`. Connect one-liners: https://sbx.agent-next.com/docs/agents/.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["jsonrpc","method"],"properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{},"method":{"type":"string","enum":["initialize","tools/list","tools/call"]},"params":{"type":"object"}}}}}},"responses":{"200":{"description":"JSON-RPC response (result or error) for the request","content":{"application/json":{"schema":{"type":"object","properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{},"result":{"type":"object"},"error":{"type":"object","properties":{"code":{"type":"integer"},"message":{"type":"string"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/.well-known/openai-apps-challenge":{"get":{"tags":["mcp"],"operationId":"openaiAppsChallenge","summary":"OpenAI plugin-directory domain verification token","description":"Serves the exact plain-text token shown in the OpenAI plugin portal so it can verify ownership of this MCP host (developers.openai.com/plugins/deploy/submission). The token is read from the OPENAI_APPS_CHALLENGE env var per request and stripped of surrounding whitespace; the route 404s while it is unset or blank, so the file only exists while a verification is active.","security":[],"responses":{"200":{"description":"the verification token, verbatim","content":{"text/plain":{"schema":{"type":"string"}}}},"404":{"description":"OPENAI_APPS_CHALLENGE is unset or blank"}}}},"/v0/facts":{"get":{"tags":["meta"],"operationId":"getFacts","summary":"The user-facing numbers the docs quote (no auth)","description":"Every value is read from the same constant, env or config the gateway enforces: upload_max_bytes_per_file is the PUT cap the app applies (GATEWAY_GPU_MAX_UPLOAD_BYTES, 512 MiB default; nginx caps the same path at the proxy), idle_pause_seconds is IDLE_PAUSE_SECONDS, paused_retention_days is the effective retention policy the reaper enforces (GATEWAY_PAUSED_RETENTION_DAYS / PAUSED_RETENTION_S / tier overrides); gpu holds GATEWAY_GPU_MAX_LEASES_PER_OWNER (per host; null on a front for GPU nodes, where each node enforces its own), the accel.vram_gb bounds, the idle auto-pause default and accepted range, the TIER_*_GPU_HOURS_MONTHLY allowances and the operator template catalog; tiers holds the same numbers GET /v1/tiers serves; mcp_tools is the remote MCP tools/list catalog. prices is the one docs-sourced block (\"source\": \"docs\"): billing is in beta and nothing meters the list prices, so they quote the site's pricing files — gpu class prices are read from the price sheet the pricing page renders from (null per class = no list price yet), or null whole when no sheet is readable; never an invented number.","security":[],"responses":{"200":{"description":"facts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Facts"}}}}}}},"/v1/tiers":{"get":{"tags":["signup"],"operationId":"getTiers","summary":"Self-serve tier limits (no auth)","description":"The live free and verified tier numbers (credit hours, concurrent running / paused sandboxes, GPU flag and, when GPU is on, the monthly GPU-hour allowance, the lease cap and on the free tier the largest slice) and whether signup and card verification are enabled on this gateway.","security":[],"responses":{"200":{"description":"tier table","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tiers"}}}}}}},"/v1/signup/github":{"get":{"tags":["signup"],"operationId":"signupGithub","summary":"Start GitHub signup (browser redirect, no auth)","description":"302 to GitHub's OAuth authorize page with a state bound to a short-lived cookie. One account per GitHub id; this is the no-human abuse control. 503 when the GitHub OAuth app or SITE_ORIGIN is not configured.","security":[],"responses":{"302":{"description":"redirect to GitHub"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"description":"not configured on this gateway (no silent fallback)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/signup/github/callback":{"get":{"tags":["signup"],"operationId":"signupGithubCallback","summary":"GitHub OAuth callback (browser redirect, no auth)","description":"Verifies the state cookie, exchanges the code, records the GitHub id and redirects to the site console with a one-time claim token in the URL fragment (`#claim=...`), which POST /v1/claim redeems for the free-tier key (`issued`, shown once). A GitHub id that already claimed its key is redirected with `#signup=exists`.","security":[],"responses":{"302":{"description":"redirect to the console"},"400":{"$ref":"#/components/responses/BadRequest"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/BadGateway"},"503":{"description":"not configured on this gateway (no silent fallback)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/billing/verify-card":{"post":{"tags":["billing"],"operationId":"verifyCard","summary":"Start $0 card verification (free tier -> verified)","description":"Creates a Stripe Checkout Session in setup mode (no charge) for the calling key and returns its URL. After the signed webhook confirms the card the key moves to the verified tier: the credit difference is added once and concurrency rises. One card fingerprint upgrades at most one account.","security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Stripe Checkout URL","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"$ref":"#/components/responses/Conflict"},"502":{"$ref":"#/components/responses/BadGateway"},"503":{"description":"not configured on this gateway (no silent fallback)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/stripe/webhook":{"post":{"tags":["billing"],"operationId":"stripeWebhook","summary":"Stripe webhook (Stripe-Signature auth)","description":"Authenticated by the `Stripe-Signature` header (HMAC-SHA256 with the webhook secret, 5 minute tolerance), not by an API key. Handled: `checkout.session.completed` (setup mode) and `setup_intent.succeeded`; other events are acknowledged. A provider failure answers 500 so Stripe retries.","security":[],"responses":{"200":{"description":"acknowledged"},"400":{"$ref":"#/components/responses/BadRequest"},"413":{"$ref":"#/components/responses/TooLarge"},"500":{"description":"processing failed; Stripe retries"},"503":{"description":"not configured on this gateway (no silent fallback)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"tenant API key (the value of E2B_API_KEY; `e2b_…` secrets minted via waitlist claim or the admin CLI)"}},"parameters":{"SandboxId":{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string"},"description":"the `sandboxID` from create/list"},"PortPath":{"name":"port","in":"path","required":true,"schema":{"type":"integer","minimum":1,"maximum":65535},"description":"a data-plane port (1-65535); the envd port answers 400"},"TemplateId":{"name":"template_id","in":"path","required":true,"schema":{"type":"string"},"description":"tenant template id (`tpl-…`)"},"BuildId":{"name":"build_id","in":"path","required":true,"schema":{"type":"string"},"description":"build id (`bld-…`); must belong to the template"},"SnapshotName":{"name":"name","in":"query","required":false,"schema":{"type":"string"},"description":"match a snapshot id or one of its names"},"SnapshotId":{"name":"snapshot_id","in":"path","required":true,"schema":{"type":"string"},"description":"the `snapshotID` from create/list, or one of its names"},"Limit":{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":100},"description":"max items; outside 1-100 or non-integer is a 400"},"ListOrder":{"name":"order","in":"query","required":false,"schema":{"type":"string","enum":["asc","desc"],"default":"desc"},"description":"sort by (created_at, sandboxID); any other value is a 400"},"ListLimit":{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":100},"description":"page size; outside 1-100 or non-integer is a 400"},"NextToken":{"name":"nextToken","in":"query","required":false,"schema":{"type":"string"},"description":"continuation cursor from the previous page's X-Next-Token header — opaque, HMAC-signed, bound to the tenant and the `order` direction; a tampered, foreign or direction-mismatched value is a 400"},"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Optional exactly-once key for this create: send your own opaque, key-unique value (a UUID) to make the call safe to retry after a lost response (network timeout, 5xx). The first request creates the sandbox and ties it to the key; every later request with the same account key, Idempotency-Key and request body returns that same sandbox instead of a second, billable one. Ties are retained 24 h (GATEWAY_IDEMPOTENCY_RETENTION_S); a concurrent or early retry while the first create is still in flight returns 409 idempotency_in_progress (retry shortly, same key); reusing a key with a different body returns 409 idempotency_key_reused; a create that fails before the sandbox exists releases the key immediately so a retry can create. Omit the header to keep the default (non-idempotent) behavior."}},"responses":{"BadRequest":{"description":"invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"missing, invalid or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"CreditExhausted":{"description":"running-hours credit used up","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Suspended":{"description":"tenant suspended for an abuse-policy violation, or the sandbox is abuse-paused","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"not found (or owned by another tenant — same answer)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Conflict":{"description":"state conflict (e.g. paused, build already started, alias taken)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"TooLarge":{"description":"request body over the cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"QuotaExceeded":{"description":"per-tenant quota (running or paused) exceeded; integer Retry-After header; body carries the current `running` count and the key's `limit`","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"seconds (the only Retry-After form the e2b SDK retries)"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"per-client-IP rate limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"ClientDisconnected":{"description":"the client hung up while queued or mid-upstream-call; the gateway-side state was rolled back","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotImplemented":{"description":"an E2B feature the beta does not ship","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"BadGateway":{"description":"upstream unreachable, malformed or refused","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Capacity":{"description":"node at capacity, admission queue full, or (on nodes with ADMISSION_DISK_PATH set) host disk below its free-space floor; integer Retry-After header","headers":{"Retry-After":{"schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"GatewayTimeout":{"description":"upstream did not respond in time","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"integer","description":"the HTTP status, repeated in the body (E2B wire envelope)"},"message":{"type":"string"},"error_code":{"type":"string","description":"optional machine-readable discriminator, e.g. quota_running_exceeded / quota_paused_exceeded / idempotency_in_progress (a create with this Idempotency-Key is still in flight) / idempotency_key_reused (same Idempotency-Key, different body)"},"error_id":{"type":"string","description":"present on 5xx answers: correlates the generic client body with the upstream detail in the server log"},"status":{"type":"string","description":"present on 5xx answers: the public status page URL"},"running":{"type":"integer","description":"on a quota_running_exceeded 429: the tenant's current running count"},"limit":{"type":"integer","description":"on a quota 429: the tenant key's limit that was hit"}}},"NewSandbox":{"type":"object","description":"E2B v1 create body; unlisted fields pass through to the upstream untouched. cpuCount/memoryMB/resourceClass follow the take-effect-or-400 sizing contract (a SIZED_TEMPLATES variant applies the size; anything else is a 400 unless UPSTREAM_HONORS_CREATE_SIZING), diskSizeMB the node's disk cap, timeout SANDBOX_MAX_TIMEOUT_S, and network.allowOut/denyOut are validated and canonicalised before forwarding. Under EGRESS_POLICY_ENABLED the network block may also carry networkOff (translated to the canonical deny-all), and an explicit allow_internet_access: true beside that deny-all is a 400 — the pair has no single meaning","properties":{"templateID":{"type":"string","description":"template id, tenant alias, platform alias or owned snapshot id; omitted/`base` -> the deployment default. A `<alias>:<size>` ref (`agent-full:standard`) is the SDK dialect for `resourceClass` — the stock SDKs cannot send that field — and resolves to the same SIZED_TEMPLATES variant; a ref that resolves verbatim (the tenant's own `name:tag` alias, a template id) keeps its meaning, and a size the template does not offer is a 404 naming the sizes it does"},"timeout":{"type":"integer","description":"keep-alive seconds without activity before the expiry action (pause by default); 300 when omitted; capped at the node's SANDBOX_MAX_TIMEOUT_S (default 86400) A key with its own max_timeout_s cap (set by the operator) is clamped to min(SANDBOX_MAX_TIMEOUT_S, that cap) instead of getting a 400, and the response reports the applied value in X-Sandbox-Timeout-Applied."},"metadata":{"type":"object","additionalProperties":true,"description":"tenant metadata echoed back on get/list"},"envVars":{"type":"object","additionalProperties":{"type":"string"}},"secure":{"type":"boolean","description":"envd token enforcement; injected true when omitted and the upstream supports it"},"allow_internet_access":{"type":"boolean"},"accel":{"$ref":"#/components/schemas/GpuAccel"},"autoPause":{"type":"boolean","description":"consumed by the gateway: picks the idle reaper's action when the keep-alive lapses — true/omitted -> paused (never before the IDLE_PAUSE_SECONDS idle floor, default 300 s, and up to a reaper sweep late), false -> killed (E2B's lifecycle.onTimeout=kill)"},"autoResume":{"oneOf":[{"type":"boolean"},{"type":"object","required":["enabled"],"properties":{"enabled":{"type":"boolean"}}}],"description":"data-plane auto-resume of a paused sandbox (default false — connect always resumes); true (or {\"enabled\": true}) lets data-plane traffic wake it"},"cpuCount":{"type":"integer","description":"vCPUs; applied only as a cpuCount+memoryMB pair equal to a class spec — mapped to the template's SIZED_TEMPLATES variant, else a 400 (forwarded bounded by TEMPLATE_MAX_CPU only when the deployment's upstream honors per-create sizing)"},"memoryMB":{"type":"integer","description":"memory MiB; applied only as a cpuCount+memoryMB pair equal to a class spec — mapped to the template's SIZED_TEMPLATES variant, else a 400 (forwarded bounded by TEMPLATE_MAX_MEMORY_MB only when the deployment's upstream honors per-create sizing)"},"diskSizeMB":{"type":"integer","description":"disk MiB; bounded by TEMPLATE_MAX_DISK_MB (default 30720)"},"resourceClass":{"type":"string","enum":["small","standard","large","xlarge"],"description":"named size gated by the node's RESOURCE_CLASSES allowlist; applied via the template's SIZED_TEMPLATES variant — a class with no variant for the template is a 400 naming the available classes; cannot combine with cpuCount/memoryMB"}},"additionalProperties":true},"NewSandboxV2":{"type":"object","description":"E2B v2 create body (NewSandboxV2). On a v1-only upstream, fields it cannot express (mcp, iam, volumeMounts, unknown keys — and a network block carrying anything but allowPublicTraffic, which is gateway-enforced) are a 400.","properties":{"templateID":{"type":"string"},"timeout":{"type":"integer","description":"keep-alive seconds; 300 when omitted; capped at the node's SANDBOX_MAX_TIMEOUT_S A key with its own max_timeout_s cap (set by the operator) is clamped to min(SANDBOX_MAX_TIMEOUT_S, that cap) instead of getting a 400, and the response reports the applied value in X-Sandbox-Timeout-Applied."},"metadata":{"type":"object","additionalProperties":true},"envVars":{"type":"object","additionalProperties":{"type":"string"}},"secure":{"type":"boolean"},"allow_internet_access":{"type":"boolean"},"allowInternetAccess":{"type":"boolean","description":"camelCase twin of allow_internet_access; both accepted"},"autoPause":{"type":"boolean","description":"picks the idle reaper's action when the keep-alive lapses: true/omitted -> paused (never before the IDLE_PAUSE_SECONDS idle floor, default 300 s, and up to a reaper sweep late), false -> killed (E2B's lifecycle.onTimeout=kill); the e2b SDK's lifecycle={\"on_timeout\": \"pause\"} maps to autoPause=true on the wire"},"autoPauseMemory":{"type":"boolean","description":"pause snapshot kind; only full-memory (true/omitted) is expressible on a v1 upstream — false is a 400"},"autoResume":{"oneOf":[{"type":"boolean"},{"type":"object","required":["enabled"],"properties":{"enabled":{"type":"boolean"}}}],"description":"data-plane auto-resume of a paused sandbox (default false — connect always resumes); true (or {\"enabled\": true}) lets data-plane traffic wake it"},"cpuCount":{"type":"integer","description":"vCPUs; applied only as a cpuCount+memoryMB pair equal to a class spec — mapped to the template's SIZED_TEMPLATES variant, else a 400 (forwarded bounded by TEMPLATE_MAX_CPU only when the deployment's upstream honors per-create sizing)"},"memoryMB":{"type":"integer","description":"memory MiB; applied only as a cpuCount+memoryMB pair equal to a class spec — mapped to the template's SIZED_TEMPLATES variant, else a 400 (forwarded bounded by TEMPLATE_MAX_MEMORY_MB only when the deployment's upstream honors per-create sizing)"},"diskSizeMB":{"type":"integer","description":"disk MiB; bounded by TEMPLATE_MAX_DISK_MB (a 400 unknown-field on a v1-only upstream)"},"resourceClass":{"type":"string","enum":["small","standard","large","xlarge"],"description":"named size gated by the RESOURCE_CLASSES allowlist; applied via the template's SIZED_TEMPLATES variant — a class with no variant for the template is a 400 naming the available classes"}},"additionalProperties":true},"ConnectSandboxV2":{"type":"object","description":"on a v1-only upstream only `timeout` and `memory` survive translation — any other field is a 400","properties":{"timeout":{"type":"integer","description":"keep-alive seconds; 300 when omitted; capped at the node's SANDBOX_MAX_TIMEOUT_S A key with its own max_timeout_s cap (set by the operator) is clamped to min(SANDBOX_MAX_TIMEOUT_S, that cap) instead of getting a 400, and the response reports the applied value in X-Sandbox-Timeout-Applied."},"memory":{"type":"boolean"}},"additionalProperties":true},"ResumeSandbox":{"type":"object","properties":{"timeout":{"type":"integer","description":"keep-alive seconds (send it — a bodiless resume can hit a short upstream default); capped at the node's SANDBOX_MAX_TIMEOUT_S A key with its own max_timeout_s cap (set by the operator) is clamped to min(SANDBOX_MAX_TIMEOUT_S, that cap) instead of getting a 400, and the response reports the applied value in X-Sandbox-Timeout-Applied."}},"additionalProperties":true},"SetTimeout":{"type":"object","required":["timeout"],"properties":{"timeout":{"type":"integer","description":"keep-alive seconds; capped at the node's SANDBOX_MAX_TIMEOUT_S — over it is a 400 A key with its own max_timeout_s cap (set by the operator) is clamped to min(SANDBOX_MAX_TIMEOUT_S, that cap) instead of getting a 400, and the response reports the applied value in X-Sandbox-Timeout-Applied."}},"additionalProperties":true},"CreateSnapshot":{"type":"object","properties":{"name":{"type":"string","description":"optional name attached to the snapshot"}},"additionalProperties":true},"SandboxPortUpdate":{"type":"object","description":"{public, title?} — the port's registry flags; only these two keys are accepted — anything else is a 400","required":["public"],"properties":{"public":{"type":"boolean","description":"false opts the port into the token gate; true (and every unregistered port) serves open — unless the sandbox itself was created with network.allowPublicTraffic=false, which gates every port"},"title":{"type":["string","null"],"maxLength":128,"description":"optional label for the port"}},"additionalProperties":false},"SandboxPort":{"type":"object","properties":{"port":{"type":"integer"},"public":{"type":"boolean"},"title":{"type":["string","null"]},"token":{"type":"string","description":"the port's access token — accepted on the data plane as `?_gw_token=<token>` or `Authorization: Bearer <token>`; HMAC-derived per sandbox+port, never stored"},"url":{"type":"string","description":"the port's data-plane base URL, https://<port>-<sandboxID>.<domain>"}}},"NetworkUpdate":{"type":"object","description":"{allowOut?, denyOut?, networkOff?}; an omitted list field clears that list. Only these keys are accepted — anything else is a 400. networkOff (EGRESS_POLICY_ENABLED deployments) is the one-call deny-all: true becomes denyOut [\"0.0.0.0/0\"] gateway-side (an explicit denyOut in the same body wins; combined with allowOut that is the allowlist pattern) and the key itself is never forwarded; false is consumed with no effect, and any non-boolean value (null included) is a 400","properties":{"networkOff":{"type":"boolean","description":"true = turn this sandbox's egress off: translated to the canonical denyOut [\"0.0.0.0/0\"] before the hop, so the upstream only ever sees the lists"},"allowOut":{"type":"array","items":{"type":"string","minLength":1},"maxItems":64,"description":"CIDRs, bare IPs, domains (`*.example.com`) or the presets `@packages`/`@trusted`; presets expand gateway-side and the <=64-entry cap applies after expansion. Entries are canonicalised (hosts lowercased, trailing dot stripped; IPv4-mapped IPv6 must be written as IPv4) and must be public targets — loopback, link-local, RFC1918, CGNAT, multicast, reserved/unspecified ranges, local zones and numeric-host IP spellings are a 400, and the wide forms `*`, `0.0.0.0/0`, `::/0` need the node's ALLOW_TENANT_WIDE_EGRESS flag. Entries are capped at 253 BYTES (not characters) and must be valid UTF-8"},"denyOut":{"type":"array","items":{"type":"string","minLength":1},"maxItems":64,"description":"CIDRs/IPs/domains — presets are not valid here; entries are canonicalised like allowOut, and since a deny can only narrow egress, private targets and a `/0` deny-all stay valid; entries capped at 253 bytes"}},"additionalProperties":false},"NetworkPolicy":{"type":"object","description":"GET /sandboxes/{id}/network's read-back of the stored egress policy (EGRESS_POLICY_ENABLED). No stored policy answers `{}`; otherwise both lists are the canonical entries the gateway last forwarded (empty list = that side was cleared) and `source` names the view's owner — the gateway's stored record, which an out-of-band upstream mutation could diverge from","properties":{"allowOut":{"type":"array","items":{"type":"string"},"description":"canonical allowOut entries (egress.py normalisation: CIDR/IP/hostname, presets already expanded)"},"denyOut":{"type":"array","items":{"type":"string"},"description":"canonical denyOut entries"},"source":{"type":"string","enum":["gateway"],"description":"whose view of the policy this is: the gateway's stored record, not a live upstream read"}}},"GpuAccel":{"type":"object","description":"GPU lease request, flag-gated (the key needs a GPU grant or GPU-enabled tier; 403 without, 402 with an empty GPU bucket). A free-tier key is limited to its trial: by default a slice over 2 GiB, `network: internet`, a class other than nvidia.5090, a `template` other than the default or an `idle_timeout_s` of 0 or over 600 s is a 403 with error_code `gpu_free_tier_limit` (the body's `param` names the field), and the message says how to lift it. A body carrying `accel` creates a GPU lease (`gl-*` id, 201 -> {sandboxID, templateID, phase, accel}) instead of a CPU sandbox; the CPU fields above do not apply to it. Drive the lease with POST /sandboxes/{id}/exec ({code, timeout_s}), POST .../pause, POST .../resume, GET .../files, DELETE /sandboxes/{id}. A lease is killed by a timeout_s overrun, a VRAM over-cap kill, a dead worker or the maximum lease age. See https://sbx.agent-next.com/docs/.","properties":{"tier":{"type":"string","enum":["S","M","L","XL"],"description":"fixed slice size: S 1, M 2, L 8, XL 32 GiB; an alternative to vram_gb (both given must agree, else 400). Nothing above the card's max_admit_gib (GET /v0/gpu/capacity) is admitted, so XL is not seatable on today's card"},"vram_gb":{"type":"number","minimum":0.5,"maximum":32,"default":1,"description":"VRAM slice in GiB, enforced as a hard cap; out of range or non-numeric -> 400"},"template":{"type":"string","default":"default","description":"operator-allowlisted worker image name; an unknown name -> 400 listing the valid ones (discover with GET /gpu/templates). Never an image reference"},"class":{"type":"string","pattern":"^(nvidia|amd|tpu|ascend)\\.[a-z0-9._-]+$","description":"GPU class to place the lease on: one the fleet serves now (GET /v0/gpu/capacity lists them, e.g. nvidia.5090). The lease lands only on a node that lists it, and the reply, GET and list report that class. A class nothing serves -> 400 `gpu_class_unavailable` with the available ones, nothing placed; while a node did not answer (it may serve the class) -> retryable 503 `gpu_node_unreachable`. A class the operator rents on demand that no node serves yet -> retryable 503 `gpu_class_provisioning` (Retry-After and `eta_s` are the seconds until a node should take leases), or 503 `gpu_class_no_supply` when the provider reported no stock (Retry-After 300, `alternatives` lists the classes served now). Omitted -> any node with a seat, and the reply names the class the lease got"},"reserve":{"type":"string","enum":["hold","exec"],"description":"hard-cap hosts only: what the worker does with its slice reserve between jobs. `exec` (default, for every template) hands the full slice to the job's own allocator; `hold` (an explicit opt-in) keeps it for the whole lease"},"wait_s":{"type":"number","minimum":0,"maximum":120,"default":0,"description":"opt-in server-side wait for a GPU seat. 0 (default) refuses at once when the plane is full. With wait_s > 0 the create waits up to that many seconds (idle residents are suspended for the waiter) and returns 201 as soon as a seat is admitted, or the same typed `gpu_capacity_exhausted` refusal with Retry-After when the wait expires. A class rented on demand (`gpu_class_provisioning`) is waited for the same way; `gpu_class_no_supply` returns at once. Lease-cap, credit and fair-share refusals return at once. Outside [0, 120] -> 400"}}},"ForkSandbox":{"type":"object","description":"{timeout?, count?, <create fields>}; `count` is consumed gateway-side and only the listed fields reach each upstream fork call (with the create-timeout default applied, capped by SANDBOX_MAX_TIMEOUT_S) — any other key is a 400. A fork inherits the source sandbox's size: cpuCount/memoryMB/diskSizeMB/resourceClass are a 400 here too unless the deployment's upstream honors per-request sizing; timeout (SANDBOX_MAX_TIMEOUT_S) and network validation are applied exactly like a create. EGRESS_POLICY_ENABLED deployments: the network block may carry networkOff (translated like a create) and a body without allowOut/denyOut inherits the source's stored lists per side (an explicit internet allow in the body speaks for itself and inherits nothing; paired with an explicit deny-all it is a 400, as on create)","properties":{"count":{"type":"integer","minimum":1,"maximum":128,"default":1},"timeout":{"type":"integer","description":"keep-alive seconds; capped at the node's SANDBOX_MAX_TIMEOUT_S A key with its own max_timeout_s cap (set by the operator) is clamped to min(SANDBOX_MAX_TIMEOUT_S, that cap) instead of getting a 400, and the response reports the applied value in X-Sandbox-Timeout-Applied."},"envVars":{"type":"object","additionalProperties":{"type":"string"}},"metadata":{"type":"object","additionalProperties":true},"secure":{"type":"boolean"},"allow_internet_access":{"type":"boolean"},"allowInternetAccess":{"type":"boolean","description":"camelCase twin of allow_internet_access; both accepted"},"network":{"type":"object","description":"{allowOut?, denyOut?} egress policy — validated and canonicalised like PUT /sandboxes/{id}/network (see NetworkUpdate); under EGRESS_POLICY_ENABLED the block may also carry networkOff, and an explicit internet allow beside the canonical deny-all is a 400 (the pair has no single meaning)"},"cpuCount":{"type":"integer","description":"refused — a fork inherits the source sandbox's size (forwarded bounded by TEMPLATE_MAX_CPU only when the deployment's upstream honors per-request sizing)"},"memoryMB":{"type":"integer","description":"refused — a fork inherits the source sandbox's size (forwarded bounded by TEMPLATE_MAX_MEMORY_MB only when the deployment's upstream honors per-request sizing)"},"diskSizeMB":{"type":"integer","description":"refused — a fork inherits the source sandbox's size (forwarded bounded by TEMPLATE_MAX_DISK_MB only when the deployment's upstream honors per-request sizing)"},"resourceClass":{"type":"string","enum":["small","standard","large","xlarge"],"description":"refused — a fork inherits the source sandbox's size (forwarded only when the deployment's upstream honors per-request sizing)"}},"additionalProperties":false},"ForkResult":{"type":"object","description":"one entry per requested fork","properties":{"sandbox":{"$ref":"#/components/schemas/Sandbox"},"error":{"type":"object","properties":{"code":{"type":"integer"},"message":{"type":"string"}}}}},"Sandbox":{"type":"object","description":"v1 sandbox object (upstream shape; `domain` rewritten to the gateway domain, `state` coerced to running|paused)","properties":{"sandboxID":{"type":"string"},"templateID":{"type":"string"},"clientID":{"type":"string"},"alias":{"type":"string"},"envdVersion":{"type":"string","description":"envd version as the upstream reported it; a deployment-declared version fills it only when omitted — never inflated"},"domain":{"type":"string"},"envdAccessToken":{"type":"string","description":"send as X-Access-Token on the envd port; upstream-provided or gateway-minted"},"startedAt":{"type":"string","format":"date-time"},"endAt":{"type":"string","format":"date-time"},"cpuCount":{"type":"integer"},"memoryMB":{"type":"integer"},"diskSizeMB":{"type":"integer"},"state":{"type":"string","enum":["running","paused"]},"metadata":{"type":"object","additionalProperties":true}},"additionalProperties":true},"SandboxV2":{"type":"object","description":"v2 sandbox object: no v1-only fields like state/metadata/startedAt","properties":{"templateID":{"type":"string"},"sandboxID":{"type":"string"},"clientID":{"type":"string"},"envdVersion":{"type":"string"},"envdAccessToken":{"type":"string"},"trafficAccessToken":{"type":"string"},"domain":{"type":"string"}},"additionalProperties":true},"ListedSandbox":{"type":"object","description":"Sandbox minus the credential fields: envdAccessToken and trafficAccessToken are always stripped on bulk surfaces","properties":{"sandboxID":{"type":"string"},"templateID":{"type":"string"},"clientID":{"type":"string"},"alias":{"type":"string"},"envdVersion":{"type":"string"},"domain":{"type":"string"},"startedAt":{"type":"string","format":"date-time"},"endAt":{"type":"string","format":"date-time"},"cpuCount":{"type":"integer"},"memoryMB":{"type":"integer"},"diskSizeMB":{"type":"integer"},"state":{"type":"string","enum":["running","paused"]},"metadata":{"type":"object","additionalProperties":true}},"additionalProperties":true},"SandboxMetrics":{"type":"object","description":"per-sandbox metrics row relayed from the upstream (exact upstream schema is upstream-defined)","properties":{"sandboxID":{"type":"string"},"state":{"type":"string"},"cpuUsed":{"type":"number"},"memUsedMb":{"type":"integer"}},"additionalProperties":true},"Snapshot":{"type":"object","properties":{"snapshotID":{"type":"string"},"names":{"type":"array","items":{"type":"string"}},"status":{"type":"string","enum":["pending","ready","failed"],"description":"async creates (#420): pending until the upstream create lands, then ready/failed; on list items and the pending create answer"},"error":{"type":"string","description":"client-safe failure reason, only on a failed snapshot"},"cpuCount":{"type":"integer"},"memoryMB":{"type":"integer"},"diskSizeMB":{"type":"integer"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"additionalProperties":true},"TemplateListItem":{"type":"object","properties":{"templateID":{"type":"string"},"buildID":{"type":"string"},"public":{"type":"boolean"},"aliases":{"type":"array","items":{"type":"string"}},"names":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"createdBy":{"type":["string","null"]},"lastSpawnedAt":{"type":["string","null"]},"spawnCount":{"type":"integer"},"buildCount":{"type":"integer"},"buildStatus":{"type":"string","enum":["waiting","building","ready","error"]},"cpuCount":{"type":"integer"},"memoryMB":{"type":"integer"},"diskSizeMB":{"type":"integer"}},"additionalProperties":true},"RegisterTemplate":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":128,"pattern":"^[A-Za-z0-9_-]+$","description":"the alias; `base` and platform aliases are reserved"},"cpuCount":{"type":"integer","minimum":1,"description":"vCPUs (deployment cap derived from RESOURCE_CLASSES, 4 by default)"},"memoryMB":{"type":"integer","minimum":1,"description":"memory in MiB (deployment cap derived from RESOURCE_CLASSES, 16384 by default)"},"resourceClass":{"type":"string","enum":["small","standard","large","xlarge"],"description":"named size instead of cpuCount/memoryMB — small (2 vCPU / 8192 MiB), standard (4 / 16384), large (8 / 32768), xlarge (16 / 65536); gated by the node's RESOURCE_CLASSES allowlist (default small,standard), so a disabled or unknown name answers 400 and the field cannot be combined with cpuCount/memoryMB"}},"additionalProperties":true},"TemplateRegistered":{"type":"object","properties":{"templateID":{"type":"string"},"buildID":{"type":"string"},"public":{"type":"boolean","enum":[false]},"names":{"type":"array","items":{"type":"string"}},"tags":{"type":"array","items":{"type":"string"}},"aliases":{"type":"array","items":{"type":"string"}}}},"StartBuild":{"type":"object","required":["fromImage"],"properties":{"fromImage":{"type":"string","description":"OCI image reference (name[:tag][@digest]); registry host must pass the deployment allowlist"},"steps":{"type":"array","maxItems":200,"description":"Build steps in the e2b SDK wire shape (what from_dockerfile / .run_cmd() post, parsed client-side): RUN/ENV/WORKDIR/USER, one arg each, ENV taking KEY/value pairs. `force` is forwarded. COPY/ADD, any other type, unknown keys, non-string args or over 64 KiB total args answer 400; another type or arity answers 400 naming the step. Refused entirely — 501 `build steps not supported yet; use from_image` — unless the deployment sets TEMPLATE_STEPS_ENABLED. Omit for a plain fromImage build.","items":{"type":"object"}},"startCmd":{"type":"string"},"readyCmd":{"type":"string"}},"additionalProperties":true},"BuildStatus":{"type":"object","properties":{"logs":{"type":"array","items":{}},"logEntries":{"type":"array","items":{}},"templateID":{"type":"string"},"buildID":{"type":"string"},"status":{"type":"string","enum":["waiting","building","ready","error"]},"reason":{"type":"object","properties":{"message":{"type":"string"}},"description":"present only on status=error"}}},"TemplateAlias":{"type":"object","required":["templateID","public"],"properties":{"templateID":{"type":"string"},"public":{"type":"boolean"}}},"UsageReport":{"type":"object","properties":{"from":{"type":"number"},"to":{"type":"number"},"group_by":{"type":"string"},"groups":{"type":"array","items":{"$ref":"#/components/schemas/UsageGroup"}},"credit_hours":{"type":"number"},"used_hours":{"type":"number"},"remaining_hours":{"type":"number"},"tier":{"type":["string","null"],"description":"`free`, `verified`, or null for an operator/waitlist key"},"gpu_credit_hours":{"type":"number","description":"one-time free GPU grant, separate from credit_hours"},"gpu_seconds_used":{"type":"number"},"gpu_seconds_remaining":{"type":"number"}}},"UsageGroup":{"type":"object","required":["group","running_seconds","paused_seconds","cpu_seconds","gpu_seconds"],"properties":{"group":{"type":"string"},"running_seconds":{"type":"number"},"paused_seconds":{"type":"number"},"cpu_seconds":{"type":"number","description":"host-measured CPU time fed by the node cpu-agent"},"gpu_seconds":{"type":"number","description":"billed GPU time (busy+resume rows) attributed to this sandbox; sums to gpu_seconds_used"}}},"WaitlistJoin":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"},"name":{"type":"string","maxLength":256},"org":{"type":"string","maxLength":256},"use_case":{"type":"string","maxLength":2000}},"additionalProperties":true},"WaitlistJoined":{"type":"object","required":["status","claim_token"],"properties":{"status":{"type":"string","enum":["received"]},"claim_token":{"type":"string","description":"one-time claim token (43-char urlsafe); redeem at POST /v1/claim after approval"}}},"ClaimRequest":{"type":"object","required":["claim_token"],"properties":{"claim_token":{"type":"string"}}},"ClaimResponse":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["pending","rejected","issued","claimed"]},"key":{"type":"string","description":"the tenant secret — returned exactly once, on `issued`"},"key_id":{"type":"string"},"credit_hours":{"type":"number"},"tier":{"type":"string","description":"`free` for a GitHub signup; absent for a waitlist key"}}},"Account":{"type":"object","required":["key_id","key_prefix","tenant","created_at","limits","tier"],"properties":{"key_id":{"type":"string","description":"key id prefix (first 12 characters)"},"key_prefix":{"type":"string","description":"first 8 characters of the current secret"},"tenant":{"type":"string"},"created_at":{"type":"number","description":"epoch seconds"},"limits":{"type":"object","properties":{"max_concurrent":{"type":"integer"},"max_paused":{"type":"integer"},"credit_hours":{"type":"number"},"gpu_credit_hours":{"type":"number"}}},"tier":{"type":["string","null"],"description":"null until the store carries a tier"}}},"RotatedKey":{"type":"object","required":["api_key","key_id","key_prefix"],"properties":{"api_key":{"type":"string","description":"the new secret, returned once and never logged"},"key_id":{"type":"string","description":"key id prefix (first 12 characters)"},"key_prefix":{"type":"string"}}},"Facts":{"type":"object","required":["upload_max_bytes_per_file","idle_pause_seconds","paused_retention_days","gpu","tiers","mcp_tools","prices","billing_status"],"properties":{"upload_max_bytes_per_file":{"type":"integer","description":"largest single PUT upload in bytes the app accepts (413 past it): GATEWAY_GPU_MAX_UPLOAD_BYTES over the 512 MiB default; nginx caps the same path at the proxy"},"idle_pause_seconds":{"type":"number","description":"IDLE_PAUSE_SECONDS: a running CPU sandbox pauses after this idle time"},"paused_retention_days":{"type":"number","description":"the retention the reaper enforces for an untiered key, in days: GATEWAY_PAUSED_RETENTION_DAYS with PAUSED_RETENTION_S overriding it (per-tier values sit on tiers.<tier>.paused_retention_days)"},"gpu":{"$ref":"#/components/schemas/FactsGpu"},"tiers":{"type":"object","description":"self-serve tier numbers, the same read GET /v1/tiers serves","properties":{"free":{"$ref":"#/components/schemas/FactsTier"},"verified":{"$ref":"#/components/schemas/FactsTier"}}},"mcp_tools":{"type":"array","items":{"type":"string"},"description":"names in the remote MCP tools/list answer"},"prices":{"$ref":"#/components/schemas/FactsPrices"},"billing_status":{"type":"string","enum":["beta"],"description":"billing is not open; the list prices apply once it is"}}},"FactsGpu":{"type":"object","required":["served","max_leases_per_owner_per_node","vram_gb_min","vram_gb_max","idle_autopause_default_s","idle_timeout_s_min","idle_timeout_s_max","monthly_allowance_hours","template_catalog"],"properties":{"served":{"type":"boolean","description":"a GPU plane here, or a front serving GPU through GATEWAY_GPU_NODES(_FILE)"},"max_leases_per_owner_per_node":{"type":["integer","null"],"description":"GATEWAY_GPU_MAX_LEASES_PER_OWNER: live leases one key may hold per GPU host; null on a front for GPU nodes (each node enforces its own)"},"vram_gb_min":{"type":"number","description":"smallest accel.vram_gb slice (gpu_lease.MIN_SLICE_GIB)"},"vram_gb_max":{"type":"number","description":"largest accel.vram_gb slice (gpu_lease.MAX_SLICE_GIB)"},"idle_autopause_default_s":{"type":["number","null"],"description":"GATEWAY_GPU_IDLE_AUTOPAUSE_S: default idle auto-pause of a lease that does not name accel.idle_timeout_s; null on a front for GPU nodes (each node applies its own)"},"idle_timeout_s_min":{"type":"number","description":"accepted accel.idle_timeout_s floor (0 disables the auto-pause)"},"idle_timeout_s_max":{"type":"number","description":"accepted accel.idle_timeout_s ceiling"},"monthly_allowance_hours":{"type":"object","description":"TIER_*_GPU_HOURS_MONTHLY: GPU-hours per UTC calendar month per tier (refilled, not cumulative)","properties":{"free":{"type":"number"},"verified":{"type":"number"}}},"template_catalog":{"type":"object","required":["names"],"properties":{"names":{"type":["array","null"],"items":{"type":"string"},"description":"accel.template names (the implicit default plus GATEWAY_GPU_TEMPLATES); null when this front serves the fleet's live catalog on GET /gpu/templates instead"},"note":{"type":"string"}}}}},"FactsTier":{"type":"object","required":["sandbox_hours","concurrent_sandboxes","monthly_gpu_hours","gpu_leases","gpu_max_vram_gb","paused_retention_days"],"properties":{"sandbox_hours":{"type":["number","null"],"description":"credit hours; the 402 credit-exhausted refusal enforces them regardless of any GPU plane"},"concurrent_sandboxes":{"type":["integer","null"]},"monthly_gpu_hours":{"type":["number","null"],"description":"null while this gateway serves no GPU plane"},"gpu_leases":{"type":["integer","null"]},"gpu_max_vram_gb":{"type":["number","null"]},"paused_retention_days":{"type":"number","description":"the retention the reaper enforces for this tier's keys, in days: TIER_<TIER>_PAUSED_RETENTION_S over the global policy"}}},"FactsPrices":{"type":"object","required":["source","billing_status","cpu_usd_per_consumed_vcpu_hour","memory_usd_per_configured_gib_hour","gpu_usd_per_busy_gib_hour"],"properties":{"source":{"type":"string","enum":["docs"],"description":"the docs block: billing is not open, no code meters these prices; they quote the site's pricing files"},"billing_status":{"type":"string","enum":["beta"]},"cpu_usd_per_consumed_vcpu_hour":{"type":"number","description":"the pricing page's CPU row, USD per consumed vCPU-hour"},"memory_usd_per_configured_gib_hour":{"type":"number","description":"the pricing page's memory row, USD per configured GiB-hour while running"},"gpu_usd_per_busy_gib_hour":{"type":["object","null"],"description":"per accel class the USD list price per busy GiB-hour, read from the price sheet the pricing page renders from; null per class = no list price yet, null whole = no readable sheet","additionalProperties":{"type":["number","null"]}}}},"Tiers":{"type":"object","properties":{"tiers":{"type":"array","items":{"type":"object","properties":{"tier":{"type":"string","enum":["free","verified"]},"credit_hours":{"type":"number"},"quota_running":{"type":"integer"},"quota_paused":{"type":"integer"},"gpu":{"type":"boolean"},"gpu_hours_monthly":{"type":"number","description":"GPU-hours per UTC calendar month included on this tier (present when gpu is true)"},"gpu_leases":{"type":"integer","description":"simultaneous GPU leases on this tier (present when gpu is true)"},"gpu_max_vram_gb":{"type":"number","description":"largest GPU slice in GiB; free tier only, present when gpu is true"}}}},"signup_enabled":{"type":"boolean"},"card_verification_enabled":{"type":"boolean"}}}}}}