/ docs / the spawnd api

Sandboxes for agents.

spawnd gives an AI agent a secure Firecracker microVM over a REST call. Create a sandbox, run untrusted code inside it, expose a port, snapshot and fork it — and pay only for the resource-seconds you allocate.

Overview

The primary consumer of spawnd is a machine — an agent or app driving sandboxes at scale, not a human clicking around. A sandbox is one Firecracker microVM running a generic Ubuntu rootfs with common toolchains preinstalled (Node, Python, Go, Rust, and the usual build utilities). Guest code is hostile by default and isolated by construction; it cannot reach the host, the control plane, or another sandbox.

REST / SDK API

The primary surface. Create, exec, files, ports, and the full lifecycle.

SSH & terminal

A browser terminal or SSH into the VM for human sessions.

Preview URLs

Expose a guest port at <id>--<workspace>.spawnd.oonamy.xyz via reverse proxy.

MCP server

Sandbox control as MCP tools, so LLM clients drive VMs directly.

Quickstart

From zero to a running sandbox with curl. Base URL is https://spawnd.oonamy.xyz.

  1. Request a magic link

    In dev mode the sign-in link is returned inline as devLink.

    curl -s -X POST https://spawnd.oonamy.xyz/auth/magic-link \
      -H 'Content-Type: application/json' \
      -d '{"email":"you@example.com"}'
    # → { "sent": true, "expiresInMinutes": 15, "devLink": "https://spawnd.oonamy.xyz/auth/verify?token=..." }
  2. Verify to get a session JWT

    curl -s 'https://spawnd.oonamy.xyz/auth/verify?token=THE_TOKEN'
    # → { "token": "<jwt>", "user": {...}, "org": {...} }
  3. Mint an org API key

    Session-authed. The full key is shown once — store it.

    curl -s -X POST https://spawnd.oonamy.xyz/v1/keys \
      -H 'Authorization: Bearer <jwt>' \
      -H 'Content-Type: application/json' \
      -d '{"name":"cli"}'
    # → { "key": "sk_live_...", "keyRecord": { "prefix": "sk_live_...", ... } }
  4. Create a sandbox and exec

    curl -s -X POST https://spawnd.oonamy.xyz/v1/sandboxes \
      -H 'Authorization: Bearer sk_live_...' \
      -H 'Content-Type: application/json' -d '{"template":"base"}'
    # → { "id": "sbx_123", "state": "running", ... }
    
    curl -s -X POST https://spawnd.oonamy.xyz/v1/sandboxes/sbx_123/exec \
      -H 'Authorization: Bearer sk_live_...' \
      -H 'Content-Type: application/json' \
      -d '{"cmd":"echo","args":["hello from the box"]}'
    # → { "exitCode": 0, "stdout": "hello from the box\n", "durationMs": 42 }
  5. Kill it when done

    Idempotent — billing stops the moment the VM is reaped.

    curl -s -X DELETE https://spawnd.oonamy.xyz/v1/sandboxes/sbx_123 \
      -H 'Authorization: Bearer sk_live_...'
    # → { "ok": true }

Authentication

Two disjoint auth surfaces share one port. A session JWT cannot drive the sandbox SDK, and an API key cannot manage keys.

Org API key

All sandbox and usage calls. Sent as a bearer token:

Authorization: Bearer sk_live_...

Stored as a sha256 hash + 20-char prefix; the secret is shown once at creation.

Session JWT

