Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.stacyide.xyz/llms.txt

Use this file to discover all available pages before exploring further.

StacyVM REST API Reference

This document is the source of truth for the StacyVM HTTP API. The Python and TypeScript SDKs are thin wrappers over these endpoints — anything they do, you can do with curl.
  • Base URL: http://localhost:7423/api/v1
  • Content type: application/json (request and response, except where noted)
  • OpenAPI spec: swagger.yaml / swagger.json

Table of contents


Authentication

Optional headers:
HeaderPurposeRequired when
X-API-KeyAPI key authenticationauth.enabled: true in stacyvm.yaml
X-Admin-API-KeyAdmin API key authenticationauth.admin_api_key is configured and calling /api/v1/admin/*
X-User-IDMulti-tenant pool mode user identifierpool.enabled: true
curl -H 'X-API-Key: sk-xyz123' \
     -H 'X-User-ID: alice@example.com' \
     http://localhost:7423/api/v1/sandboxes
CORS is permissive by default for local development (server.cors_allowed_origins: ["*"]). Public deployments should set exact origins, for example:
server:
  cors_allowed_origins:
    - "https://console.example.com"
stacyvm config lint --production fails when CORS is left wildcard or empty. X-User-ID is trimmed when present. It must be 128 characters or fewer and cannot contain whitespace, control characters, or path separators.

Rate limiting

API rate limiting is optional and disabled by default. When rate_limit.enabled is true, StacyVM applies an in-memory token bucket to API routes.
rate_limit:
  enabled: true
  requests_per_minute: 120
  burst: 60
  key_by: owner # owner, api_key, or ip
  bucket_ttl: 15m
  cleanup_interval: 1m
The default owner mode uses X-User-ID when present, then falls back to X-API-Key, then client IP. Limited requests return 429 Too Many Requests with Retry-After, X-RateLimit-Limit, and X-RateLimit-Remaining headers. Rate-limit buckets store hashed identity keys internally; raw owner IDs, API keys, and IP addresses are not exposed in diagnostics or metrics.

Conventions

  • IDs. Sandbox IDs look like sb-a1b2c3d4. Templates are addressed by name.
  • Durations. All ttl and timeout fields use Go duration strings: 30s, 5m, 1h30m.
  • Timestamps. ISO 8601 UTC, e.g. 2026-05-04T10:30:00Z.
  • File modes. Octal strings, e.g. "755", "644".
  • Streaming. POST /sandboxes/{id}/exec switches to NDJSON (application/x-ndjson) when stream: true.

Errors

Errors return a JSON body with HTTP status reflecting the failure class:
{
  "code": "not_found",
  "message": "sandbox sb-a1b2c3d4 not found"
}
StatusCodeWhen
400bad_requestInvalid input — missing field, malformed JSON
401unauthorizedBad / missing API key
404not_foundSandbox / template / provider does not exist
409conflictTemplate name already exists
429resource_limitQuota, capacity, or API rate limit exceeded
500provider_errorProvider failed (Docker, Firecracker, etc.)
503unavailablePool full with overflow: reject

Admin API

StacyVM supports an optional separate admin API key:
auth:
  api_key: "sk-client"
  admin_api_key: "sk-admin"
  admin_fallback_enabled: false
Use X-Admin-API-Key for admin requests. X-API-Key is still accepted when it matches the admin key. If auth.admin_api_key is empty, admin routes fall back to auth.api_key for backwards compatibility unless auth.admin_fallback_enabled is set to false. For dashboard setup, quota workflows, diagnostics, audit history, CSV export, and storage notes, see admin-control-plane. Admin route aliases:
MethodPathPurpose
GET/api/v1/admin/providersList providers with health details
GET/api/v1/admin/providers/{name}Provider detail
POST/api/v1/admin/providers/testRun provider health checks
GET/api/v1/admin/quotasList owner quota overrides
GET/api/v1/admin/quotas/summaryRedacted quota coverage summary
GET/api/v1/admin/quotas/{ownerID}Get owner quota
PUT/api/v1/admin/quotas/{ownerID}Create or update owner quota
GET/api/v1/admin/quotas/{ownerID}/usageOwner usage against effective quota
DELETE/api/v1/admin/quotas/{ownerID}Delete owner quota
GET/api/v1/admin/diagnosticsRedacted operational diagnostics
GET/api/v1/admin/metricsStructured JSON metrics
GET/api/v1/admin/metrics/prometheusPrometheus metrics
GET/api/v1/admin/auditFilterable admin audit history, with optional format=csv
The existing non-admin paths remain available for compatibility in this phase.

Sandboxes

Spawn a sandbox

POST /api/v1/sandboxes
Request body (all fields optional, server defaults apply):
{
  "image": "python:3.12",
  "provider": "docker",
  "memory_mb": 1024,
  "vcpus": 2,
  "ttl": "1h",
  "metadata": { "user": "alice" }
}
Response 201 Created:
{
  "id": "sb-a1b2c3d4",
  "state": "running",
  "provider": "docker",
  "image": "python:3.12",
  "memory_mb": 1024,
  "vcpus": 2,
  "created_at": "2026-05-04T10:30:00Z",
  "expires_at": "2026-05-04T11:30:00Z",
  "metadata": { "user": "alice" },
  "preview_domain": "localhost"
}

Evaluate spawn admission

POST /api/v1/sandboxes/admission
Preflight a spawn request against current quota and scheduler limits without creating a sandbox. X-User-ID overrides owner_id, matching the spawn endpoint. Request body: same shape as POST /api/v1/sandboxes. Response 200 OK:
{
  "allowed": false,
  "queueable": true,
  "reason": "max_sandboxes",
  "active_sandboxes": 100,
  "max_sandboxes": 100,
  "active_owner_sandboxes": 2,
  "max_owner_sandboxes": 10,
  "max_ttl": "24h0m0s"
}
queueable reflects the configured spawn overflow mode. Capacity denials are queueable only when defaults.spawn_overflow is queue; TTL denials are never queueable.

List sandboxes

GET /api/v1/sandboxes
Response 200 OK: array of sandbox objects.

Get a sandbox

GET /api/v1/sandboxes/{id}
Response 200 OK or 404 Not Found.

Destroy a sandbox

DELETE /api/v1/sandboxes/{id}
Response 200 OK:
{ "status": "destroyed" }

Prune expired sandboxes

DELETE /api/v1/sandboxes
Response 200 OK:
{ "pruned": 7 }

Extend TTL

POST /api/v1/sandboxes/{id}/extend
Request body:
{ "ttl": "1h" }
Response 200 OK: full sandbox object with updated expires_at.

Execute a command

POST /api/v1/sandboxes/{id}/exec
Request body:
{
  "command": "python3 -c 'print(40+2)'",
  "args": ["--coverage"],
  "env": { "NODE_ENV": "test" },
  "workdir": "/app",
  "timeout": "30s",
  "stream": false
}
Response 200 OK (non-streaming):
{
  "exit_code": 0,
  "stdout": "42\n",
  "stderr": "",
  "duration": "127ms"
}
Response 200 OK (streaming, stream: true): application/x-ndjson — one JSON object per line:
{"stream":"stdout","data":"installing pandas...\n"}
{"stream":"stdout","data":"done\n"}
{"stream":"stderr","data":"warning: deprecated flag\n"}

Console logs

GET /api/v1/sandboxes/{id}/logs?lines=200
lines defaults to 100. Response 200 OK:
["[init] mounting /workspace", "[init] starting agent", "..."]

Files

All file paths are absolute inside the sandbox. The endpoints below are scoped under /sandboxes/{id}/files.

Write a file

POST /api/v1/sandboxes/{id}/files
{ "path": "/app/main.py", "content": "print('hi')", "mode": "644" }
Response 200 OK: { "status": "written" }.

Read a file

GET /api/v1/sandboxes/{id}/files?path=/app/main.py
Response 200 OK: raw file contents (binary safe). The SDKs decode as UTF-8.

Delete a file or directory

DELETE /api/v1/sandboxes/{id}/files?path=/app/cache&recursive=true
recursive defaults to false. Response 200 OK: { "status": "deleted" }.

List a directory

GET /api/v1/sandboxes/{id}/files/list?path=/app
path defaults to /. Response 200 OK:
[
  {
    "name": "main.py",
    "path": "/app/main.py",
    "size": 11,
    "is_dir": false,
    "mod_time": "2026-05-04T10:32:14Z",
    "mode": "0644"
  }
]

Move / rename

POST /api/v1/sandboxes/{id}/files/move
{ "old_path": "/app/main.py", "new_path": "/app/entry.py" }
Response 200 OK: { "status": "moved" }.

Change permissions

POST /api/v1/sandboxes/{id}/files/chmod
{ "path": "/app/run.sh", "mode": "755" }
Response 200 OK: { "status": "chmod applied" }.

Stat

GET /api/v1/sandboxes/{id}/files/stat?path=/app/main.py
Response 200 OK: a single FileInfo object (same shape as list).

Glob

GET /api/v1/sandboxes/{id}/files/glob?pattern=/app/**/*.py
Response 200 OK:
["/app/main.py", "/app/utils/helpers.py"]

