The “drop-in replacement” framing is approximately true for the
common E2B SDK workflow (create → run code → close) and approximately
false for anything observability-adjacent. The CubeAPI/README.md
support matrix is the canonical source.
Implemented endpoints
From CubeAPI/README.md + cross-checked against CubeAPI/src/routes.rs:
| Method | Path | Role in common E2B flow |
|---|---|---|
| GET | /health | readiness probe |
| POST | /sandboxes | Sandbox.create(template=…) |
| GET | /sandboxes | Sandbox.list() |
| GET | /v2/sandboxes | v2 list w/ filters |
| GET | /sandboxes/:id | Sandbox.get(id) |
| DELETE | /sandboxes/:id | sandbox.close() |
| POST | /sandboxes/:id/pause | snapshot/pause for later resume |
| POST | /sandboxes/:id/resume | (deprecated; prefer connect) |
| POST | /sandboxes/:id/connect | attach client, auto-resume from pause |
| GET | /events/sandboxes | recent lifecycle events |
| GET | /events/sandboxes/:id | lifecycle events for one sandbox |
| GET/POST | /events/webhooks | lifecycle webhook list/create |
| GET/PATCH/DELETE | /events/webhooks/:id | lifecycle webhook inspect/update/delete |
| GET | /events/webhooks/:id/deliveries | webhook delivery-attempt history |
| POST | /sandboxes/:id/timeout | set the sandbox timeout window |
| POST | /sandboxes/:id/refreshes | refresh the timeout window |
| GET | /sandboxes/:id/logs (v1) | per-sandbox log collection |
| GET | /v2/sandboxes/:id/logs | v2 log alias |
| GET | /sandboxes/:id/metrics | CPU/memory/disk gauges for one sandbox |
| PUT | /sandboxes/:id/network | live egress policy update |
| POST | /sandboxes/:id/snapshots | durable ZFS snapshot creation |
| GET | /sandboxes/snapshots | list durable snapshots |
| GET | /templates | list jail templates (#68) |
| GET | /templates/:name | single template metadata (#68) |
| POST | /templates/build | async template import/build job |
| GET | /templates/builds/:job_id/logs | retained build output |
| GET | /templates/builds/:job_id/events | build-log SSE stream |
| GET | /pool | warm-pool status per template (#68) |
| POST | /pool/:template/warm | pre-warm N sandboxes for a template (#68) |
| POST | /pool/:template/drain | release every warm entry for a template (#68) |
This is the lifecycle. Sandbox.create → run_code → close only touches
POST /sandboxes, the WebSocket connect, and DELETE /sandboxes/:id.
All three are in the ✅ column.
Lifecycle compatibility
POST /sandboxes accepts timeoutMs and the E2B AutoResume lifecycle
block. It also accepts the E2B SDK 2.20 top-level shape
"autoPause": true, "autoResume": {"enabled": true}:
{
"timeoutMs": 600000,
"lifecycle": {
"onTimeout": "pause",
"autoResume": true
}
}
For jail-backed sandboxes and bhyve SSH guests, the reaper pauses on
timeout and envd/files/commands activity auto-resumes the sandbox before
dispatch. Detail responses
embed the lifecycle block (started_at, last_active_at, end_at,
state, onTimeout, autoResume, timeoutMs) so newer SDKs and
dashboards can render state without composing it manually. See
/appendix/auto-suspend-resume.
GET /events/sandboxes and GET /events/sandboxes/:id expose the
E2B-shaped event history for created, paused, resumed, updated,
snapshotted, and killed sandboxes. Query parameters match E2B’s read
API: limit, offset, orderAsc, and repeated types= filters. See
/appendix/lifecycle-events.
/events/webhooks implements the companion webhook management shape:
register a URL, enable it for selected lifecycle event types, sign each
POST with E2B-compatible e2b-signature headers, retry failed attempts,
and inspect recent delivery rows through
/events/webhooks/:id/deliveries.
Still missing or intentionally partial
| Surface | Status |
|---|---|
| Local IDE attach / Remote-SSH | Browser code-server is shipped; expiring local IDE credentials are shipped for SSH-backed bhyve guests. |
| Lifecycle event webhooks | Wire shape is shipped; persistence and replay are production-hardening work. |
| Per-team API keys, quotas, audit | Optional key registry, coarse route scopes, create-path per-tenant active-sandbox quotas, and /audit/events are shipped; durable org/key management remains hardening work. |
| bhyve pause/resume through the gateway | Shipped for pool-backed SSH guests and host-console guests via owner-preserving SIGSTOP/SIGCONT; paused pool entries count as in-use capacity. |
WebSocket surface
E2B uses WebSocket for the Sandbox.run_code streaming path. axum 0.7
with the ws feature handles this; CubeAPI implements it. The byte-level
protocol (JSON framing, message types) mirrors E2B’s. Client code written
against the E2B SDK does not notice the switch.
Authentication
From the quickstart:
export E2B_API_URL="http://127.0.0.1:3000"
export E2B_API_KEY="dummy"
In the open-source single-node install, E2B_API_KEY is ignored unless
the operator configures COPPICE_API_KEYS or COPPICE_API_KEYS_FILE.
When configured, the gateway requires X-API-Key or
Authorization: Bearer ... on the API and envd surfaces while keeping
/health public. See /appendix/api-key-auth.
What ports to FreeBSD
Axum and tokio are portable. CubeAPI does not touch Linux-specific
primitives in its HTTP layer; the calls it makes downward go to
CubeMaster over gRPC. So rather than cross-compile Tencent’s CubeAPI,
we built our own Axum service — e2b-compat/ — that speaks the same
E2B REST surface but dispatches downward into jails + bhyve rather
than containerd + cube-hypervisor. On honor, the E2B Python SDK
passes 10/10 of its API-server calls against this service, and the
envd-compat endpoint on :49999 streams run_code NDJSON via
jexec python3 — see
/appendix/run-code-protocol for the
wire format and the round-trip transcript.
The complexity in a FreeBSD port lives below the API layer, not at the API layer itself — as expected.
Caveats
”E2B” is a moving target
E2B’s own API has been shifting — endpoints added, versioned, deprecated. CubeAPI pins against a specific E2B version (implicitly, via the routes it chose to implement). If the E2B SDK introduces a new required endpoint, CubeAPI will either return 501 on that endpoint or lag behind until Cube adds it. “Drop-in” is pinned to a moment in time.
Template semantics
CUBE_TEMPLATE_ID in the quickstart is a Cube-specific ID that maps to
a stored VM template. E2B’s template field in Sandbox.create is
per-team on E2B cloud and maps to E2B-managed templates. Cube accepts
E2B-shaped template IDs by treating them as opaque strings and looking
them up in CubeMaster’s template registry. If you have existing
“template” IDs on E2B cloud and expect them to work unchanged
on Cube, they won’t — you upload templates to Cube separately.
Snapshots are not snapshots
E2B has two distinct concepts: /pause (stateful pause, live memory
snapshot) and /snapshots (create a durable snapshot image, e.g., to
fork from later). Cube implements the former and not the latter. If your
code calls sandbox.snapshot() expecting to fork later, it’ll get a 501.
If it calls sandbox.pause()/sandbox.resume(), it works.