Dashboard and key-management calls (/auth/*, /v1/keys):

Authorization: Bearer <jwt>

Issued by the passwordless magic-link flow. No passwords anywhere.

Magic link, dev mode. With no email provider configured the sign-in link is returned in the response (devLink) and logged — never dropped. The console auto-provisions your default API key on first authenticated load.

Sandboxes & sizes

template accepts a named size preset. Allocation (vCPU + memory) is fixed at create time and is the basis for resource-seconds billing. Unknown names fall back to the base size.

NamevCPUMemoryUse for
smallbox / base12048 MiBLight scripts, quick execs — cheapest.
box / default24096 MiBThe default. General-purpose agent work.
bigbox48192 MiBHeavy compilation, parallel workloads.
megabox816384 MiBThe largest box — big builds, many parallel jobs.

Lifecycle

Every sandbox supports the full lifecycle, and every verb is idempotent: kill on a dead sandbox succeeds, pause on a paused VM is a no-op, and create with the same idempotency key returns the same sandbox.

VerbMeaning
createAllocate a sandbox from a template; resumes from the warm pool when it can.
startBring a created sandbox to running.
execRun a command inside the guest and get its stdout, stderr, and exit code.
snapshotCapture full VM state (memory + disk) as a restorable artifact.
forkCopy-on-write clone from a snapshot — cheap children from one parent state.
pauseSnapshot memory + state and freeze CPU; billing drops to the paused rate.
resumeUn-freeze a paused VM back to running.
stopGraceful shutdown.
killHard teardown; release every host resource immediately. Terminal.

Legal states: creating, running, paused, stopping, stopped, killed, plus transient snapshotting and forking. killed is terminal. Cold starts use snapshot-restore from a warm pool, so create and resume are sub-second.

state machine
                 create
   (none) ─────────────────▶ creating
                                 │ boot + guest ping ok
                                 ▼
     ┌──────────── pause ◀── running ──▶ stopping ──▶ stopped
     │                        ▲   │        (graceful)      │
   paused ── resume ──────────┘   │ snapshot               │ resume/start
     │                            ▼                        │
     │                     (snapshot taken, stays running) │
     └──────────────── kill ─────┴──────── kill ───────────┘ ──▶ killed (terminal)

   fork(snapshot) ─────────▶ creating (a NEW sandbox id)

Isolation

Guest code is treated as hostile. Each VM boots under jailer (chroot + dropped privileges) in its own network namespace, with a per-VM tap device and double-NAT'd outbound internet. From inside a sandbox there is no route to the host, no route to the control plane, and no route to another sandbox; /dev/kvm and host files are unreachable. Isolation is enforced by construction, not by trusting the guest.

Billing — resource-seconds

spawnd bills allocated resource-seconds, not flat wall-clock, so cost scales with size. Both dimensions derive from the create-time allocation × time:

vCPU-secondsvcpu × seconds
GiB-seconds(memMib / 1024) × seconds

Allocation is fixed at create, so billing is deterministic and unforgeable — the guest cannot change its own vcpu/memMib.

StateBilledRate class
runningvCPU-s + GiB-s, liveactive
snapshotting, forkinglive (still running)active
pausedvCPU-s + GiB-s, paused ratepaused
creatingnot billed until guest ping ok
stopped, killednot billed

API · Auth

The passwordless sign-in flow. No auth required to start it; the token is the credential.

POST/auth/magic-linknone

Find-or-create the org + user, mint a single-use token, and send the sign-in link. devLink is present only in dev mode.

Request
{ "email": string }
Response
{ "sent": boolean, "email": string, "expiresInMinutes": number, "devLink"?: string }
curl -s -X POST https://spawnd.oonamy.xyz/auth/magic-link \
  -H 'Content-Type: application/json' \
  -d '{"email":"you@example.com"}'
GET/auth/verify?token=the token itself

Consume the single-use token (unexpired, unused) and issue a session JWT. Invalid, used, or expired → 401.

Response
{ "token": string, "user": {...}, "org": {...} }
curl -s 'https://spawnd.oonamy.xyz/auth/verify?token=THE_TOKEN'
GET/auth/mesession JWT

Return the current profile for a session. Missing, invalid, or expired token → 401.

Response
{ "user": {...}, "org": {...} }
curl -s https://spawnd.oonamy.xyz/auth/me \
  -H 'Authorization: Bearer <jwt>'

API · Keys

Session-authed key management. The console provisions your first key automatically.

GET/v1/keyssession JWT

List the org's keys. The secret is never returned.

Response
ApiKey[] // { id, prefix, name, createdAt, lastUsedAt, revokedAt }
curl -s https://spawnd.oonamy.xyz/v1/keys \
  -H 'Authorization: Bearer <jwt>'
POST/v1/keyssession JWT

Mint a new sk_live_ key. The full key is returned exactly once.

Request
{ "name"?: string }
Response
{ "key": string, "keyRecord": ApiKey }
curl -s -X POST https://spawnd.oonamy.xyz/v1/keys \
  -H 'Authorization: Bearer <jwt>' \
  -H 'Content-Type: application/json' \
  -d '{"name":"ci"}'

API · Sandboxes

Authenticated with an org API key and scoped to the caller's org. A sandbox from another org returns 404.

POST/v1/sandboxesAPI key

Allocate and start a sandbox. Resumes from the warm pool when possible. template is a named size; unknown names fall back to base.

Request
{ "template": string, "metadata"?: object, "idempotencyKey"?: string }
Response
Sandbox
curl -s -X POST https://spawnd.oonamy.xyz/v1/sandboxes \
  -H 'Authorization: Bearer sk_live_...' \
  -H 'Content-Type: application/json' \
  -d '{"template":"bigbox","metadata":{"job":"build-42"}}'
GET/v1/sandboxesAPI key

List the caller's org sandboxes, newest first.

Response
Sandbox[]
curl -s https://spawnd.oonamy.xyz/v1/sandboxes \
  -H 'Authorization: Bearer sk_live_...'
GET/v1/sandboxes/:idAPI key

Get one sandbox's current state.

Response
Sandbox
curl -s https://spawnd.oonamy.xyz/v1/sandboxes/sbx_123 \
  -H 'Authorization: Bearer sk_live_...'
POST/v1/sandboxes/:id/execAPI key

Run a command inside the guest over vsock and return its result.

Request
{ "cmd": string, "args"?: string[], "cwd"?: string, "env"?: object, "timeoutMs"?: number }
Response
{ "exitCode": number, "stdout": string, "stderr": string, "durationMs": number }
curl -s -X POST https://spawnd.oonamy.xyz/v1/sandboxes/sbx_123/exec \
  -H 'Authorization: Bearer sk_live_...' \
  -H 'Content-Type: application/json' \
  -d '{"cmd":"python3","args":["-c","print(2+2)"]}'
POST/v1/sandboxes/:id/filesAPI key

Write a file into the guest. Contents are base64-encoded.

Request
{ "path": string, "contentB64": string }
Response
{ "bytes": number }
curl -s -X POST https://spawnd.oonamy.xyz/v1/sandboxes/sbx_123/files \
  -H 'Authorization: Bearer sk_live_...' \
  -H 'Content-Type: application/json' \
  -d '{"path":"/root/app.py","contentB64":"cHJpbnQoMSk="}'
GET/v1/sandboxes/:id/files?path=API key

Read a file from the guest. Contents come back base64-encoded.

Response
{ "contentB64": string }
curl -s 'https://spawnd.oonamy.xyz/v1/sandboxes/sbx_123/files?path=/root/app.py' \
  -H 'Authorization: Bearer sk_live_...'
POST/v1/sandboxes/:id/portsAPI key

Expose a guest port publicly. Returns a preview URL at <id>--<workspace>.spawnd.oonamy.xyz.

Request
{ "port": number }
Response
{ "url": string }
curl -s -X POST https://spawnd.oonamy.xyz/v1/sandboxes/sbx_123/ports \
  -H 'Authorization: Bearer sk_live_...' \
  -H 'Content-Type: application/json' \
  -d '{"port":8000}'
POST/v1/sandboxes/:id/pauseAPI key

Snapshot and freeze the VM. Billing drops to the paused rate; memory stays resident.

Response
Sandbox
curl -s -X POST https://spawnd.oonamy.xyz/v1/sandboxes/sbx_123/pause \
  -H 'Authorization: Bearer sk_live_...'
POST/v1/sandboxes/:id/resumeAPI key

Restore a paused VM back to running from its pause snapshot.

Response
Sandbox
curl -s -X POST https://spawnd.oonamy.xyz/v1/sandboxes/sbx_123/resume \
  -H 'Authorization: Bearer sk_live_...'
POST/v1/sandboxes/:id/snapshotAPI key

Take a named, restorable full-state snapshot (memory + disk). The VM stays running.

Request
{ "name"?: string }
Response
Snapshot
curl -s -X POST https://spawnd.oonamy.xyz/v1/sandboxes/sbx_123/snapshot \
  -H 'Authorization: Bearer sk_live_...' \
  -H 'Content-Type: application/json' \
  -d '{"name":"after-deps"}'
POST/v1/sandboxes/:id/forkAPI key

Copy-on-write clone from a snapshot into a fresh sandbox (a new id). Omit snapshotId to fork the latest.

Request
{ "snapshotId"?: string }
Response
Sandbox // the new child
curl -s -X POST https://spawnd.oonamy.xyz/v1/sandboxes/sbx_123/fork \
  -H 'Authorization: Bearer sk_live_...' \
  -H 'Content-Type: application/json' \
  -d '{"snapshotId":"snap_abc"}'
POST/v1/sandboxes/:id/stopAPI key

Gracefully shut down the sandbox.

Response
Sandbox
curl -s -X POST https://spawnd.oonamy.xyz/v1/sandboxes/sbx_123/stop \
  -H 'Authorization: Bearer sk_live_...'
DELETE/v1/sandboxes/:idAPI key

Kill the sandbox and reap all host resources. Idempotent — killing a dead sandbox still returns ok.

Response
{ "ok": boolean }
curl -s -X DELETE https://spawnd.oonamy.xyz/v1/sandboxes/sbx_123 \
  -H 'Authorization: Bearer sk_live_...'

API · Usage

Query the org's resource-seconds ledger over a time window.

GET/v1/usageAPI key

Return the org's resource-seconds (vCPU-s + GiB-s) over a time window.

Response
UsageSummary
curl -s 'https://spawnd.oonamy.xyz/v1/usage?from=2026-08-01T00:00:00Z&to=2026-08-07T00:00:00Z' \
  -H 'Authorization: Bearer sk_live_...'

Errors

Every API error is { code, message, retryable }. Only capacity is generally worth retrying.

CodeMeaningHTTP
unauthorizedMissing, invalid, or wrong-surface credential401
not_foundNo such sandbox or resource for this org404
invalid_stateThe verb is illegal for the sandbox's current state409
quota_exceededOrg quota reached429
capacityNo host slot right now (retryable)503
guest_timeoutThe guest did not respond in time504
internalUnexpected server error500

CLI

The spawn CLI wraps the same REST API. Authenticate once with your org API key, then drive sandboxes from the terminal.

CommandDoes
spawn smallboxCreate and enter a smallbox (1 vCPU / 2048 MiB).
spawn bigboxCreate and enter a bigbox (4 vCPU / 8192 MiB).
spawn lsList your org's sandboxes.
spawn exec <id> -- <cmd>Run a command inside a sandbox and stream the result.
spawn fork <id>Copy-on-write clone a sandbox from its latest snapshot.
spawn rm <id>Kill a sandbox and reap its resources.
# create a bigbox, run a build, then tear it down
spawn bigbox
spawn exec sbx_123 -- cargo build --release
spawn rm sbx_123

MCP server

The orchestrator exposes sandbox control as an MCP server, so any MCP client — an IDE, an agent framework, an LLM app — can drive sandboxes as tools. Register it with your org API key as the bearer credential.

client registration
{
  "mcpServers": {
    "spawnd": {
      "url": "https://spawnd.oonamy.xyz/mcp",
      "headers": { "Authorization": "Bearer sk_live_..." }
    }
  }
}
ToolDoes
spawn_sandboxCreate a sandbox from a size template.
execRun a command inside a sandbox.
write_fileWrite a file into the guest.
read_fileRead a file from the guest.
expose_portExpose a guest port and get a preview URL.
snapshot_sandboxTake a named snapshot.
fork_sandboxCopy-on-write clone from a snapshot.
pause_sandbox / resume_sandboxPause or resume a sandbox.
kill_sandboxKill a sandbox and reap resources.

Preview URLs

Expose a guest port to the public internet with POST /v1/sandboxes/:id/ports. The response url is http://<sandbox-id>--<workspace>.spawnd.oonamy.xyz — a single DNS label (double-dash separator) so one *.spawnd.oonamy.xyz wildcard covers every preview — routed by the HTTP Host header. Previews are org-scoped by construction and never outlive their VM — stopping or killing the sandbox deregisters the mapping.

# start a dev server in the sandbox, expose it, share the URL
spawn exec sbx_123 -- python3 -m http.server 8000 &
curl -s -X POST https://spawnd.oonamy.xyz/v1/sandboxes/sbx_123/ports \
  -H 'Authorization: Bearer sk_live_...' \
  -H 'Content-Type: application/json' -d '{"port":8000}'
# → { "url": "http://sbx_123--acme.spawnd.oonamy.xyz" }

# routed by Host header, so it is testable locally:
curl -H 'Host: sbx_123--acme.spawnd.oonamy.xyz' http://127.0.0.1:8080/

Everything on this page is also available as one self-contained markdown file for agents to ingest.