Skip to main content

API overview

Basics​

The API starts with rc serve and listens on http://127.0.0.1:8080 by default. There is no authentication. The default is loopback only; do not expose it beyond that without an authenticating proxy in front of it.

MethodPathWhat it does
GET/healthstatus and version
GET/api/v1/languagescatalog and whether each language's image is ready
POST/api/v1/runruns files, with a single stdin or in batch
POST/api/v1/executeruns standalone code or files, with a per-request timeout

GET /health​

{"status": "ok", "version": "dev"}

version is the version stamped into the binary at release time; in a local build, dev.

GET /api/v1/languages​

[
{"name": "python", "image": "python:3.12-alpine", "ready": true},
{"name": "nodejs", "image": "node:22-alpine", "ready": false}
]

ready says whether the language's ext4 has already been built on this host. A language with ready: false is rejected with E006, as if it were not in the catalog; build the image with image build.

POST /api/v1/run​

{
"language": "python",
"files": [{"name": "main.py", "content": "print(input())"}],
"stdin": "olá"
}
FieldTypeRequiredDescription
languagestringyesname in the catalog
fileslist of {name, content}yesthe first file is the main one
stdinstring or listnoa list runs once per item, each in a fresh VM

Response with a single stdin:

{"status": "success", "stdout": "olá\n", "stderr": null, "exception": null, "executionTime": 33}

With "stdin": ["a", "b"], the response is a list, with the stdin echoed in each item:

[
{"status": "success", "stdout": "a\n", "stderr": null, "exception": null, "executionTime": 32, "stdin": "a"},
{"status": "success", "stdout": "b\n", "stderr": null, "exception": null, "executionTime": 31, "stdin": "b"}
]

The executionTime values above are illustrative.

POST /api/v1/execute​

{
"language": "python",
"code": "import time; time.sleep(2); print('ok')",
"timeout": 5
}
FieldTypeRequiredDescription
languagestringyesname in the catalog
codestringnocode of the main file, when there are no files
fileslist of {name, content}nofiles; the first one is the main one
stdinstringnostandard input
timeoutinteger (seconds)nodefault 10, maximum 300

The response has the same format as /api/v1/run.

POST /api/v1/execute/stream​

Same body as /api/v1/execute. The response is text/event-stream (Server-Sent Events): the output arrives while the program runs, and a result event closes the response.

curl -N -X POST http://127.0.0.1:8080/api/v1/execute/stream -H 'content-type: application/json' \
-d '{"language":"python","code":"import time\nprint(\"early\")\ntime.sleep(1.5)\nprint(\"late\")"}'
event: stdout
data: "early\n"

event: stdout
data: "late\n"

event: result
data: {"status":"success","exitCode":0,"exception":null,"executionTime":1534,"timedOut":false}
Eventdata
stdout, stderrthe output chunk as a JSON string (the newline comes escaped: SSE does not accept a raw newline in data)
resultstatus, exitCode, exception, executionTime (ms), timedOut, and error when present. It does not repeat stdout/stderr
error{"error": "..."}: infrastructure failure after the stream has started (e.g. the connection to the guest dropped)
  • A job rejected by validation responds with 400 JSON, just like /execute, before opening the stream. An infrastructure failure before the first event responds with 500 JSON.
  • A list stdin (batch) is not accepted on this route: 400.
  • The X-Accel-Buffering: no header disables nginx proxy buffering.
  • Languages that block-buffer stdout when the output is a pipe (C, Ruby, Perl) only deliver on the program's flush; Python runs with PYTHONUNBUFFERED=1. Details in rc run.

Response fields​

FieldDescription
status"success" when the API ran the job. It does not mean the program succeeded: see stderr and exception
stdoutstandard output
stderrerror output, or null when empty
exceptionnull, "Timeout", "Compilation failed" or the description of an infrastructure failure
executionTimeexecution time inside the VM, in ms
errorpresent on rejection (E005, E006 or the reason) and when the guest does not respond in time (E001)
stdinbatch mode only: that item's stdin

Errors and HTTP status​

SituationHTTPBody
execution completed (including a program that failed or timed out)200normal response
invalid JSON or missing required field400{"status": "failed", "error": "requisição inválida: ..."}
stdin above 1 MB400"error": "E005: stdin too long"
language outside the catalog or without an image built on this host400"error": "E006: unsupported language <nome>"
another domain limit violated (timeout outside (0, 300 s], job with no code and no files, repeated file, invalid name, too many files, code too large)400"error" with the reason
guest did not respond within the timeout plus the host grace period200{"status": "failed", "error": "E001: operation timed out"}
infrastructure failure (VM did not boot, error on the channel to the guest)500{"status": "failed", "exception": "..."}

Limits​

