> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stateset.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sandbox API Reference

> Full HTTP API reference for the StateSet Sandbox node controller.

# API Reference (Node Controller)

Base URL: `https://api.sandbox.stateset.com/api/v1`

The Node controller is the default deployed by `k8s/deployment.yaml`.

## Authentication

This API supports three auth modes:

1. API key (recommended for servers/CLI):

```text theme={null}
Authorization: ApiKey sk_your_api_key
```

2. Session JWT (recommended for same-origin apps):

```text theme={null}
Authorization: Bearer eyJhbGciOi...
```

3. Browser session cookie (recommended for dashboards):

* `stateset_session` is an `httpOnly` cookie set by registration/login/invite accept/SSO callback.
* For state-changing requests authenticated via the session cookie (POST/PUT/PATCH/DELETE), include `X-CSRF-Token: <token>`.
* After you have a session cookie, the controller returns an `x-csrf-token` response header on safe requests (GET/HEAD/OPTIONS)
  and sets a `__stateset_csrf` cookie. Use that token value for subsequent writes.
* Requests authenticated via an `Authorization` header (API key or Bearer JWT) do not require CSRF.

## Errors

Errors are returned as:

```json theme={null}
{
  "error": {
    "code": "AUTH_REQUIRED",
    "message": "Authentication required"
  }
}
```

## Health & Monitoring

### GET /health

No auth.

### GET /ready

No auth. Returns 503 if not ready.

### GET /health/detailed

No auth.

### GET /metrics

Internal auth in production (`X-Internal-Token` or `Authorization: Bearer <internal-key>`).

## Authentication Endpoints

### POST /register

Creates an organization + user + API key and sets a session cookie.

Request:

```json theme={null}
{
  "first_name": "Ada",
  "last_name": "Lovelace",
  "organization_name": "Example Co",
  "email": "ada@example.com",
  "password": "SecurePassword123!",
  "use_case": "AI agent development"
}
```

Response:

```json theme={null}
{
  "organization": { "id": "org_...", "name": "Example Co", "slug": "example-co", "plan": "hobby" },
  "user": { "id": "usr_...", "email": "ada@example.com", "role": "owner" },
  "api_key": { "key": "sk_test_...", "key_prefix": "sk_test_...", "name": "Default API Key" },
  "token": "eyJhbGciOi..."
}
```

### POST /auth/login

Authenticates and returns a session payload (also sets `stateset_session` cookie).

Request:

```json theme={null}
{ "email": "ada@example.com", "password": "SecurePassword123!" }
```

Response:

```json theme={null}
{
  "token": "eyJhbGciOi...",
  "expiresAt": "2030-01-01T00:00:00.000Z",
  "user": {
    "id": "usr_...",
    "email": "ada@example.com",
    "firstName": "Ada",
    "lastName": "Lovelace",
    "role": "owner",
    "emailVerified": false
  },
  "organization": {
    "id": "org_...",
    "name": "Example Co",
    "slug": "example-co",
    "plan": "hobby"
  }
}
```

### POST /auth/logout

Clears the session cookie.

### GET /auth/providers

Returns which optional auth providers are configured.

Response shape:

```json theme={null}
{
  "workos": {
    "enabled": true,
    "redirect_uri": "https://sandbox.stateset.com/auth/callback"
  }
}
```

### GET /auth/session

Returns the current session payload (same shape as `/auth/login`).

Auth: cookie session (`stateset_session`) or `Authorization: Bearer <token>`.

### GET /auth/workos/authorize

Starts WorkOS AuthKit SSO. Redirects to WorkOS and sets short-lived PKCE cookies.

Requires `WORKOS_API_KEY`, `WORKOS_CLIENT_ID`, `WORKOS_REDIRECT_URI`.

Query params:

* `return_to`: redirect destination after successful login (same-origin relative paths only; protocol-relative values like `//example.com` are rejected)
* `organization_id`, `connection_id`, `domain_hint`, `login_hint`, `screen_hint`: WorkOS hints

