On Runtime, every product sits behind one HTTPS API. curl can reach
everything that the SDKs, the CLI and the MCP server can do. Money is an
integer number of microdollars. Every error has one shape, with a hint. A
create that you retry with the same Idempotency-Key never makes two sandboxes.
API version 0.2.0, checked 26 October 2026 (API reference).
Get a key into a variable
Make a key at API keys. Or, from a
terminal, run npx withruntime keys create --name curl. Keep the key in your
secret store. Load it into the shell without typing it on the command line:
Terminalread -rs RUNTIME_API_KEY && export RUNTIME_API_KEYNever add -v or --trace to a curl command that carries the key. Both print
the Authorization header.
Create, run, stop
Terminal# Create, and wait up to 60 seconds for it to be running.ID=$(curl -sS https://api.withruntime.com/v1/sandboxes \ -H "Authorization: Bearer ${RUNTIME_API_KEY}" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: build-1842" \ -H "Prefer: wait=60" \ -d '{"timeoutSeconds": 600}' | jq -r .id)# Run a command. The answer is {exitCode, stdout, stderr, timedOut}.curl -sS "https://api.withruntime.com/v1/sandboxes/${ID}:exec" \ -H "Authorization: Bearer ${RUNTIME_API_KEY}" \ -H "Content-Type: application/json" \ -d '{"command": "python3 -c \"print(sum([125, 250, 375]))\""}'# Stop it.curl -sS -X POST "https://api.withruntime.com/v1/sandboxes/${ID}:stop" \ -H "Authorization: Bearer ${RUNTIME_API_KEY}"Write "${ID}:exec" with braces. zsh, the default shell on macOS, reads $ID:e
as a modifier, and sends /xec.
Files and background processes
Terminal# Upload a file: the raw body becomes the file; parents are made.curl -sS -X PUT --data-binary @main.py \ "https://api.withruntime.com/v1/sandboxes/${ID}/files/content?path=/workspace/main.py" \ -H "Authorization: Bearer ${RUNTIME_API_KEY}" \ -H "Content-Type: application/octet-stream"# Start a server in the background, then read its output.PID=$(curl -sS "https://api.withruntime.com/v1/sandboxes/${ID}/processes" \ -H "Authorization: Bearer ${RUNTIME_API_KEY}" \ -H "Content-Type: application/json" \ -d '{"command": "python3 -m http.server 8000"}' | jq -r .id)curl -sS "https://api.withruntime.com/v1/sandboxes/${ID}/processes/${PID}/output?waitMs=5000" \ -H "Authorization: Bearer ${RUNTIME_API_KEY}"# Download a file.curl -sS -o result.txt \ "https://api.withruntime.com/v1/sandboxes/${ID}/files/content?path=/workspace/result.txt" \ -H "Authorization: Bearer ${RUNTIME_API_KEY}"# A whole folder, each way, as a tar archive (up to 1 MiB in one PUT).curl -sS -X PUT --data-binary @project.tar.gz \ "https://api.withruntime.com/v1/sandboxes/${ID}/files/archive?path=/workspace/project" \ -H "Authorization: Bearer ${RUNTIME_API_KEY}"curl -sS -o out.tar.gz \ "https://api.withruntime.com/v1/sandboxes/${ID}/files/archive?path=/workspace/project&gzip=true" \ -H "Authorization: Bearer ${RUNTIME_API_KEY}"How the API is shaped
| Convention | Rule |
|---|---|
| Resources | /v1/<plural>/{id} |
| Actions | POST /v1/<plural>/{id}:<verb>, such as :stop, :pause, :wake |
| Field names | camelCase; unknown fields are refused, and every wrong one is named |
| Times | ISO 8601 in UTC |
| Money | Integer microdollars: 1,000,000 is one US dollar |
| Waiting | Prefer: wait=N (at most 120), or ?waitFor=running&timeoutSeconds=60 |
| Lists | {"data": [...], "nextCursor": "..."}; pass it back as cursor |
| Retries | Idempotency-Key, remembered for 24 hours |
| Rate | About 50 requests a second per key, bursts of 200 |
| Request id | x-request-id on every response |
Read errors
Every error has the shape {"error": {"code", "status", "message", "hint", "requestId"}}.
To print the code and the hint, use jq:
Terminalcurl -sS https://api.withruntime.com/v1/sandboxes/not-an-id \ -H "Authorization: Bearer ${RUNTIME_API_KEY}" | jq -r '.error | "\(.code): \(.hint)"'| Status | Typical code | What to do |
|---|---|---|
| 401 | unauthorized |
Check the key |
| 402 | included_usage_used_up, insufficient_funds |
Add credit, or read GET /v1/limits |
| 409 | sandbox_paused |
Resolve the state, then retry |
| 422 | idempotency_key_reused |
Same key needs the same body; new work, new key |
| 429 | rate_limited, no_credit_running_limit |
Wait Retry-After, retry with the same key |
| 503 | no_capacity, busy, host_unavailable |
Retry after Retry-After with the same key |
Mistakes and how Runtime handles them
- Running a command before the sandbox is up. Without
Prefer: wait=60, a create can returnstarting. Add the header, or read the sandbox with?waitFor=running. - Retrying a create after a timeout with no key. The first request may
have worked. Send the same
Idempotency-Keyand body again. The server returns the first result with"replayed": true. Nothing happens twice. - Reusing a key for different work. A changed body under the same key is refused with 422. It is not run.
- Reading a large output from
:exec. The JSON response holds 64 KiB of each stream, for a command that runs up to 60 seconds. Send"stream": truefor NDJSON with no output limit, or write to a file and download it. - Calling a 0.1.0 route. Routes under
/v1/resourcesreturn 410upgrade_required, anddetails.replacementnames the new route.
The SDKs do these retries for you. Use curl in a shell script, in a language without an SDK, or for a quick check from a CI step.
Related
- How to use the OpenAPI spec to generate a typed client
- How to run sandboxes from GitHub Actions
- How to pause and resume a sandbox
- Troubleshooting
Facts on this page were checked on 26 October 2026.