Templates

Create a template

POST /api/v1/templates
{
  "name": "python-dev",
  "image": "python:3.12-slim",
  "memory_mb": 1024,
  "vcpus": 2,
  "ttl": "1h",
  "provider": "docker",
  "metadata": { "language": "python" }
}
Response 201 Created: the template object. 409 Conflict if name is taken.

List templates

GET /api/v1/templates
Response 200 OK: array of templates.

Get a template

GET /api/v1/templates/{name}
Response 200 OK or 404 Not Found.

Update a template

PUT /api/v1/templates/{name}
Same body as create (without name). Response 200 OK or 404.

Delete a template

DELETE /api/v1/templates/{name}
Response 200 OK: { "status": "deleted" }.

Spawn from a template

POST /api/v1/templates/{name}/spawn
Optional override body:
{ "ttl": "30m", "provider": "firecracker" }
Response 201 Created: full sandbox object.

Quotas

Owner quotas are persisted overrides for per-owner sandbox and runtime limits. They apply when requests include an owner via X-User-ID or owner_id. Owner IDs are trimmed and must be 128 characters or fewer. They cannot contain whitespace, control characters, or path separators. Quota durations must use whole-second Go duration strings; use 0s or omit a duration to inherit the global default.

List owner quotas

GET /api/v1/quotas
Response 200 OK:
[
  {
    "owner_id": "team-a",
    "max_sandboxes": 5,
    "max_ttl": "2h0m0s",
    "max_exec_timeout": "1m0s",
    "created_at": "2026-05-08T10:30:00Z",
    "updated_at": "2026-05-08T10:30:00Z"
  }
]