### GET /auth/workos/callback

WorkOS callback. Exchanges `code` for user identity, then sets a `stateset_session` cookie and redirects back.

### GET /auth/workos/link

Returns the current WorkOS linking status for the authenticated organization.

Auth: **cookie session** (admin/owner).

### POST /auth/workos/link

Links a WorkOS Organization ID to the authenticated organization.

Auth: **cookie session** (owner).

Request:

```json theme={null}
{ "workos_organization_id": "workos_org_..." }
```

### DELETE /auth/workos/link

Unlinks WorkOS from the authenticated organization.

Auth: **cookie session** (owner).

When `AUDIT_LOGS_ENABLED=true`, WorkOS link/unlink operations emit audit actions:

* `auth.sso.link`
* `auth.sso.unlink`

## Sandboxes

### POST /sandbox/create

Creates a new sandbox.

Request:

```json theme={null}
{
  "cpus": "2",
  "memory": "2Gi",
  "timeout_seconds": 600,
  "isolation": "wasm",
  "env": { "EXAMPLE": "true" },
  "wasm_network": {
    "enabled": true,
    "allow_tcp": true,
    "allow_udp": false,
    "allow_ip_lookup": true
  }
}
```

Notes:

* `isolation` supports `container`, `gvisor`, `microvm`, and `wasm`.
* `wasm_network` is valid only when `isolation` is `wasm`.

Response:

```json theme={null}
{
  "sandbox_id": "f3533033-a515-4f29-a440-74ab29021be5",
  "org_id": "org_...",
  "session_id": "1c3a...",
  "status": "running",
  "pod_ip": "10.34.3.135",
  "created_at": "2026-01-16T03:24:05.549Z",
  "expires_at": "2026-01-16T03:34:05.549Z",
  "startup_metrics": { "total_ms": 2634 }
}
```

### GET /sandboxes?limit=20\&offset=0

Lists sandboxes for the authenticated organization.

### GET /sandbox/:id

Returns sandbox details.

### GET /sandbox/:id/status

Returns status + expiry (lightweight).

### POST /sandbox/:id/stop

Stops/deletes the sandbox.

Alias:

* `DELETE /sandbox/:id`

## Command Execution

### POST /sandbox/:id/execute

Executes a command in the sandbox.

Request:

```json theme={null}
{
  "command": ["python3", "-c", "print(\"hello\")"],
  "working_dir": "/workspace",
  "env": { "DEBUG": "true" },
  "timeout": 60000,
  "stream": false
}
```

Response (non-streaming):

```json theme={null}
{ "exit_code": 0, "stdout": "hello\n", "stderr": "" }
```

Streaming (SSE):

* Set `"stream": true`
* Response `Content-Type: text/event-stream`
* Events are emitted as `data: {...}\n\n` with `type` values: `stdout`, `stderr`, `exit`, `done`, `error`

## File Operations

File contents are base64 encoded.

### POST /sandbox/:id/files

Writes one or more files.

Request:

```json theme={null}
{
  "files": [
    { "path": "/workspace/main.py", "content": "cHJpbnQoImhlbGxvIikK" }
  ]
}
```

### GET /sandbox/:id/files?path=/workspace/main.py

Reads a file (JSON response containing base64 content).

### GET /sandbox/:id/files/download?path=/workspace/main.py

Downloads a file as `application/octet-stream`.

## REPL Sessions

Interactive Python REPL with persistent session state. Variables and imports survive across execute calls. Requires `REPL_ENABLED=true`.

### POST /sandbox/:id/repl/sessions

Creates a new REPL session.

Request:

```json theme={null}
{ "language": "python" }
```

`language` is optional and defaults to `python`.

Response:

