DocumentationAccount

API reference

One API for every product

Sandboxes, commands, files, images and volumes, with one set of rules.

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.

Terminalcurl 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.

Terminalcurl -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/<plural>/{id}. A change that is not a plain update is POST /v1/<plural>/{id}:<verb>: :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 <id>), 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: <any unique string> 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; 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 maps each old call to its new one. The OpenAPI document's info.version is the version you are talking to.

Was this page right?