code-runner
API Reference

REST API

Every HTTP endpoint, request/response shape, and status code.

The API gateway exposes a small, internal HTTP surface. All /v1/* endpoints require an Authorization: Bearer <EXECUTOR_API_TOKEN> header; a missing or invalid token returns 401 {"error":"unauthorized"} (compared in constant time). GET /health is the only unauthenticated route.

Base URL in local dev: http://localhost:8080.


GET /health

Unauthenticated readiness probe.

GET /health  →  200 {"status":"ok"}

POST /v1/execute

Submit code for execution. Returns immediately, before any process starts, with a job ID and the soketi channel to subscribe to.

Request body

{
  "language": "python",
  "version": "3.12",
  "files": [
    { "name": "main.py", "content": "name = input('name? ')\nprint(f'hello {name}')\n" }
  ],
  "limits": {
    "wallTimeMs": 30000, "idleMs": 10000, "cpuMs": 15000,
    "memoryMb": 128, "pids": 64, "outputKb": 512,
    "maxArtifacts": 20, "maxArtifactBytes": 4194304
  },
  "collectOutput": false
}
FieldRequiredNotes
languageyesName or alias ("python", "py", "rust", "rs", "sqlite", "sql").
versionnoOmit to use the only/most-recent match.
filesyesArray of FileInput; at least one. File names 1–255 chars. See file inputs.
limitsnoPer-request overrides; absent fields use manifest defaults.
collectOutputnotrue to persist a pullable RunResult. Defaults to false.

File inputs

Each entry in files is a FileInput. Every file is materialized under the sandbox workspace (/workspace) before the entrypoint runs.

FieldRequiredNotes
nameyesRelative path under /workspace. May contain / for subdirectories (e.g. data/input.csv). Absolute paths (/etc/passwd) and .. traversal are rejected with 400.
contentconditionalInline file body. UTF-8 text by default; base64-encoded bytes when encoding:"base64". Mutually exclusive with ref.
encodingno"utf8" (default, back-compatible) or "base64" for arbitrary bytes (xlsx, images, zip, …). Ignored when ref is set.
refconditional"sha256:<64-hex>" — a pre-uploaded content-addressed blob. Mutually exclusive with content.

A FileInput carries exactly one of content or ref. The content/ref XOR is enforced in both the API and the worker; the worker re-sanitizes every path regardless of the API (host-escape-only threat model). See Multi-file input and Large & shared files.

Responses

CodeBodyWhen
202{"jobId","channel":"private-run-<jobId>","status":"queued"}Accepted; job enqueued.
400{"error","details":[...]}Invalid body, unknown language/version, invalid base64 content, absolute or ..-traversing name, or a file with both / neither content and ref / a malformed ref.
413{"error":...}Total decoded size of all files exceeds MAX_FILES_BYTES (default 8 MiB). For base64 files the decoded byte length counts, not the larger base64 string.
429{"error":"Executor at capacity...","retryAfterMs":1000}LLEN(jobs:queue) >= MAX_QUEUE_DEPTH. Checked after manifest resolution, so invalid bodies get 400, not 429.

POST /v1/jobs/:id/start

The start-handshake signal. Call it after your client has subscribed to the job's soketi channel. See Request Lifecycle.

POST /v1/jobs/:id/start  →  202 {"ok":true}

Persists a durable start flag (start:<jobId>, with a TTL) and publishes {type:"start"} on ctrl:<jobId>. The durable flag is the source of truth: a job still waiting in the queue when /start is called has no worker subscribed to ctrl:<jobId> yet, so the publish alone would be lost — the worker reads the flag when it claims the job. This makes the handshake safe under queue backpressure (the common case when autoscaling lags a burst).


POST /v1/jobs/:id/stdin

Write a chunk to the running process's stdin.

Request body

{ "chunk": "World\n" }

Responses

CodeBodyWhen
200{"ok":true}Chunk published to stdin:<jobId>.
400{"error","details":[...]}Invalid body (missing chunk).
429{"error":"...","retryAfterMs":1000} or {"error":"...","capBytes":65536}Frame-rate cap (30 frames/s) or pending-byte cap (64 KiB) exceeded.

POST /v1/jobs/:id/stdin/close

Signal EOF to stdin (like Ctrl-D). Publishes {type:"stdin_close"} on ctrl:<jobId>.

POST /v1/jobs/:id/stdin/close  →  200 {"ok":true}

POST /v1/jobs/:id/kill

SIGKILL the sandbox. Publishes {type:"kill"} on ctrl:<jobId>.

POST /v1/jobs/:id/kill  →  200 {"ok":true}

GET /v1/jobs/:id

Poll job status.

Responses

CodeBodyWhen
200JobStatusJob found.
404{"error":"Job not found: <id>"}Unknown job id.

JobStatus

{
  "jobId": "<uuid>",
  "channel": "private-run-<uuid>",
  "language": "python",
  "version": "3.12",
  "state": "running",
  "updatedAtMs": 1700000000000
}

statequeued · starting · running · done · killed · error.


GET /v1/jobs/:id/status

Alias of GET /v1/jobs/:id — returns the same JobStatus. Provided as an explicit sub-path (mirroring /output) for late-join reconciliation: a client that subscribes to the soketi channel after the job already moved past queued missed those stage events, so it pulls the authoritative state here on subscribe instead of waiting for the next event. The React SDK's onResolveStatus callback wires this for you.

Responses

CodeBodyWhen
200JobStatusJob found.
404{"error":"Job not found: <id>"}Unknown job id.

GET /v1/jobs/:id/output

Pull the persisted RunResult for a job that ran with collectOutput: true.

Responses

CodeBodyWhen
200RunResultOutput present.
404{"error":...}Unknown id, output not collected, or past TTL.

RunResult

{
  "exitCode": 0,
  "signal": null,
  "timedOut": false,
  "idleTimedOut": false,
  "truncated": false,
  "durationMs": 312,
  "stdout": "hello World\n",
  "stderr": "",
  "artifacts": [
    { "name": "plot.png", "mimeType": "image/png", "bytes": 20481, "url": "https://.../plot.png?X-Amz-..." }
  ],
  "artifactsTruncated": false
}

For compiled languages, an optional compile field carries the build stage, kept distinct from the run stdout/stderr. It's absent for interpreted languages. A non-zero compile.exitCode means compilation failed and the run stage did not execute.

{
  "compile": {
    "exitCode": 0,
    "signal": null,
    "stdout": "",
    "stderr": "",
    "output": "",
    "durationMs": 540
  }
}

compile.output is the interleaved stdout+stderr of the compile command in emission order (mirrors Piston's compile.output).

See Artifacts & Pullable Output.


GET /v1/languages

List the languages discovered from languages/*/manifest.json. No language identifiers are hardcoded.