```json theme={null}
{
  "session": {
    "id": "repl_01H...",
    "sandboxId": "f3533033-a515-4f29-a440-74ab29021be5",
    "language": "python",
    "kernelId": "k_abc123",
    "status": "idle"
  }
}
```

### GET /sandbox/:id/repl/sessions

Lists REPL sessions for a sandbox.

Response:

```json theme={null}
{
  "sessions": [
    {
      "id": "repl_01H...",
      "sandboxId": "f3533033-a515-4f29-a440-74ab29021be5",
      "language": "python",
      "kernelId": "k_abc123",
      "status": "idle"
    }
  ]
}
```

### POST /sandbox/:id/repl/sessions/:sessionId/execute

Executes code in a REPL session. Variables and imports persist across calls.

Request:

```json theme={null}
{ "code": "x = 42\nprint(x)" }
```

Response:

```json theme={null}
{
  "executionCount": 1,
  "outputs": [
    { "type": "stdout", "text": "42\n" },
    { "type": "result", "data": { "text/plain": "42" } }
  ],
  "status": "ok",
  "durationMs": 15
}
```

### POST /sandbox/:id/repl/sessions/:sessionId/stream

Executes code with streaming output via SSE.

Request:

```json theme={null}
{ "code": "for i in range(5): print(i)" }
```

Response `Content-Type: text/event-stream`. Events are emitted as output is produced:

```text theme={null}
event: output
data: {"type":"stdout","text":"0\n"}

event: output
data: {"type":"stdout","text":"1\n"}

event: done
data: {"executionCount":2,"status":"ok","durationMs":25}
```

### POST /sandbox/:id/repl/sessions/:sessionId/interrupt

Interrupts a running execution in the session.

Response:

```json theme={null}
{ "status": "interrupted" }
```

### DELETE /sandbox/:id/repl/sessions/:sessionId

Destroys a REPL session and its kernel.

Response:

```json theme={null}
{ "status": "destroyed" }
```

### Session Status Values

| Status     | Description                     |
| ---------- | ------------------------------- |
| `starting` | Kernel is being provisioned     |
| `idle`     | Ready for code execution        |
| `busy`     | Currently executing code        |
| `dead`     | Kernel crashed or was destroyed |

### Output Types

| Type     | Description                                                  |
| -------- | ------------------------------------------------------------ |
| `stdout` | Standard output text                                         |
| `stderr` | Standard error text                                          |
| `result` | Execution result (rich MIME data in `data` field)            |
| `error`  | Execution error with `name`, `value`, and `traceback` fields |

***

## Templates

### GET /templates

Lists templates (summaries).

### GET /templates/categories

Lists categories.

### GET /templates/:id

Returns a template including file contents.

### POST /sandbox/create-from-template

Creates a sandbox based on a template.

## Secrets (BYOK)

Secrets are organization-scoped and injected into sandbox pods as environment variables.

### POST /secrets

Creates a secret.

### GET /secrets

Lists secrets (values are never returned).

### PUT /secrets/:name

Updates a secret.

### DELETE /secrets/:name

Deletes a secret.

## API Keys

### GET /api-keys

Lists API keys for the organization.

### POST /api-keys

Creates a new API key (the plaintext key is only returned once).

### DELETE /api-keys/:id

Revokes an API key.

### POST /api-keys/:id/regenerate

Regenerates a key (returns a new plaintext key).

## GitHub Integrations

These routes are available when GitHub integration routes are enabled.

### GET /github/installations

Lists linked/pending GitHub App installations for the authenticated organization.

### POST /github/installations/:id/connect

Links an installation to the authenticated organization.

### DELETE /github/installations/:id

Removes a linked installation from the authenticated organization.

### GET /github/settings

Returns GitHub automation settings for the organization.

Includes `auto_pr_comment_on_sandbox` to control whether PR-triggered sandboxes post an automatic GitHub comment with dashboard/sandbox details.

