Reference
Every Sandbox API endpoint, with request/response shapes and examples.
All endpoints are under https://infra.omg.dev. Sandbox routes accept a Bearer
API key (omg_sk_…) or a short-lived omg user JWT (aud=vibes);
the API-key management routes require the user JWT (or a service JWT acting
on behalf of a user) and reject API keys. JSON in, JSON out. IDs are the short
sandbox id returned by create.
Errors are always { "error": "message" } with a matching status:
| Status | Meaning |
|---|---|
400 | Invalid request body or parameters. |
401 | Missing, unknown, revoked, expired, or malformed credential. |
402 | Plan gate — sandbox creation requires a paid Computer plan, or the requested size isn't included in it — or insufficient compute credit. |
403 | Credential is valid but doesn't own the resource. |
404 | Resource not found. |
409 | Lifecycle conflict. |
421 | Request reached the wrong node for an X-Vibes-Node pin. |
429 | API-key creation rate limit. |
503 | Sandbox capacity or dependency unavailable. |
API key management
These routes manage keys and require a dashboard JWT
(Authorization: Bearer <dashboard jwt>) — an API key cannot manage keys.
The dashboard's Sandboxes → API keys screen drives them for you. Limits:
10 active keys per owner, 5 creations per owner per hour.
Create a key
POST /v1/api-keys
name is required (max 80 chars). expiresAt is optional RFC-3339 and must
be in the future. The plaintext key is returned exactly once.
curl -sS https://infra.omg.dev/v1/api-keys \
-H "Authorization: Bearer $OMG_DASHBOARD_JWT" \
-H "Content-Type: application/json" \
-d '{ "name": "Production backend", "expiresAt": "2026-12-31T23:59:59Z" }'{
"key": "omg_sk_...",
"apiKey": {
"id": "ak_...",
"name": "Production backend",
"prefix": "omg_sk_abc12",
"scopes": ["sandbox"],
"createdAt": "2026-07-07T13:00:00Z",
"lastUsedAt": null,
"revokedAt": null,
"expiresAt": "2026-12-31T23:59:59Z"
}
}The top-level key is the one-time plaintext — store it now. The apiKey
object is the metadata you'll see again in the list.
List keys
GET /v1/api-keys — metadata only; secrets and hashes are never returned.
{
"apiKeys": [
{
"id": "ak_...",
"name": "Production backend",
"prefix": "omg_sk_abc12",
"scopes": ["sandbox"],
"createdAt": "2026-07-07T13:00:00Z",
"lastUsedAt": "2026-07-07T13:10:00Z",
"revokedAt": null,
"expiresAt": null
}
]
}Revoke a key
DELETE /v1/api-keys/{id} — revokes a key you own; permanent. Returns the
key with revokedAt set.
{ "apiKey": { "id": "ak_...", "revokedAt": "2026-07-07T13:15:00Z", "...": "..." } }Sandbox response
Create, get, lifecycle, fork, and delete all return the same shape. Every
/v1/sandboxes* route below accepts Authorization: Bearer omg_sk_… and is
owner-scoped.
{
"id": "abc123def456",
"status": "running",
"cwd": "/home/user",
"vmIP": "10.10.0.2",
"portMap": { "41000": 5173 },
"createdAt": "2026-07-07T13:00:00Z",
"preferredModel": "",
"nodeId": "box-1",
"llmProxyUrl": "http://..."
}llmProxyUrl is omitted when not applicable. When you store nodeId, pin
follow-up calls with X-Vibes-Node: <nodeId> (a mismatch returns 421).
Create a sandbox
POST /v1/sandboxes
Creates a sandbox owned by the caller. API-key creates are raw programmatic
sandboxes — no project/session/app attribution — and appear in the list with
kind: "api". When authenticated with an API key and agentServerSource is
omitted, the server defaults skipAppProcesses to true (i.e. a raw sandbox,
not a dashboard dev-editor sandbox).
Creating a sandbox requires a paid Computer plan — Personal, Pro, or Always
On, or a grandfathered legacy plan — free accounts get 402.
All body fields are optional:
| Field | Type | Notes |
|---|---|---|
ports | number[] | Ports to expose from the guest. |
env | object | Environment variables. |
timeout | number | Idle timeout in seconds. |
size | string | Machine size (see Sizes). Defaults to the machine included with your plan. |
vcpus + memMb | number | Exact shape instead of size — must both be set and match a curated shape. |
templateId | string | Fork from a baked snapshot template (e.g. "react-ts"). See Templates. |
template | string | Coding-agent preset: opencode, pi, codex, claude, lfg. Mutually exclusive with templateId. |
gitRepositoryUrl | string | Source attribution for git imports (gitRef optional alongside). |
preferredModel | string | LLM the guest's proxy routes to. |
skipAppProcesses | boolean | Defaults true for API-key callers. |
idempotencyKey | string | Optional owner-scoped key (max 256 chars). Replays return the original sandbox response, including after the first response was lost. Reusing a key with a different request returns 409. |
Sizes
size | vCPUs | RAM | Plan |
|---|---|---|---|
small | 1 | 1 GB | legacy Pro/Max only |
medium | 2 | 2 GB | legacy Pro/Max only |
large | 4 | 4 GB | legacy Pro/Max only |
pro | 4 | 8 GB | Personal (default) |
xlarge | 8 | 16 GB | Pro (default) |
fleet | 12 | 36 GB | Always On (default) |
An explicit size wins over vcpus/memMb. If you pass vcpus/memMb
instead, the pair must exactly match one of the shapes above — anything else
is 400 unsupported shape.
Current Computer plans each get exactly one included shape and reject any
other size — Personal → pro (4 vCPU / 8 GB), Pro → xlarge (8 vCPU / 16 GB),
Always On → fleet (12 vCPU / 36 GB) — so leave size unset to get it
automatically. small/medium/large remain selectable only on a
grandfathered legacy Pro/Max plan.
curl -sS https://infra.omg.dev/v1/sandboxes \
-H "Authorization: Bearer $OMG_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "size": "large", "ports": [5173], "templateId": "react-ts" }'const sb = await fetch("https://infra.omg.dev/v1/sandboxes", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OMG_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ size: "large", ports: [5173], templateId: "react-ts" }),
}).then((r) => r.json())When template (agent preset) is set, a successful response adds template,
templateStatus: "ready", installLogPath, and — for presets
that serve a web UI — servePort + serveLogPath. See
Templates.
The VM boots asynchronously — poll GET /v1/sandboxes/{id} until status is
running before connecting.
List your sandboxes
GET /v1/sandboxes — the caller's account-scoped list plus usage totals.
Query params: limit (capped at 500) and offset.
{
"sandboxes": [
{
"id": "abc123def456",
"slug": null,
"project": null,
"projectId": null,
"appId": null,
"kind": "api",
"status": "running",
"region": "box-1",
"vcpus": 2,
"memMb": 2048,
"createdAt": "2026-07-07T13:00:00Z",
"lastActiveAt": "2026-07-07T13:00:00Z",
"usageSeconds": 60,
"usageCost": null,
"computeCost": 0.0001,
"sshEnabled": false,
"sshHost": null,
"sshUser": null,
"sshCommand": null,
"sshKeysConfigured": false
}
],
"totalUsageSeconds": 60,
"totalComputeCost": 0.0001,
"computeCostBilled": false,
"totalLlmCost": null,
"total": 1,
"limit": 0,
"offset": 0
}
computeCost/totalComputeCostare list-rate estimates;computeCostBilledisfalsewhile sandbox compute billing is in shadow mode.
Get a sandbox
GET /v1/sandboxes/{id} — returns one sandbox if owned by the caller (same
shape as create), with optional SSH metadata. Use it to poll for
status: "running".
Stop and delete
DELETE /v1/sandboxes/{id} — stops the sandbox. Idempotent for
already-stopped sandboxes.
curl -sS -X DELETE https://infra.omg.dev/v1/sandboxes/$SANDBOX_ID \
-H "Authorization: Bearer $OMG_API_KEY"Hibernate (pause)
POST /v1/sandboxes/{id}/hibernate — alias POST /v1/sandboxes/{id}/pause.
Snapshots and stops the VM while keeping the row wakeable. Returns the sandbox
with status: "hibernated".
Wake (resume)
POST /v1/sandboxes/{id}/wake — alias POST /v1/sandboxes/{id}/resume. Both
body fields optional. Returns the sandbox with status: "running".
curl -sS -X POST https://infra.omg.dev/v1/sandboxes/$SANDBOX_ID/wake \
-H "Authorization: Bearer $OMG_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "readinessPort": 5173, "ports": [5173] }'Snapshot
POST /v1/sandboxes/{id}/snapshot — creates a full snapshot.
The optional JSON body accepts restoreKind (full_rootfs or project_only)
and idempotencyKey (up to 256 characters). Repeating the same non-empty
idempotencyKey for the same source sandbox returns the original snapshot,
including after the destructive capture stopped the sandbox. Use a stable key
for any request whose response may be retried.
{
"restoreKind": "full_rootfs",
"idempotencyKey": "cloud-computer:computer-id:publish:7"
}{
"id": "snap...",
"nodeId": "box-1",
"kind": "user_project",
"rootfsSha": "...",
"sizeBytes": 123456,
"tarballSizeBytes": 12345,
"sourceSandboxId": "abc123def456",
"uploadedToTigris": false,
"createdAt": "2026-07-07T13:00:00Z"
}Fork
POST /v1/sandboxes/{id}/fork— fork from a source sandbox you own. If it's running the server snapshots it first; if hibernated it forks from the wake snapshot. Body:ports,sessionId,projectId,projectSlug,appId,preferredModel,skipAppProcesses,readinessPort, andstartCommand(all optional).POST /v1/sandboxes/fork— fork from an explicit snapshot id you own. Same body plus requiredsnapshotId. Forks are automatically routed to the node that holds the snapshot — noX-Vibes-Nodepin needed here.
The explicit snapshot fork also accepts idempotencyKey (max 256 characters).
The key is scoped to the authenticated owner and reserves the sandbox id before
VM allocation. Repeating it returns the first fork response instead of creating
another VM, so callers should use a stable key whenever a response may be lost.
curl -sS https://infra.omg.dev/v1/sandboxes/fork \
-H "Authorization: Bearer $OMG_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "snapshotId": "'$SNAPSHOT_ID'", "ports": [5173], "skipAppProcesses": true }'Pass skipAppProcesses: true when the snapshot came from a raw sandbox — it
keeps the fork raw instead of starting the dashboard dev-editor process set.
When startCommand is provided, readinessPort is required and must also
appear in ports. The server pins those two fields as the sandbox's durable
wake spec, so later preview wakes do not need the caller to resend launch
options.
Snapshot + fork is also the way to bake reusable custom templates; see
Templates.
Execute a command
POST /v1/sandboxes/{id}/exec — runs a command in the guest (as root) and
returns its output. The sandbox must be running.
curl -sS -X POST https://infra.omg.dev/v1/sandboxes/$SANDBOX_ID/exec \
-H "Authorization: Bearer $OMG_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "cmd": "bash", "args": ["-lc", "bun install"], "cwd": "/home/user/project", "timeoutMs": 300000 }'{ "stdout": "...", "stderr": "", "exitCode": 0 }Body: cmd plus optional args, cwd, env, timeoutMs, detached.
exec injects ANTHROPIC_BASE_URL, OPENAI_BASE_URL, OMG_AI_URL, and
OMG_MEDIA_URL pointing at the per-sandbox LLM proxy (your env values
win), so AI SDKs in the guest work without provider keys.
Read and write files
POST /v1/sandboxes/{id}/files — write files. The body is a JSON array;
content is base64. Returns 204.
curl -sS -X POST https://infra.omg.dev/v1/sandboxes/$SANDBOX_ID/files \
-H "Authorization: Bearer $OMG_API_KEY" \
-H "Content-Type: application/json" \
-d '[{ "path": "/home/user/project/hello.txt", "content": "'$(printf 'hi' | base64)'", "mode": 420 }]'GET /v1/sandboxes/{id}/files?path=/home/user/project/hello.txt — read one
file back as base64 JSON:
{ "content": "aGk=" }Preview URL
GET /v1/sandboxes/{id}/url/{port} — the public HTTPS URL for a port the
sandbox serves:
{ "url": "https://<preview-host>" }Other sandbox routes
The same sandbox auth (API key or user JWT) + owner check apply to:
| Method | Path | Notes |
|---|---|---|
POST | /v1/sandboxes/{id}/filesystem-snapshot | Project tarball snapshot; returns kind: "files_only" |
POST | /v1/sandboxes/{id}/tarball | Alias for filesystem snapshot; used as the local-deploy source carrier |
GET | /v1/sandboxes/{id}/disk | Read live root-filesystem capacity and usage |
PATCH | /v1/sandboxes/{id}/disk | Grow an older running sandbox to the 16 GB platform floor without restarting |
PUT | /v1/sandboxes/{id}/uploads/{path...} | Browser upload path |
GET | /v1/sandboxes/{id}/usage | Sandbox usage (cpuMs, wallClockMs) |
POST | /v1/sandboxes/{id}/extend | Extend the idle timeout |
Interactive shell (WebSocket)
Not a REST call — this upgrades to a WebSocket carrying a live PTY.
-
Endpoint:
wss://ssh-ws.omg.dev/<sandbox-id>(the public shell gateway host). The underlying API route isGET /v1/sandboxes/{id}/shell. -
Subprotocol:
vibes.shell.v1. -
Auth (API-key client) — any one of:
Authorization: Bearer omg_sk_...header, or?access_token=omg_sk_...query param, or- a
vibes.api_key.<base64url(api key)>entry inSec-WebSocket-Protocol(alongsidevibes.shell.v1) — the option browsers need, since they can't set headers on a WebSocket.
(The dashboard uses
vibes.jwt.<base64url(jwt)>instead.) A key can open the shell for any sandbox its owner owns. -
Frames are binary, tagged by a leading byte:
Byte Type 0data 1control 2ping 3pong
Local terminal command
Use the first-party bridge script when you want a local terminal attached to a sandbox:
export OMG_API_KEY=omg_sk_...
tmp=$(mktemp)
curl -fsSL https://omg.dev/sandbox-shell.js -o "$tmp"
node "$tmp" <sandbox-id>
rm -f "$tmp"The hosted shell is not raw SSH. This command speaks the same framed shell
protocol as the browser terminal and requires Node.js 22+ or another runtime
with a global WebSocket.
const key = process.env.OMG_API_KEY
const b64url = (s) =>
btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
const ws = new WebSocket(`wss://ssh-ws.omg.dev/${sandboxId}`, [
"vibes.shell.v1",
`vibes.api_key.${b64url(key)}`,
])
ws.binaryType = "arraybuffer"
ws.onmessage = (e) => {
const frame = new Uint8Array(e.data)
if (frame[0] === 0) process.stdout.write(frame.subarray(1)) // data
}
// send keystrokes as data frames: [0, ...utf8bytes]