# HTTPS API reference One API for every Runtime Cloud product, at `https://api.withruntime.com`. The SDKs, the CLI and MCP are thin layers over it. This page is the contract; the machine-readable one is the OpenAPI document at `https://api.withruntime.com/v1/openapi.json`, public and generated from the same route definitions the server validates with. API version: **0.2.0**. ## Requests Send the key as a bearer token. Keys stay in server-side secret storage; never in a URL, a browser or a command line others can read. ```bash no-run curl https://api.withruntime.com/v1/sandboxes \ -H "Authorization: Bearer ${RUNTIME_API_KEY}" \ -H "Content-Type: application/json" \ -H "Prefer: wait=60" \ -d '{}' ``` In a shell, put a variable before a colon in braces: `"${ID}:exec"`. zsh, the macOS default, reads `$ID:e` as a modifier and sends `/xec`. Never use `curl -v` or `--trace` with the key: they print the `Authorization` header. ```bash no-run 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)\""}' ``` - Resources live at `/v1//{id}`. A change that is not a plain update is `POST /v1//{id}:`: `:stop`, `:pause`, `:wake`, `:exec`. - Field names are camelCase. Times are ISO 8601 in UTC. Money is integer microdollars: 1,000,000 is one US dollar. - Unknown fields are refused, and every wrong field is named in one answer. - `Prefer: wait=N` (seconds, at most 120) on a change answers once the resource has settled rather than at once. `GET /v1/sandboxes/{id}?waitFor=running&timeoutSeconds=60` waits for a state without polling. ## Responses and errors Every response carries `x-request-id`. Every error has one shape: ```json { "error": { "code": "trial_busy", "status": 409, "message": "The trial runs 3 sandboxes at once; ... are running or paused.", "hint": "Stop one (runtime sandbox stop ), or pass funding: \"paid\" to use prepaid credit.", "details": { "sandboxIds": ["..."], "concurrent": 3 }, "requestId": "req_0mud1dtgjogtn2z8t6y" } } ``` Follow the `hint`. Quote the `requestId` when you report a problem. A 5xx never carries internal detail, only a fixed message and the request id. | Status | Codes you may see | Safe next step | | ------ | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | 400 | `invalid_request` (with `details.issues`), `invalid_region`, `invalid_trial` | Fix the named fields | | 401 | `unauthorized` | Check the key | | 402 | `trial_exhausted`, `insufficient_funds`, `spending_limit_reached` | Add credit, pay with `funding: "paid"`, or read `GET /v1/limits` | | 403 | `forbidden` (a read-only key asking to change something is one), `permission_denied` | Do not work around a refusal | | 404 | `not_found`, `route_not_found`, `file_not_found` | Check the id or path | | 409 | `no_capacity`, `trial_busy`, `not_running`, `sandbox_paused`, `sandbox_stopped`, `sandbox_not_ready`, `is_a_directory` | Resolve the state, then retry | | 422 | `idempotency_key_reused` | Same key, same body; or a new key for new work | | 426 | `upgrade_required` | Move to this API version | | 429 | `rate_limited` | Wait `Retry-After`, then retry with the same key | | 503 | `busy`, `host_unavailable`, `api_unavailable`, `guest_busy` | Retry after `Retry-After` with the same key (the SDKs do) | | 503 | `unavailable`, `fork_unavailable`, `previews_unavailable` | Switched off here on purpose; retrying will not help | The server can refuse capacity; a request is not a guarantee of allocation. ## Retries and idempotency Send `Idempotency-Key: ` on a change. The server remembers the key for 24 hours: - The same key with the same body answers the first result again, marked `"replayed": true` and with the header `Idempotency-Replayed: true`. Nothing happens twice. - The same key with a different body is refused with 422 `idempotency_key_reused`. - A new key is new work. The SDKs send a key on every change and retry transport failures, 429, 502, 503 and 504 with it. Changing the body while reusing a key is not a retry. ## Limits Each key may make about 50 requests a second, in bursts of up to 200, with 128 being served at once; past that, a request waits its turn for up to five seconds before it is refused. An organization may have 192 requests in flight across all its keys, and eight terminals open. Over a limit you get 429 with `Retry-After`. These are protection, not quotas, and a refusal costs nothing. ## Sandboxes | Method and path | What it does | | ----------------------------------- | -------------------------------------------------------------------------------------- | | `POST /v1/sandboxes` | Create. Every field optional. | | `GET /v1/sandboxes` | List: `state`, `name`, `label=key:value` (repeat), `includeStopped`, `limit`, `cursor` | | `GET /v1/sandboxes/{id}` | Read; `waitFor` and `timeoutSeconds` wait for a state | | `POST /v1/sandboxes/{id}:stop` | Stop. Compute ends on confirmed shutdown. | | `POST /v1/sandboxes/{id}:pause` | Save memory and files; compute billing stops | | `POST /v1/sandboxes/{id}:wake` | Restore a paused sandbox; `timeoutSeconds` sets its next lease | | `POST /v1/sandboxes/{id}:extend` | `{"seconds": 600}` more before the lease ends | | `POST /v1/sandboxes/{id}:retention` | `{"days": 30}` to keep a paused sandbox, 1 to 365 | | `POST /v1/sandboxes/{id}:restart` | Start a stopped persistent sandbox again from its disk | The create body: | Field | Default | Notes | | ------------------ | ------------------------------- | ------------------------------------------------------------ | | `name`, `labels` | none | Your own handle and up to 32 `key: value` tags | | `funding` | trial while it lasts, then paid | `"trial"` never falls back to paid credit | | `region` | the default region | Use a region listed for your account | | `vcpu` | 2 | Sizes are subject to the deployed host's measured capacity | | `memoryMiB` | 4096 | | | `diskMiB` | 4096 | | | `cpu` | `"shared"` | `"reserved"` guarantees every vCPU | | `cpuFloorMillis` | 50 | Guaranteed CPU while shared, in thousandths of a vCPU | | `timeoutSeconds` | 1800 | How long it may run before its lease ends, at most 3600 | | `onLeaseEnd` | `"pause"` | Or `"stop"` | | `pausable` | true | | | `network` | the public web on ports 80, 443 | Same shape as `PUT /v1/sandboxes/{id}/network` | | `maxCostMicros` | none | Refuse the create if its first lease would cost more | | `image`, `volumes` | none | A ready image id and up to four `{volumeId, path, mode}` | | `snapshot` | none | A ready snapshot id: start as a copy of it. Not with `image` | Sizes are subject to the deployed host's measured capacity. Sizes are limits you ask for; the server checks their combinations, account limits and available host capacity. ## Commands and processes | Method and path | What it does | | ------------------------------------------------------ | ------------------------------------------------------------------------------ | | `POST /v1/sandboxes/{id}:exec` | Run and answer `{exitCode, stdout, stderr, timedOut}` | | `POST /v1/sandboxes/{id}/processes` | Start in the background; add `"pty": {"cols", "rows"}` for a terminal | | `GET /v1/sandboxes/{id}/processes` | Running and recent processes | | `GET /v1/sandboxes/{id}/processes/{processId}` | One process | | `GET /v1/sandboxes/{id}/processes/{processId}/output` | Output from `cursor`; `waitMs` long-polls; `follow=true` streams | | `POST /v1/sandboxes/{id}/processes/{processId}:write` | `{"data"}` or `{"base64"}`, with `offset` and `eof`; offsets make retries safe | | `POST /v1/sandboxes/{id}/processes/{processId}:signal` | `{"signal": "SIGTERM"}` | | `POST /v1/sandboxes/{id}/processes/{processId}:resize` | `{"cols", "rows"}` for a pty | The exec body is `command` (run under `bash -c`) or `argv` (no shell), and optionally `cwd`, `env`, `stdin`, `timeoutMs` (default 60,000; at most 24 hours) and `stream`. With `"stream": true` the answer is NDJSON, one event a line: `start`, `stdout`, `stderr`, `exit`, and `continue` with a cursor when a stream passes the server's time limit, from which `…/output?follow=true&cursor=` resumes. A timeout is a result with `timedOut: true`, not an error. `env` values are never echoed and are stored only as hashes. ## Terminals `GET /v1/sandboxes/{id}/terminal` with `Upgrade: websocket` and the bearer header opens a terminal: `cols`, `rows`, `command` (default `bash -l`), `cwd`, or `processId` to attach to a process started with a pty. Binary frames are terminal bytes both ways. Text frames are JSON: you send `{"type":"resize","cols":120,"rows":40}`; the server sends `{"type":"ready","processId":...}` and `{"type":"exit","exitCode":...}`. ## Files | Method and path | What it does | | --------------------------------------------------- | --------------------------------------------------------- | | `GET /v1/sandboxes/{id}/files/content?path=` | The file's raw bytes | | `PUT /v1/sandboxes/{id}/files/content?path=` | Replace the file with the raw request body; makes parents | | `GET /v1/sandboxes/{id}/files/list?path=` | Entries; `depth`, `glob`, `hidden`, `limit` | | `GET /v1/sandboxes/{id}/files/stat?path=` | Type, size, mode and times, or `{"exists": false}` | | `POST /v1/sandboxes/{id}/files:mkdir` | `{"path", "parents"}` | | `POST /v1/sandboxes/{id}/files:remove` | `{"path", "recursive"}` | | `POST /v1/sandboxes/{id}/files:rename` | `{"from", "to", "overwrite"}` | | `POST /v1/sandboxes/{id}/uploads` | Begin a large upload: `{"path", "size", "sha256"}` | | `PUT /v1/sandboxes/{id}/uploads/{uploadId}?offset=` | One chunk of raw bytes; chunks may go in parallel | | `POST /v1/sandboxes/{id}/uploads/{uploadId}:commit` | Check the digest and move the file into place atomically | | `POST /v1/sandboxes/{id}/uploads/{uploadId}:abort` | Give up | Paths are absolute. File errors name the path: `file_not_found`, `is_a_directory`, `not_a_directory`, `permission_denied`. ## Other products | Method and path | Product | | ----------------------------------------------------------------------------------------------------------------------- | ------------------- | | `POST /v1/sandboxes/{id}/interpreter:run`, `…/interpreter/contexts` | Code interpreter | | `POST`, `GET /v1/sandboxes/{id}/previews`, `…/previews/{port}`, `…/previews/{port}:rotate` | Previews | | `POST /v1/sandboxes/{id}/desktop:start`, `:stop`, `:act`, `GET …/desktop/screenshot` | Desktop | | `GET`, `PUT /v1/sandboxes/{id}/network` | Network rules | | `POST`, `GET /v1/feedback` | Feedback | | `POST /v1/support/messages`, `GET /v1/support/conversations/{id}` | Support | | `GET /v1/me`, `GET /v1/usage`, `GET /v1/limits` | Account | | `POST`, `GET /v1/images`, `GET /v1/images/{id}`, `…/logs`, `:delete` | Custom images | | `POST`, `GET /v1/volumes`, `GET /v1/volumes/{id}`, `:delete` | Volumes | | `POST /v1/sandboxes/{id}:fork`, `POST /v1/sandboxes/{id}:snapshot`, `GET /v1/snapshots`, `…/{id}`, `:extend`, `:delete` | Forks and snapshots | The OpenAPI document has every field of every route. ## Account `GET /v1/me` says who a key is: `orgId`, `principalId`, `credentialId`. `GET /v1/usage` says what the account has and has used. Money is integer microdollars in strings (1,000,000 is one dollar), exact however large: | Field | What it is | | ----------- | ----------------------------------------------------------------- | | `credited` | Every credit ever added: purchases and grants | | `spent` | Settled usage, including what refunds and disputes took back | | `held` | Reserved against running and paused work, not yet settled | | `expired` | Credit that expired, or grant credit taken back | | `available` | What can still be spent: `credited - spent - expired - held` | | `takenBack` | The part of `spent` that refunds and disputes took | | `trial` | `{totalMs, usedMs, reservedMs, availableMs}`, or null | | `resources` | The newest hundred resources, with what each used and was charged | `GET /v1/limits` says what this key may do and what its agent may still spend: ```json { "access": "full", "daily": { "limitMicros": "25000000", "usedMicros": "3100000", "remainingMicros": "21900000", "window": "24h" } } ``` `access` is `full` (every action in every product), `read` (a read-only key: every read, no change and no spending) or `selected` (the actions the key names). `daily.limitMicros` is the most the key's agent may commit in any 24 hours, or null when the owner set no limit. `usedMicros` counts settled charges plus money still on hold. Past the limit, a create, wake, extension or renewal fails with 402 `spending_limit_reached` and charges nothing. Only an account owner sets, changes or removes a limit, at [API keys](https://withruntime.com/account/keys); no key can. ## Pages Every list, in every product, answers `{"data": [...], "nextCursor": "..."}`. Pass `nextCursor` back as `cursor`; `null` ends the list. `limit` is 1 to 100. ## Versions This is 0.2.0. The 0.1.0 routes under `/v1/resources` were removed on 22 September 2026: each answers 410 `upgrade_required`, and `details.replacement` names the route that replaced it. [The migration guide](./migrate) maps each old call to its new one. The OpenAPI document's `info.version` is the version you are talking to.