Get quota summary

GET /api/v1/quotas/summary
Returns redacted policy coverage counts without exposing owner IDs. Response 200 OK:
{
  "total": 2,
  "with_max_sandboxes": 1,
  "with_max_ttl": 1,
  "with_max_exec_timeout": 1
}

Save owner quota

PUT /api/v1/quotas/{ownerID}
Request:
{
  "max_sandboxes": 5,
  "max_ttl": "2h",
  "max_exec_timeout": "1m"
}
Response 200 OK: full owner quota object. Invalid owner IDs, negative sandbox counts, malformed durations, sub-second durations, and fractional-second durations return 400 Bad Request.

Get owner usage

GET /api/v1/quotas/{ownerID}/usage
Response 200 OK:
{
  "owner_id": "team-a",
  "active_sandboxes": 3,
  "max_sandboxes": 5,
  "max_ttl": "2h0m0s",
  "max_exec_timeout": "1m0s",
  "quota_configured": true
}

Delete owner quota

DELETE /api/v1/quotas/{ownerID}
Response 200 OK: { "status": "deleted" }.

Providers

List providers

GET /api/v1/providers
Response 200 OK:
[
  {
    "name": "docker",
    "healthy": true,
    "default": true,
    "latency_ms": 3,
    "last_checked": "2026-05-08T10:30:00Z",
    "capabilities": ["spawn", "exec", "exec_stream", "files", "console", "health", "runtime_inventory", "container"],
    "runtime_count": 4
  },
  {
    "name": "firecracker",
    "healthy": false,
    "default": false,
    "latency_ms": 1,
    "last_checked": "2026-05-08T10:30:00Z",
    "error": "health check returned false",
    "capabilities": ["spawn", "exec", "exec_stream", "files", "console", "health", "snapshots", "microvm", "vsock_agent"]
  }
]

Get a provider