[
  { "language": "python", "version": "3.12", "aliases": ["py","py3","python3"], "interactive": true },
  { "language": "rust",   "version": "1.83", "aliases": ["rs"],                 "interactive": true },
  { "language": "r",      "version": "4.4",  "aliases": ["R"],                  "interactive": false },
  { "language": "sqlite", "version": "3",    "aliases": ["sql"],                "interactive": true }
]

Blobs

Content-addressed storage for large or reused input files. A file is identified by the sha256 of its bytes, uploaded once to the configured object store, and then referenced from any job by ref: "sha256:<hex>" instead of being inlined. This sidesteps the MAX_FILES_BYTES cap and re-uploading the same bytes on every run. See the Blobs guide for the full flow, dedup, and TTL/GC behaviour.

The handshake is check → presigned PUT → finalize → execute(ref). The API only presigns upload URLs (pure local crypto — no S3 call, no byte proxying) and records liveness in Redis; the bytes go client → store directly. The worker is the only party that reads blob bytes, and verifies sha256 == ref authoritatively on pull.

Both blob routes require the object store to be configured server-side (the same S3 endpoint/bucket/creds as artifacts). If it is unconfigured, /v1/blobs/* returns 501 and you should keep using inline files. See Configuration.

POST /v1/blobs/check

Ask which of a set of blobs the store already has. Missing blobs come back with a presigned PUT URL; present blobs have their idle TTL refreshed.

Request body

{ "hashes": ["sha256:9f86d0…<64 hex>", "sha256:2c26b4…<64 hex>"] }

Responses

CodeBodyWhen
200{"missing":[{"hash","uploadUrl"}],"present":["sha256:…"]}Existence checked. uploadUrl is a presigned PUT (default 15-min window).
400{"error","details":[...]}Malformed hash (not sha256:<64-hex>).
501{"error":...}Blob store unconfigured.

After receiving missing, PUT the raw bytes of each missing blob to its uploadUrl (client → store direct).

POST /v1/blobs/finalize

Record liveness for blobs whose bytes you just uploaded. The API sets blob:meta:<hash> (idle TTL) and adds membership in blobs:index. It does not read the bytes — integrity is verified by the worker on pull.

Request body

{ "hashes": ["sha256:9f86d0…<64 hex>"] }

Responses

CodeBodyWhen
200{"finalized":["sha256:…"]}Liveness recorded/refreshed.
400{"error","details":[...]}Malformed hash.
501{"error":...}Blob store unconfigured.

Then submit the job with POST /v1/execute using files: [{ name, ref: "sha256:<hex>" }]. The client.blobs.upload(buffer) SDK method wraps this entire handshake (and skips the PUT when the blob is already present).


POST /v1/channel-auth

Optional. Registered only when ENABLE_CHANNEL_AUTH=true. Intended for local demos — in production your own backend signs channel auth (see Channel Auth).

Request body

{ "socket_id": "123.456", "channel_name": "private-run-<jobId>" }

Responses

CodeBodyWhen
200{"auth":"<app_key>:<hmac>"}Channel authorized.
400{"error":"Missing required fields: socket_id, channel_name"}Missing fields.
403{"error":"Only private-run-<jobId> channels can be authorized here"}Non-private-run- channel.

Typed client

You don't have to call these by hand — the @teovilla/code-runner-sdk-node client wraps every endpoint with typed methods and typed errors.

On this page