HTTP API
The raw JSON contract behind the typed client — the runtime Worker's routes, the sandbox's wire protocol, and the gateway paths.
Mode: both.
@sandbox-workers/core’s typed client (see Lifecycle, Code interpreter, and Files) sits on top of two separate HTTP contracts: the runtime Worker’s own routes (stable, and useful if you call a runtime Worker directly), and the wire protocol between your Sandbox Durable Object and that runtime Worker (documented here for completeness — the Sandbox Durable Object’s own routes are internal to the client and not meant to be called directly).
The runtime Worker’s routes
Every @sandbox-workers/<language> Worker serves these routes, whether or not it has an INTERPRETER Durable Object binding.
POST /execute
Stateless execution, used by stateless mode’s free runCode() and by the stateful-mode fallback (sandbox.interpreter.runCode(code, { binding }) against a contexts: false binding): a runtime Worker always executes a single language — the runtime is chosen by the Service Binding (or, on the Playground gateway, the URL path), never by the request. Use Content-Type: application/json.
| Field | Type | Required | Meaning |
|---|---|---|---|
code |
string | Yes | Nonempty script; maximum 64 KiB UTF-8 |
envVars |
object of string values | No | Environment variables exposed to the script |
language |
string | No | Validated, not executed — see below |
The complete request is limited to 96 KiB. envVars keys must match /^[A-Za-z_][A-Za-z0-9_]*$/, and every value must be a string; null/undefined values are skipped. A request that still contains an input key is rejected — pass data with envVars.
language, when given, is only validated, never executed with — the runtime Worker always executes its own language. It accepts the same aliases as the typed client (python3 → python, js/node → javascript, ts → typescript, case-insensitively) and must equal the runtime’s language after normalization (typescript is also accepted on a javascript runtime); otherwise the request 400s with VALIDATION_FAILED and a message like Unsupported language 'python' on this runtime (javascript). Omitting language entirely always works, on every runtime.
{
"code": "const x = Number(process.env.X);\nx ** 2",
"envVars": { "X": "12" }
}
Code is a script: the value of the last top-level expression is the result. There is no persistent context on this route — every call boots a fresh Wasm instance. For a durable, stateful alternative, see “Code contexts” later on this page.
Responses
Every execution — success, a guest error, or a fuel/output/result limit — returns HTTP 200 with an ExecutionResult (see Code interpreter for the full shape and the result-mapping rules):
{
"code": "const x = Number(process.env.X);\nx ** 2",
"language": "javascript",
"engine": "SpiderMonkey 147 / goccy spidermonkey-wasm v0.2.6",
"durationMs": 8,
"logs": { "stdout": [], "stderr": [] },
"results": [{ "text": "144" }],
"usage": {
"fuelConsumed": 1800000,
"fuelLimit": 50000000,
"memoryBytes": 34668544
}
}
A guest error looks like this instead — logs produced before the error are still returned, and results is empty:
{
"code": "1 / 0",
"language": "python",
"engine": "CPython 3.14.6",
"durationMs": 5,
"logs": { "stdout": [], "stderr": [] },
"results": [],
"error": {
"name": "ZeroDivisionError",
"message": "division by zero",
"traceback": ["Traceback (most recent call last):", "..."]
}
}
Status codes
| Status | Meaning |
|---|---|
| 200 | Every execution: success, a guest error, or a fuel/output/result limit — check error |
| 400 | Invalid JSON, an unsupported /execute/<language> gateway path, invalid envVars, an input key in the body, or a language that doesn’t match the runtime (VALIDATION_FAILED) |
| 405 | Wrong HTTP method (VALIDATION_FAILED) |
| 413 | Request or code too large (VALIDATION_FAILED) |
| 415 | Unsupported Content-Type (VALIDATION_FAILED) |
| 502 | Gateway could not call a Service Binding (INTERNAL_ERROR) |
Only request/transport failures (400, 405, 413, 415, 502) use a non-200 status, with the body shaped as an ErrorResponse (see Errors) — { "code": "VALIDATION_FAILED", "message": "...", "context": {}, "httpStatus": 400, "timestamp": "..." }. There is no ok field and no 422 status — fuel exhaustion and output/result limits are reported as a 200 response with error.name set to "ExecutionLimitError".
Always check the error field, not the HTTP status, to see whether guest code succeeded.
GET /interpreter
Served by every runtime Worker without a Durable Object round trip:
{ "language": "python", "engine": "CPython 3.14.6", "contexts": true }
contexts is false for Ruby and for any runtime Worker deployed without an INTERPRETER Durable Object binding (the CLI’s --stateless flag). This is the probe a Sandbox Durable Object runs before creating a code context against a binding.
Code contexts (/interpreters/:key/*)
These routes exist only when the runtime Worker has an INTERPRETER Durable Object binding (contexts: true); otherwise every one of them answers 400 VALIDATION_FAILED (“Code contexts are not supported for ruby” / “Code contexts are not supported: this Worker has no INTERPRETER Durable Object binding”). :key is the calling Sandbox Durable Object’s own id (a 64-hex string), so two callers using the same sandbox id against the same runtime Worker never collide; it must match /^[A-Za-z0-9._-]{1,128}$/. These routes are called by the Sandbox Durable Object, not directly by application code — use sandbox.interpreter.* (see Code interpreter) instead.
| Method and path | Body | Response |
|---|---|---|
POST /interpreters/:key/contexts |
{ id, cwd } |
{ id, cwd, createdAt } (201); 400 when over 8 contexts |
DELETE /interpreters/:key/contexts/:id |
{ success: true }; 404 CONTEXT_NOT_FOUND |
|
DELETE /interpreters/:key |
{ success: true } — wipes this interpreter’s snapshots and contexts |
Context ids are minted by the sandbox and passed in on create — the interpreter never generates its own.
Running code in a context is not an HTTP route: it’s the Workers RPC method executeInContext(key, args, getFiles), called directly on the runtime Worker binding and forwarded unchanged to env.INTERPRETER.get(idFromName(key)).executeInContext(key, args, getFiles). args.envVars arrives flat and already merged (sandbox-level, context-level, and call-level envVars, computed by the sandbox) — the interpreter applies it as-is. See Workspace sync: a pull over RPC below.
Workspace sync: a pull over RPC
/workspace has one source of truth, the Sandbox Durable Object; each interpreter keeps an in-memory mirror, reconciled on every executeInContext call. args.workspace carries the shape of /workspace, never its contents:
interface WorkspaceManifest {
dirs: string[]; // every directory under /workspace (absolute paths), full list
manifest: Record<string, string>; // every file: absolute path -> content hash
}
The interpreter reconciles its mirror before running anything: create every directory in dirs, delete anything in the mirror that isn’t in manifest/dirs. If manifest still names a path the interpreter can’t match (it was evicted, or never held this workspace), it calls back getFiles(missing) — an RPC stub the sandbox passed as an argument — to pull exactly those paths’ contents, then reconciles again. A path still missing after that is INTERNAL_ERROR: the sandbox is the interpreter’s only source of truth for /workspace, so this means the sandbox itself failed to answer getFiles correctly, not a race to retry.
executeInContext never throws to report an application error — Workers RPC only serializes name/message/stack off a thrown Error, which would drop the code/details/HTTP status the sandbox needs. Its result is always one of:
type InterpreterExecuteRpcResult =
| { ok: true; result: InterpreterExecuteResponse }
| { ok: false; status: number; body: Record<string, unknown> }; // same shape an ErrorResponse would carry
A successful result’s InterpreterExecuteResponse carries ExecutionResult’s fields, the interpreter’s own view of the context, and the workspace diff:
{
...ExecutionResult, // code, language, engine, durationMs, logs, results, error?, usage?
executionCount: number,
context: { id, cwd, executions, snapshotMs?, snapshot: SnapshotInfo | null },
workspace: {
dirs: string[]; // full directory list after the run
files: Array<{ path; data: Uint8Array; updatedAt }>; // created or updated by the run
deleted: string[]; // files removed by the run
}
}
The Sandbox Durable Object applies workspace to its own tree, persists the diff, and strips workspace (and replaces context/executionCount with its own registry’s view) before answering the caller — see ExecutionResult.context in Code interpreter for the shape the client actually sees. Because this is an RPC call rather than an HTTP request, there’s no dedicated request-size cap for it — Workers RPC’s own 32 MiB serialized-message limit comfortably covers the 16 MiB workspace, and Uint8Array file contents need no base64 transcoding on the wire.
Wire protocol: client → Sandbox Durable Object
Internal to the typed client — documented here for completeness, not a contract application code should call directly. The client sends x-sandbox-id: <id> and talks to https://sandbox.internal<path> over the Durable Object namespace given to getSandbox().
| Method and path | Body | Response |
|---|---|---|
POST /execute |
{ code, contextId?, binding?, envVars? } |
ExecutionResult (+ executionCount, context: { id, cwd, executions, snapshotMs?, expiresAt? } when a context ran; neither for a stateless binding) |
POST /contexts |
{ binding, cwd?, envVars? } |
{ id, binding, language, cwd, createdAt, lastUsed } (201) |
GET /contexts |
{ contexts: [{ id, binding, language, cwd, createdAt, lastUsed }] } |
|
DELETE /contexts/:contextId |
{ success: true }; 404 CONTEXT_NOT_FOUND |
|
POST /env |
{ envVars: Record<string, string | null> } |
{ success: true } |
POST /files |
{ op, path, newPath?, content?, encoding?, recursive?, force?, includeHidden? } |
Per operation — see Files |
GET / |
SandboxInfo — see Lifecycle |
|
DELETE / |
{ success: true } — wipes storage and drops every context |
binding must match /^[A-Za-z_][A-Za-z0-9_]*$/ and resolve to a real sandbox-workers runtime Worker (see Errors: binding validation).
Gateway paths
Only the Playground gateway fronts more than one runtime Worker and hosts its own Sandbox Durable Object; it picks a runtime from the URL path.
POST /execute/<language>, where<language>isjavascript,python,perl, orruby.POST /executeon the gateway is an alias for/execute/javascript. An unsupported<language>returns 400./languages/:language/sandboxes/:idand any further sub-path forward, forGET,POST, andDELETE, to the gateway’s ownSandboxDurable Object keyed by:id— the same sandbox id reached through two languages is one sandbox with two contexts, one per binding. ForPOST .../contextsandPOST .../execute, the JSON body’sbindingis forced to:language.toUpperCase()(the gateway’s Service Binding names:JAVASCRIPT,PYTHON,PERL,RUBY), overriding anything the client sent. Every other sub-path is forwarded unchanged.
GET /languages
The Playground gateway returns {languages:[...]} with runtime IDs, names, package versions, engine names, execution modes, capabilities, and configured limits. Individual runtime Workers do not expose this route.
Raw Service Binding calls
The request URL may use any placeholder hostname — the binding determines the destination Worker. This API provides no host-network capability to the submitted code.