Includes `auto_stop_sandbox_on_pr_close` to control whether PR-triggered sandboxes are automatically stopped when the pull request is closed.

### PUT /github/settings

Updates GitHub automation settings.

### GET /github/events

Returns recent GitHub webhook processing events for the organization.

### GET /github/health?window\_hours=24

Returns integration health summary (installations, webhook success/failure counts, status, latest event/failure).

* `window_hours` is optional and must be an integer between `1` and `168`.

## Internal Snapshot API (`/internal`)

These endpoints require internal auth (`X-Internal-Token` or `Authorization: Bearer <internal-token>`) in production.

### GET /internal/snapshots

List all snapshot profiles.

Response:

```json theme={null}
{
  "snapshots": [
    {
      "profileKey": "2vcpu-2048mb",
      "memoryPath": "/var/lib/firecracker/snapshots/2vcpu-2048mb/memory.bin",
      "statePath": "/var/lib/firecracker/snapshots/2vcpu-2048mb/state.bin",
      "createdAt": "2026-02-20T12:00:00.000Z",
      "vcpuCount": 2,
      "memSizeMib": 2048,
      "snapshotType": "full",
      "memoryBackend": "file",
      "version": 1
    }
  ],
  "total": 1,
  "enabled": true
}
```

### GET /internal/snapshots/status

Snapshot service health and configuration.

```json theme={null}
{
  "enabled": true,
  "snapshotCount": 1,
  "snapshotDir": "/var/lib/firecracker/snapshots",
  "memoryBackend": "file",
  "snapshotType": "full"
}
```

### POST /internal/snapshots

Create a snapshot profile.

Request example:

```json theme={null}
{
  "profile": "2vcpu-2048mb",
  "vcpu_count": 2,
  "mem_size_mib": 2048,
  "kernel_path": "/var/lib/firecracker/kernel/vmlinux",
  "rootfs_path": "/var/lib/firecracker/rootfs/rootfs.ext4"
}
```

### GET /internal/snapshots/:profileKey

Get metadata for a single profile.

### DELETE /internal/snapshots/:profileKey

Delete a snapshot profile.

### POST /internal/snapshots/:profileKey/restore

Restore a snapshot profile into a mock Firecracker runtime.

### POST /internal/snapshots/warm

Warm cache, with optional body `{ "count": 3 }`.

### POST /internal/snapshots/cleanup

Remove old snapshots by age and/or keep policy.

Request example:

```json theme={null}
{
  "keep_most_recent": 10,
  "max_age_seconds": 86400,
  "dry_run": true
}
```

Response example:

```json theme={null}
{
  "message": "Snapshot cleanup completed",
  "dry_run": true,
  "profiles_removed": 2,
  "removed_profiles": [
    "1vcpu-512mb",
    "2vcpu-1024mb"
  ],
  "profiles_removed_by_age": 1,
  "profiles_removed_by_keep_limit": 1,
  "total_found": 4,
  "total_remaining": 2
}
```

## Idempotency

POST endpoints support safe retries via the `Idempotency-Key` header. When enabled (`IDEMPOTENCY_ENABLED=true`, default), the controller caches the response for each unique `(orgId, key, route)` tuple for 24 hours.

### Usage

```text theme={null}
POST /api/v1/sandbox/create
Authorization: ApiKey sk_...
Idempotency-Key: my-unique-request-id-123
Content-Type: application/json
```

### Behavior

* **First request**: Executes normally. Response is stored and returned with `Idempotency-Status: stored`.
* **Replay**: If the same `(orgId, key, route)` is seen again within 24h, the cached response is returned with `Idempotency-Status: replayed`.
* **Body mismatch**: If a replay has a different request body hash, the server returns `409 Conflict` with error code `IDEMPOTENCY_KEY_REUSE_MISMATCH`.
* **Scope**: POST requests only. The header is ignored on GET/PUT/PATCH/DELETE.
* **TTL**: 24 hours. After expiry the key can be reused.

