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_…); the API-key management routes require a dashboard session. 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 Pro/Max — 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 Pro or Max 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: small, medium, large. Default medium.
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.

Sizes

sizevCPUsRAM
small11 GB
medium (default)22 GB
large44 GB

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. All sizes are available on Pro and Max.

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, the response adds template, templateStatus (ready | failed), 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.

{
  "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 (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.
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. 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 API-key auth + owner check apply to:

MethodPathNotes
POST/v1/sandboxes/{id}/filesystem-snapshotProject tarball snapshot
POST/v1/sandboxes/{id}/tarballAlias for filesystem snapshot
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]