How to call the Runtime REST API with curl
Send Authorization: Bearer $RUNTIME_API_KEY to https://api.withruntime.com/v1/...; POST /v1/sandboxes with {} creates a sandbox.
On Runtime every product sits behind one HTTPS API, so curl reaches
everything the SDKs, the CLI and the MCP server do. Money is integer
microdollars, every error has one shape with a hint, and a create you retry
with the same Idempotency-Key never makes two sandboxes. API version 0.2.0,
checked 25 September 2026 (API reference).
Get a key into a variable
Make a key at API keys, or from a
terminal with npx withruntime keys create --name curl, and keep it 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 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 '{"funding": "trial", "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(6 * 7)\""}'# 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 macOS default shell, 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}"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, every wrong one named at once |
| 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 is {"error": {"code", "status", "message", "hint", "requestId"}}.
Print the code and the hint with 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 | trial_exhausted, insufficient_funds |
Add credit, or read GET /v1/limits |
| 409 | trial_busy, sandbox_paused |
Resolve the state, then retry |
| 422 | idempotency_key_reused |
Same key needs the same body; new work, new key |
| 429 | rate_limited |
Wait Retry-After, retry with the same key |
| 503 | 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=60a create can answerstarting. 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, and the server answers 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, not run.
- Reading a huge output from
:exec. The JSON answer holds 64 KiB of each stream for a command 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/resourcesanswer 410upgrade_required, anddetails.replacementnames the new one.
The SDKs do these retries for you. Reach for curl in a shell script, a language without an SDK, or 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 25 September 2026.