omg/docs
Sandbox API

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:

StatusMeaning
400Invalid request body or parameters.
401Missing, unknown, revoked, expired, or malformed credential.
402Plan gate — sandbox creation requires a paid Computer plan, or the requested size isn't included in it — or insufficient compute credit.
403Credential is valid but doesn't own the resource.
404Resource not found.
409Lifecycle conflict.
421Request reached the wrong node for an X-Vibes-Node pin.
429API-key creation rate limit.
503Sandbox 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:

FieldTypeNotes
portsnumber[]Ports to expose from the guest.
envobjectEnvironment variables.
timeoutnumberIdle timeout in seconds.
sizestringMachine size (see Sizes). Defaults to the machine included with your plan.
vcpus + memMbnumberExact shape instead of size — must both be set and match a curated shape.
templateIdstringFork from a baked snapshot template (e.g. "react-ts"). See Templates.
templatestringCoding-agent preset: opencode, pi, codex, claude, lfg. Mutually exclusive with templateId.
gitRepositoryUrlstringSource attribution for git imports (gitRef optional alongside).
preferredModelstringLLM the guest's proxy routes to.
skipAppProcessesbooleanDefaults true for API-key callers.
idempotencyKeystringOptional 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

sizevCPUsRAMPlan
small11 GBlegacy Pro/Max only
medium22 GBlegacy Pro/Max only
large44 GBlegacy Pro/Max only
pro48 GBPersonal (default)
xlarge816 GBPro (default)
fleet1236 GBAlways 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 / totalComputeCost are list-rate estimates; computeCostBilled is false while 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, and startCommand (all optional).
  • POST /v1/sandboxes/fork — fork from an explicit snapshot id you own. Same body plus required snapshotId. Forks are automatically routed to the node that holds the snapshot — no X-Vibes-Node pin 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:

MethodPathNotes
POST/v1/sandboxes/{id}/filesystem-snapshotProject tarball snapshot; returns kind: "files_only"
POST/v1/sandboxes/{id}/tarballAlias for filesystem snapshot; used as the local-deploy source carrier
GET/v1/sandboxes/{id}/diskRead live root-filesystem capacity and usage
PATCH/v1/sandboxes/{id}/diskGrow 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}/usageSandbox usage (cpuMs, wallClockMs)
POST/v1/sandboxes/{id}/extendExtend 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 is GET /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 in Sec-WebSocket-Protocol (alongside vibes.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:

    ByteType
    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]