GET /api/v1/providers/{name}
Response 200 OK:
{
  "name": "docker",
  "healthy": true,
  "default": true,
  "sandbox_count": 12,
  "health": {
    "name": "docker",
    "healthy": true,
    "default": true,
    "latency_ms": 3,
    "last_checked": "2026-05-08T10:30:00Z",
    "capabilities": ["spawn", "exec", "files", "runtime_inventory", "container"],
    "runtime_count": 4
  },
  "config": { "runtime": "runc", "network_mode": "stacyvm-network" }
}

Health-check all providers

POST /api/v1/providers/test
Response 200 OK:
{ "docker": true, "firecracker": true, "mock": true }

Workers

Worker registry endpoints expose the control-plane view of StacyVM workers. In single-node mode the API server registers itself as the local worker at startup. Remote workers heartbeat through /api/v1/worker/* using worker credentials. Admins can still manage registry records under /api/v1/admin/workers/*.

List workers

GET /api/v1/workers
Response 200 OK:
[
  {
    "id": "local",
    "hostname": "stacyvm-host-1",
    "status": "online",
    "providers": ["docker", "mock"],
    "capabilities": ["api", "single_node", "spawn", "exec", "files"],
    "capacity": { "max_sandboxes": 100, "max_sandboxes_per_owner": 10 },
    "last_heartbeat": "2026-05-08T10:30:00Z",
    "created_at": "2026-05-08T10:00:00Z",
    "updated_at": "2026-05-08T10:30:00Z",
    "stale": false
  }
]

Get a worker

GET /api/v1/workers/{workerID}
Response 200 OK: one worker object.

Heartbeat a worker

POST /api/v1/worker/{workerID}/heartbeat
Required headers:
X-Worker-ID: worker-a
X-Worker-Token: <signed worker token, auth.worker_tokens.worker-a, or auth.worker_token>
Request:
{
  "hostname": "worker-a.internal",
  "status": "online",
  "providers": ["docker"],
  "capabilities": ["spawn", "exec", "files"],
  "capacity": { "max_sandboxes": 50, "max_sandboxes_per_owner": 5 }
}
Response 200 OK: updated worker object. Admin heartbeat aliases remain available at /api/v1/admin/workers/{workerID}/heartbeat for controlled registry repair and test setup.

Renew a worker lease

POST /api/v1/worker/{workerID}/leases/{resourceID}/renew
Required headers:
X-Worker-ID: worker-a
X-Worker-Token: <signed worker token, auth.worker_tokens.worker-a, or auth.worker_token>
auth.worker_token is the shared staging token. For production-aligned worker identity, configure auth.worker_signing_key for short-lived signed worker tokens or auth.worker_tokens.<workerID> for individually rotatable static credentials during migration. Request:
{ "ttl": "30s" }
Response 200 OK:
{
  "lease": {
    "resource_id": "sb-abc123",
    "holder_id": "worker-a",
    "generation": 4,
    "expires_at": "2026-05-09T10:31:00Z"
  }
}

Delete a worker

DELETE /api/v1/admin/workers/{workerID}
Response 200 OK:
{ "status": "deleted" }

Snapshots

List Firecracker snapshots

GET /api/v1/snapshots
Response 200 OK: array of snapshot summaries (image name, kernel, size, created_at).

Pool

Pool status

GET /api/v1/pool/status
Response 200 OK (pool enabled):
{
  "enabled": true,
  "vms": 3,
  "max_vms": 20,
  "total_users": 14,
  "max_users_per_vm": 5
}
Response 200 OK (pool disabled):
{ "enabled": false }

System

Health

GET /api/v1/health
Response 200 OK:
{ "status": "ok", "version": "0.5.1", "uptime": "2h13m" }

Liveness

GET /api/v1/live
Response 200 OK:
{ "status": "alive", "version": "0.5.1", "uptime": "2h13m" }
Use this endpoint for process liveness checks. It only confirms that the API process is responding.

Readiness

GET /api/v1/ready
Response 200 OK:
{
  "status": "ready",
  "version": "0.5.1",
  "uptime": "2h13m",
  "ready_providers": 1,
  "total_providers": 2,
  "providers": [
    {
      "name": "docker",
      "healthy": true,
      "default": true,
      "latency_ms": 3,
      "last_checked": "2026-05-08T10:30:00Z",
      "capabilities": ["spawn", "exec", "files", "runtime_inventory", "container"],
      "runtime_count": 4
    },
    {
      "name": "firecracker",
      "healthy": false,
      "default": false,
      "latency_ms": 1,
      "last_checked": "2026-05-08T10:30:00Z",
      "error": "health check returned false",
      "capabilities": ["spawn", "exec", "files", "snapshots", "microvm", "vsock_agent"]
    }
  ]
}
Response 503 Service Unavailable when no configured provider is healthy.

Diagnostics

GET /api/v1/diagnostics
Response 200 OK:
{
  "generated_at": "2026-05-08T10:30:00Z",
  "build": {
    "version": "0.5.1",
    "goos": "linux",
    "goarch": "amd64"
  },
  "process": {
    "uptime": "2h13m",
    "goroutines": 42,
    "memory": {
      "alloc": 17825792,
      "sys": 71303168,
      "heap_alloc": 17825792,
      "gc_cycles": 8
    }
  },
  "store": {
    "healthy": true,
    "latency_ms": 1
  },
  "limits": {
    "max_sandboxes": 100,
    "max_sandboxes_per_owner": 10,
    "default_exec_timeout": "30s",
    "max_exec_timeout": "10m0s",
    "max_ttl": "24h0m0s",
    "spawn_overflow": "queue",
    "spawn_queue_timeout": "30s",
    "max_spawn_queue": 100
  },
  "scheduler": {
    "spawn_overflow": "queue",
    "spawn_queue_depth": 3,
    "max_spawn_queue": 100,
    "spawn_queue_timeout": "30s",
    "admission_control": "worker_aware_local",
    "spawn_queued_total": 18,
    "spawn_dequeued_total": 16,
    "spawn_queue_timeouts": 2,
    "spawn_queue_wait_count": 18,
    "spawn_queue_wait_total": "1m42s",
    "spawn_queue_wait_max": "12s",
    "spawn_queue_wait_avg": "5.666s",
    "spawn_queue_wait_total_ms": 102000,
    "spawn_queue_wait_max_ms": 12000,
    "spawn_queue_wait_avg_ms": 5666,
    "worker_id": "local",
    "selected_worker_id": "local",
    "eligible_workers": 1
  },
  "quotas": {
    "total": 8,
    "with_max_sandboxes": 6,
    "with_max_ttl": 4,
    "with_max_exec_timeout": 3
  },
  "rate_limit": {
    "enabled": true,
    "requests_per_minute": 120,
    "burst": 60,
    "key_by": "owner",
    "active_buckets": 14,
    "allowed_total": 9132,
    "limited_total": 27,
    "evicted_total": 4,
    "bucket_ttl": "15m0s",
    "cleanup_interval": "1m0s"
  },
  "providers": [
    {
      "name": "docker",
      "healthy": true,
      "default": true,
      "latency_ms": 3,
      "last_checked": "2026-05-08T10:30:00Z",
      "capabilities": ["spawn", "exec", "files", "runtime_inventory", "container"],
      "runtime_count": 4
    }
  ],
  "workers": {
    "total": 1,
    "online": 1,
    "stale": 0,
    "unhealthy": 0,
    "items": [
      {
        "id": "local",
        "hostname": "stacyvm-host-1",
        "status": "online",
        "providers": ["docker", "mock"],
        "capabilities": ["api", "single_node", "spawn", "exec", "files"],
        "capacity": { "max_sandboxes": 100, "max_sandboxes_per_owner": 10 },
        "last_heartbeat": "2026-05-08T10:30:00Z",
        "created_at": "2026-05-08T10:00:00Z",
        "updated_at": "2026-05-08T10:30:00Z",
        "stale": false
      }
    ]
  },
  "leases": {
    "total": 1,
    "active": 1,
    "expired": 0,
    "by_holder": { "local": 1 }
  },
  "sandboxes": {
    "total": 138,
    "active": 12,
    "by_state": { "running": 12, "destroyed": 126 },
    "by_provider": { "docker": 90, "firecracker": 48 },
    "by_worker": { "local": 138 }
  },
  "events": {
    "subscribers": 2,
    "history_size": 1000,
    "events_total": 2401
  },
  "operations": [],
  "remediation": {
    "admin_control_plane": "docs/admin-control-plane.md",
    "deployment": "docs/deployment.md",
    "production_readiness": "docs/production-readiness.md",
    "public_support_matrix": "docs/public-support-matrix.md",
    "release_verification": "docs/releasing.md",
    "runtime_certification": "docs/runtime-certification.md",
    "runtime_conformance": "docs/runtime-conformance.md",
    "security_governance": "docs/security-governance.md",
    "support_bundle": "docs/deployment.md#support-bundles",
    "upgrade_and_rollback": "docs/deployment.md#upgrade-rehearsal-and-rollback"
  },
  "redactions": ["provider secrets", "registry credentials", "environment secrets", "API keys"]
}
Diagnostics are read-only and intentionally redacted. Use this endpoint for support bundles, incident debugging, and deployment sanity checks. The remediation object points operators to the first public document to use when a diagnostics area needs follow-up.

Metrics

GET /api/v1/metrics
Response 200 OK:
{
  "uptime": "2h13m",
  "goroutines": 42,
  "memory_alloc": 17825792,
  "memory_sys": 71303168,
  "memory_heap_alloc": 17825792,
  "gc_cycles": 8,
  "sandboxes": {
    "total": 138,
    "active": 12,
    "by_state": { "running": 12, "destroyed": 126 },
    "by_provider": { "docker": 90, "firecracker": 48 },
    "by_worker": { "local": 138 }
  },
  "providers": {
    "total": 2,
    "healthy": 1,
    "items": [
      { "name": "docker", "healthy": true, "default": true },
      { "name": "firecracker", "healthy": false, "default": false }
    ]
  },
  "workers": {
    "total": 1,
    "online": 1,
    "stale": 0,
    "unhealthy": 0,
    "items": [
      {
        "id": "local",
        "hostname": "stacyvm-host-1",
        "status": "online",
        "providers": ["docker", "mock"],
        "capabilities": ["api", "single_node", "spawn", "exec", "files"],
        "capacity": { "max_sandboxes": 100, "max_sandboxes_per_owner": 10 },
        "last_heartbeat": "2026-05-08T10:30:00Z",
        "created_at": "2026-05-08T10:00:00Z",
        "updated_at": "2026-05-08T10:30:00Z",
        "stale": false
      }
    ]
  },
  "leases": {
    "total": 1,
    "active": 1,
    "expired": 0,
    "by_holder": { "local": 1 }
  },
  "events": {
    "subscribers": 2,
    "history_size": 1000,
    "events_total": 2401
  },
  "scheduler": {
    "spawn_overflow": "queue",
    "spawn_queue_depth": 3,
    "max_spawn_queue": 100,
    "spawn_queue_timeout": "30s",
    "admission_control": "worker_aware_local",
    "spawn_queued_total": 18,
    "spawn_dequeued_total": 16,
    "spawn_queue_timeouts": 2,
    "spawn_queue_wait_count": 18,
    "spawn_queue_wait_total": "1m42s",
    "spawn_queue_wait_max": "12s",
    "spawn_queue_wait_avg": "5.666s",
    "spawn_queue_wait_total_ms": 102000,
    "spawn_queue_wait_max_ms": 12000,
    "spawn_queue_wait_avg_ms": 5666,
    "worker_id": "local",
    "selected_worker_id": "local",
    "eligible_workers": 1
  },
  "quotas": {
    "total": 8,
    "with_max_sandboxes": 6,
    "with_max_ttl": 4,
    "with_max_exec_timeout": 3
  },
  "rate_limit": {
    "enabled": true,
    "requests_per_minute": 120,
    "burst": 60,
    "key_by": "owner",
    "active_buckets": 14,
    "allowed_total": 9132,
    "limited_total": 27,
    "evicted_total": 4,
    "bucket_ttl": "15m0s",
    "cleanup_interval": "1m0s"
  },
  "operations": [
    {
      "operation": "exec",
      "provider": "docker",
      "success_total": 482,
      "failure_total": 7,
      "latency_count": 489,
      "latency_total_ms": 39120,
      "latency_min_ms": 3,
      "latency_max_ms": 2500,
      "latency_avg_ms": 80
    }
  ]
}

Prometheus metrics

GET /api/v1/metrics/prometheus
Response 200 OK:
# HELP stacyvm_uptime_seconds StacyVM API process uptime in seconds.
# TYPE stacyvm_uptime_seconds gauge
stacyvm_uptime_seconds 7980
# HELP stacyvm_provider_healthy Provider health status where 1 is healthy and 0 is unhealthy.
# TYPE stacyvm_provider_healthy gauge
stacyvm_provider_healthy{provider="docker",default="true"} 1
stacyvm_spawn_queue_depth 3
stacyvm_spawn_queue_wait_milliseconds_count 18
stacyvm_owner_quotas_total 8
stacyvm_rate_limit_blocked_total 27
stacyvm_workers_total{status="total"} 1
stacyvm_workers_total{status="online"} 1
stacyvm_workers_total{status="stale"} 0
stacyvm_workers_total{status="unhealthy"} 0
stacyvm_leases_total{status="total"} 1
stacyvm_leases_total{status="active"} 1
stacyvm_leases_total{status="expired"} 0
stacyvm_sandboxes_by_worker_total{worker="local"} 138
stacyvm_operation_success_total{operation="exec",provider="docker"} 482
stacyvm_operation_failure_total{operation="exec",provider="docker"} 7
Use this endpoint for Prometheus-compatible scraping of runtime, provider, worker, sandbox, event, and operation metrics.

Events stream

GET /api/v1/events
Response 200 OK with Content-Type: text/event-stream. The server emits orchestrator events as Server-Sent Events:
data: {"id":"evt-1","type":"sandbox.created","sandbox_id":"sb-a1b2c3d4","timestamp":"2026-05-08T10:30:00Z"}

data: {"id":"evt-2","type":"exec.timeout","sandbox_id":"sb-a1b2c3d4","timestamp":"2026-05-08T10:31:00Z","data":{"operation":"exec","provider":"docker","error":"exec timeout: sb-a1b2c3d4"}}

data: {"id":"evt-3","type":"reconcile.action","sandbox_id":"sb-a1b2c3d4","timestamp":"2026-05-08T10:32:00Z","data":{"action":"adopted_runtime","provider":"docker","image":"python:3.12"}}
Common event types include:
  • sandbox.created, sandbox.running, sandbox.destroyed, sandbox.error
  • exec.started, exec.completed, exec.failed, exec.timeout
  • file.written, file.read
  • operation.failed, resource.limit, provider.failed, reconcile.action
  • spawn.queued, spawn.dequeued, spawn.queue_timeout
  • quota.saved, quota.deleted
Use any SSE client (EventSource in browsers, httpx-sse in Python, etc.) to consume.

WebSocket exec

GET /api/v1/sandboxes/{id}/exec/ws
Upgrades the connection to a WebSocket for interactive command execution. Useful for terminals, REPLs, and any case where you need bi-directional I/O. Client → server messages:
{ "type": "start",  "command": "python3", "env": { "PYTHONUNBUFFERED": "1" } }
{ "type": "stdin",  "data": "print('hi')\n" }
{ "type": "resize", "cols": 80, "rows": 24 }
{ "type": "signal", "signal": "SIGINT" }
Server → client messages:
{ "type": "stdout", "data": "hi\n" }
{ "type": "stderr", "data": "..." }
{ "type": "exit",   "exit_code": 0 }
The web dashboard uses this endpoint to power its live terminal — a concrete reference is at web/src/.

SDK mapping

If you’d rather write Python or TypeScript than curl, every endpoint above maps 1:1 to an SDK method:
EndpointPythonTypeScript
POST /sandboxesclient.spawn(...)client.spawn(...)
GET /sandboxes/{id}client.get(id)client.get(id)
POST /sandboxes/{id}/execsb.exec(cmd) / sb.exec_stream(cmd)sb.exec(cmd) / sb.execStream(cmd)
POST /sandboxes/{id}/filessb.write_file(path, content)sb.writeFile(path, content)
GET /sandboxes/{id}/filessb.read_file(path)sb.readFile(path)
POST /templates/{name}/spawnclient.spawn_template(name)client.templates.spawn(name)
GET /pool/statusclient.pool_status()client.poolStatus()
GET /healthclient.health()client.health()
Full SDK docs: Python · TypeScript.