### Response Headers

| Header               | Values               | Description                                                      |
| -------------------- | -------------------- | ---------------------------------------------------------------- |
| `Idempotency-Status` | `stored`, `replayed` | Whether the response was freshly computed or replayed from cache |

***

## Real-time Events (SSE)

`GET /events/stream` provides a Server-Sent Events stream of sandbox lifecycle events for the authenticated organization.

### Authentication

Because the browser `EventSource` API does not support custom headers, this endpoint accepts auth via multiple mechanisms:

1. `Authorization: Bearer <jwt>` header
2. `Authorization: ApiKey <key>` header
3. `?token=<jwt>` query parameter
4. `?apiKey=<key>` query parameter (requires `&orgId=<orgId>`)
5. Session cookie (`stateset_session`)

### Event Format

```text theme={null}
event: sandbox.created
id: evt_01H...
data: {"sandboxId":"sbx-abc","event":"sandbox.created","timestamp":"2026-02-27T12:00:00Z",...}

event: sandbox.stopped
id: evt_01H...
data: {"sandboxId":"sbx-abc","event":"sandbox.stopped",...}
```

### Event Types

`sandbox.created`, `sandbox.stopped`, `sandbox.deleted`, `artifact.uploaded`, `checkpoint.created`, `checkpoint.restored`, `mcp.tool_called`, `security.violation`, `resources.limit_warning`

### Resumable Streams

On reconnect, include the last received event ID to replay missed events:

* `Last-Event-ID` request header (standard SSE reconnect)
* `?lastEventId=<id>` query parameter

The server buffers recent events in memory and replays any events published after the given ID.

### Heartbeat

A `:heartbeat` comment is sent every 20 seconds to keep the connection alive.

***

## Concurrency Budgets & Queue Mode

Each organization has per-plan concurrency budgets for sandboxes and command executions.

### Per-Plan Budgets

| Plan       | Max Concurrent Sandboxes | Max Concurrent Executions |
| ---------- | ------------------------ | ------------------------- |
| Hobby      | 3                        | 5                         |
| Pro        | 10                       | 20                        |
| Team       | 25                       | 50                        |
| Enterprise | 100                      | 200                       |

Limits are configurable via `ORG_BUDGET_<PLAN>_MAX_CONCURRENT_SANDBOXES` and `ORG_BUDGET_<PLAN>_MAX_CONCURRENT_EXECUTES` environment variables.

### Exceeding Limits

When a request would exceed the budget:

* **Without queue mode**: `429 Too Many Requests` with error code `CONCURRENT_LIMIT_EXCEEDED`.
* **With queue mode** (`QUEUE_MODE_ENABLED=true`): `202 Accepted` with a queued request ID.

### Queued Response Format

```json theme={null}
{
  "status": "queued",
  "requestId": "req_abc123",
  "position": 3,
  "queue_depth": 5,
  "estimated_wait_seconds": 15
}
```

### Queued Response Headers

| Header                      | Description                    |
| --------------------------- | ------------------------------ |
| `X-Stateset-Queue-Status`   | `queued`                       |
| `X-Stateset-Queue-Position` | Position in queue              |
| `Retry-After`               | Seconds to wait before polling |

### Polling

`GET /requests/:requestId` returns the current status of a queued request, including `position`, `queue_depth`, and `estimated_wait_seconds`. Once completed, `response_body` and `response_status` are included.

### Queue Health

`GET /queue/health` returns per-org queue telemetry: `queue_depth`, `processing_count`, `oldest_queued_age_seconds`, `estimated_wait_seconds`, and rolling-window statistics (`completed_count`, `failed_count`, `expired_count`, `average_wait_ms`, `p95_wait_ms`).

### Error Codes

