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:
| Status | Meaning |
|---|---|
400 | Invalid request body or parameters. |
401 | Missing, unknown, revoked, expired, or malformed credential. |
402 | Plan gate — sandbox creation requires Pro/Max — 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 Pro or Max 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: small, medium, large. Default medium. |
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. |
Sizes
size | vCPUs | RAM |
|---|---|---|
small | 1 | 1 GB |
medium (default) | 2 | 2 GB |
large | 4 | 4 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/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.
{
"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 requiredsnapshotId. Forks are automatically routed to the node that holds the snapshot — noX-Vibes-Nodepin 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:
| Method | Path | Notes |
|---|---|---|
POST | /v1/sandboxes/{id}/filesystem-snapshot | Project tarball snapshot |
POST | /v1/sandboxes/{id}/tarball | Alias for filesystem snapshot |
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]