LimitValue
stdin≤ 1,000,000 bytes
files per jobup to 32
total code (sum of the files)≤ 4 MiB
file nameno /, no .., not starting with ., ≤ 255 bytes
timeoutdefault 10 s, maximum 300 s

Machines​

Lifecycle of persistent machines.

MethodPathBody / queryResponse
POST/api/v1/machines{"name","stack"|"image","cpus","memory_mib","disk_gib","gpu","gpu_vram_mib","env","workdir","init","workload","stop_signal","stop_timeout","stop_on_exit","net","ports":["8080:80"],"egress":{"allow_hosts",...},"network":"lab"} or {"name","runner_code":"<TOML>"} (non-empty fields win over the file)201 + machine (state: stopped)
GET/api/v1/machines—list with state and pid
GET/api/v1/machines/:name—the machine
DELETE/api/v1/machines/:name?force=true kills it if running204
POST/api/v1/machines/:name/start—200 + machine (state: running)
POST/api/v1/machines/:name/stop?timeout=N in seconds (0–300; 0 kills immediately); without it, the machine's stop_timeout (or 10)204
POST/api/v1/machines/:name/exec{"command":["argv",...],"env":["K=V"],"workdir","stdin","timeout"}{"exitCode","stdout","stderr","timedOut","executionTime"}
POST/api/v1/machines/:name/exec/streamsame bodySSE, one event per line, raw text: event: stdout / data: line1; a line that arrives split waits for the rest, and a remainder without \n is flushed before exit ({"exitCode","timedOut","executionTime"})
PUT/api/v1/machines/:name/files?path=/abs[&mode=644]raw body = content{"path","bytes"}
GET/api/v1/machines/:name/files?path=/abs—raw content (application/octet-stream)
PUT/GET/api/v1/machines/:name/files/*pathsame, with the path in the URL (files/%2Ftmp%2Fa.txt or files/tmp/a.txt)same
GET/api/v1/machines/:name/logs?tail=N&source=all|workload—console as text; workload filters the workload's output, without the marker
PUT/api/v1/machines/:name/egress{"allow_hosts","allow_host_patterns","allow_cidrs","deny_cidrs","block_private","allow_private"} (the whole policy)200 + machine; while it is running, it applies on the next connection. INVALID_STATE if running without a network card
POST/api/v1/machines/:name/pause—204; freezes the running machine (state: paused), and exec is rejected until resume
POST/api/v1/machines/:name/resume—204; a machine that is already running is not an error
POST/api/v1/machines/:name/sync—204; returns the copies of the staged volumes to the host (INVALID_STATE if stopped)
POST/api/v1/machines/:name/branch{"name":"filha"}201 + child (state: stopped), a copy-on-write copy of the disk; a running parent freezes only during the clone
GET/api/v1/machines/:name/screen—200 with the screen as JPEG; size in X-Screen-Width and X-Screen-Height (machine with a display)
POST/api/v1/machines/:name/screen/actions{"actions":[{"action":"click","x":10,"y":20}]}204; Screen control actions (click, move, drag, type, key, scroll, wait), up to 500
curl -s -X POST localhost:8080/api/v1/machines -d '{"name":"dev","stack":"python"}'
curl -s -X POST localhost:8080/api/v1/machines/dev/start
curl -s -X POST localhost:8080/api/v1/machines/dev/exec -d '{"command":["python3","-c","print(42)"]}'
# {"exitCode":0,"stdout":"42\n","stderr":"","timedOut":false,"executionTime":31}
curl -s -X PUT --data-binary @app.py 'localhost:8080/api/v1/machines/dev/files?path=/workspace/app.py'

command is an argv with no implicit shell. To use a shell, send ["sh","-c","..."].

Alternative forms accepted on create:

Alternative fieldEquivalent to
"network": true / falsenet (a string is still the network name)
"memoryMb"memory_mib
"cmd"workload
"allowedHosts", "allowedCidrs"egress.allow_hosts, egress.allow_cidrs, and they turn the network on
"nestedVirt" (or "nested")--nested; rejected with 400 unless the server is started with rc serve --allow-nested-virt

An unknown field is rejected with 400 (CONFIG_ERROR, the message names the field), on the create, exec, egress and branch routes. Silently ignoring it used to let a mistyped allowedHost create the machine without the egress restriction.

The job stream (/api/v1/execute/stream) did not change: there, data is still a JSON string, because the chunk may carry bytes without a newline.

Errors from the machine routes:

{"error":{"code":"INVALID_STATE","message":"INVALID_STATE: machine dev is not running; start it first"},"code":"INVALID_STATE"}

code also appears at the top level of the body, for clients that look for it there.

CodeHTTP
NOT_FOUND404
CONFLICT, INVALID_STATE409
CONFIG_ERROR400
RUNNER_ERROR (infrastructure)500

Internal protocol​

Between the host and the guest, the job travels over vsock in length + JSON frames. See Job request, Job response and vsock protocol.