| Code                        | Status | Description                               |
| --------------------------- | ------ | ----------------------------------------- |
| `CONCURRENT_LIMIT_EXCEEDED` | 429    | Budget exceeded and queue mode is off     |
| `QUEUE_DEPTH_EXCEEDED`      | 429    | Queue is full (`QUEUE_MAX_DEPTH` reached) |

***

## Egress Policies

Per-organization outbound network policies. Requires `EGRESS_POLICIES_ENABLED=true`.

### Policy Model

```json theme={null}
{
  "id": "pol_abc123",
  "organizationId": "org_...",
  "policyName": "default",
  "allowAll": false,
  "allowedDomains": ["api.github.com", "*.npmjs.org"],
  "deniedDomains": ["evil.example.com"],
  "allowedPorts": [80, 443],
  "logAllRequests": true
}
```

### Domain Matching

* **Exact match**: `api.github.com` matches only `api.github.com`
* **Wildcard**: `*.npmjs.org` matches `registry.npmjs.org`, `www.npmjs.org`, etc.

### Enforcement Modes (`EGRESS_PROXY_MODE`)

| Mode      | Behavior                                           |
| --------- | -------------------------------------------------- |
| `off`     | No enforcement (default)                           |
| `monitor` | Log violations but allow all traffic               |
| `enforce` | Block denied traffic, return `EGRESS_DENIED` (403) |

### Error Code

| Code            | Status | Description                      |
| --------------- | ------ | -------------------------------- |
| `EGRESS_DENIED` | 403    | Request blocked by egress policy |

***

## Pagination

List endpoints support standard offset-based pagination.

### Query Parameters

| Parameter | Type    | Default | Max  | Description               |
| --------- | ------- | ------- | ---- | ------------------------- |
| `limit`   | integer | 100     | 1000 | Number of items to return |
| `offset`  | integer | 0       | —    | Number of items to skip   |

### Response Format

```json theme={null}
{
  "data": [...],
  "total": 42,
  "limit": 100,
  "offset": 0,
  "has_more": false
}
```

Endpoints that support pagination: `/sandboxes`, `/artifacts`, `/checkpoints`, `/webhooks`, `/audit`, `/egress/audit`, `/requests`.

***

## Optional Features

Some routes are only registered when corresponding services are enabled and the database is configured.

* REPL sessions: `/sandbox/:id/repl/sessions`, execute, stream, interrupt, destroy (`REPL_ENABLED=true`)
* Checkpoints: `/sandbox/:id/checkpoints`, `/checkpoints`, ...
* Artifacts: `/sandbox/:id/artifacts/upload`, `/artifacts`, multipart uploads at `/artifacts/uploads/initiate`, `/artifacts/uploads/:upload_id/parts/:part_number`, `/artifacts/uploads/:upload_id/complete`, `/artifacts/uploads/:upload_id/abort`
* Egress audit: `/egress/audit` (org query), `/internal/egress/audit` (proxy/internal ingest)
* Egress authz adapter: `/internal/egress/authorize` (proxy authorization check)
* Egress policy snapshot: `/internal/egress/policies` (proxy polling/sync)
* Webhooks: `/webhooks`, ...
* Tunnels: `/sandbox/:id/tunnels`, ...
* Audit: `/audit`, `/sandbox/:id/audit/summary`

Queue/backpressure notes:

* When a create/execute request is queued (`202`), responses include:
  * `X-Stateset-Queue-Status: queued`
  * `X-Stateset-Queue-Position`
  * `Retry-After`
* Queue poll (`GET /requests/:requestId`) includes `position`, `queue_depth`, and `estimated_wait_seconds`.
* Queue health (`GET /queue/health`) returns per-org queue telemetry for dashboards:
  * `queue_depth`, `processing_count`, `oldest_queued_age_seconds`
  * `estimated_wait_seconds`, `seconds_per_queue_position`
  * rolling-window counts and latency stats (`completed_count`, `failed_count`, `expired_count`, `average_wait_ms`, `p95_wait_ms`)
