# 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](/docs/api)). ## Get a key into a variable Make a key at [API keys](https://withruntime.com/account/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: ```bash no-run read -rs RUNTIME_API_KEY && export RUNTIME_API_KEY ``` Never add `-v` or `--trace` to a curl that carries the key: both print the `Authorization` header. ## Create, run, stop ```bash no-run # 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 ```bash no-run # 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//{id}` | | Actions | `POST /v1//{id}:`, 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: ```bash no-run curl -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=60` a create can answer `starting`. 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-Key` and 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": true` for NDJSON with no output limit, or write to a file and download it. - **Calling a 0.1.0 route.** Routes under `/v1/resources` answer 410 `upgrade_required`, and `details.replacement` names 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](/how-to/use-the-openapi-spec) to generate a typed client - [How to run sandboxes from GitHub Actions](/how-to/run-sandboxes-from-github-actions) - [How to pause and resume a sandbox](/how-to/pause-and-resume-a-sandbox) - [Troubleshooting](/docs/troubleshooting) Facts on this page were checked on 25 September 2026.