# Sandbox

A Sandbox is an isolated execution environment where you can run code, manage files, and control network access. Each sandbox is created from a [Template](/docs/sandbox/template) and has a writable root filesystem that is checkpointed across pause/resume for the same sandbox identity.

For shell access and file copy over standard SSH clients, see [SSH](/docs/sandbox/ssh).

<Callout variant="info">
Sandbox0 Cloud clients should use `https://api.sandbox0.ai`. Set `SANDBOX0_BASE_URL` only when you are connecting to a self-hosted or private deployment.
</Callout>

## Sandbox Model

### Key Identifiers

| Field | Description |
|-------|-------------|
| `id` | Unique sandbox identifier (e.g., `sb_abc123`) |
| `template_id` | Template used to create this sandbox |
| `team_id` | Team that owns this sandbox |
| `runtime_id` | Opaque identifier of the current physical runtime allocation. Empty while paused. |
| `runtime_generation` | Monotonically increasing logical runtime generation. Resume commits a new generation. |
| `cluster_id` | Cluster where sandbox runs in multi-cluster deployments. Present on claim and list responses, not on sandbox detail responses. |

### Lifecycle States

| Status | Description |
|--------|-------------|
| `starting` | Sandbox is being initialized |
| `running` | Sandbox is active and ready to use |
| `paused` | Sandbox has no runtime; identity and latest rootfs checkpoint are preserved |
| `terminating` | Sandbox identity and durable state are being deleted |
| `failed` | Sandbox encountered an error |

<Callout variant="info">
PostgreSQL stores durable lifecycle intent, the committed runtime generation,
the exact Nomad allocation, and its resource/RootFS writer leases. `running` is
published only after ctld and the task driver prove network, RootFS, runsc, and
procd command readiness for that generation. During recovery, status returns to
`starting` until a replacement generation commits.
</Callout>

<Callout variant="info">
Pause and resume use internal lifecycle transactions, but `pausing` and `resuming` are not caller-visible statuses. Use the `paused` boolean as a convenience for `status == "paused"`. Pause does not preserve running processes, memory, sockets, PID state, or live REPL sessions.
</Callout>

### Persistent Root Filesystem

Sandbox0 persists the sandbox writable root filesystem as part of checkpointed pause/resume:

- when `ttl` expires or pause is requested, Sandbox0 publishes a block-COW rootfs checkpoint before releasing the runtime allocation
- when the sandbox resumes, Sandbox0 claims a fresh carrier and restores the latest rootfs generation before starting sandbox processes
- files written to the writable rootfs survive pause/resume for the same sandbox identity after a checkpoint succeeds
- rootfs checkpoints for the sandbox identity are deleted when the sandbox is deleted or `hard_ttl` expires

Use the root filesystem for transparent same-sandbox continuity. Use [Snapshot And Restore](/docs/sandbox/snapshot-restore) for named rootfs snapshots, restore, and fork operations across sandbox identities.

---

## Claim a Sandbox

Claim a sandbox from a template. Manager atomically selects a compatible
resource-neutral carrier and leases exact CPU and memory from dedicated node
capacity. A claim fails closed when carrier, physical capacity, RootFS device,
or node authority is unavailable.

<Endpoint method="POST">
/api/v1/sandboxes
</Endpoint>

### Request Body

| Field | Type | Description |
|-------|------|-------------|
| `template` | string | Template ID to use |
| `snapshot_id` | string | Optional rootfs snapshot ID used to initialize the writable root filesystem |
| `config` | object | Optional sandbox configuration |

### Sandbox Configuration

| Field | Type | Description |
|-------|------|-------------|
| `env_vars` | object | Sandbox-level default environment variables for new procd-managed processes |
| `ttl` | integer | Time to live in seconds (soft limit, triggers auto-pause; default: `0`, disabled) |
| `hard_ttl` | integer | Hard sandbox lifetime in seconds (deletes identity and durable state; default: `0`, disabled) |
| `resources` | object | Optional per-sandbox resource override. Only `resources.memory` is accepted; CPU is derived from the platform memory-per-CPU ratio with a `150m` minimum limit. |
| `network` | object | `SandboxNetworkPolicy`. Controls traffic rules, protocol controls, credential bindings, and destination-scoped egress auth |
| `webhook` | object | Webhook configuration |
| `auto_resume` | boolean | Auto-resume when accessed (default: true) |
| `services` | array | Sandbox Services for public HTTP routes, including Sandbox Functions |

Team quota failures return `429` with `error.code` set to `quota_exceeded`. The `sandbox_claims` policy controls sustained claim rate and immediate burst and includes a `Retry-After` header when exhausted; `active_sandboxes` controls the team's running sandbox capacity.

See [Network](/docs/sandbox/network) and [Protocol Controls](/docs/sandbox/protocol-controls) for outbound control, [Sandbox Services](/docs/sandbox/services) for public HTTP controls, [Sandbox Functions](/docs/sandbox/functions) for inline public handlers, and [Credential](/docs/sandbox/credential) for outbound auth and secret handling.

Sandbox `env_vars` override template/image environment variables for new contexts, supervised session attempts, command services, and function executions. Per-context, per-session, per-command, service runtime, and function `env_vars` override sandbox `env_vars` for that narrower scope. The resource-neutral carrier has no prestarted guest process; claim-time variables are available when procd starts.

### Sandbox Resources

Set `config.resources.memory` at claim time when one sandbox needs a different memory limit from its template. The minimum is `128Mi`. The platform maximum defaults to `16Gi` and is configured with manager's `sandbox_max_memory`. Sandbox0 enforces that maximum on template defaults and on sandbox claim, resume, and fork operations. Changing the resource lease of an existing sandbox is not exposed through the generic update endpoint.

The request only accepts memory. Manager derives CPU from
`team_template_memory_per_cpu` and applies the platform minimum. PostgreSQL
records the exact resource lease and ctld enforces it in the sandbox cgroup;
Nomad carrier resources are overhead only. To change memory, claim another
sandbox with the required resource override; the Update Sandbox API does not
change an existing sandbox's resource lease.

Claim-time helpers expose this as `WithSandboxMemory` in Go, `memory` in Python,
`memory` in TypeScript, and `--memory` in the `s0` CLI.

### TTL vs Hard TTL

Sandbox0 uses two-tier TTL to balance resource efficiency and flexibility:

| Field | Behavior | Use Case |
|-------|----------|----------|
| `ttl` | **Runtime soft pause**: When expired, Sandbox0 checkpoints the writable root filesystem, releases the runtime allocation, and marks the sandbox `paused`. | Keep sandboxes alive during active use while freeing compute resources during idle periods. |
| `hard_ttl` | **Sandbox hard delete**: When expired, Sandbox0 deletes the sandbox identity and durable state, including paused rootfs checkpoints. | Enforce a maximum lifetime for compute and storage resources. |

<Callout variant="info">
The relationship: <code>ttl {'<='} hard_ttl</code>. When <code>ttl</code> expires first, sandbox pauses but can be resumed.
When <code>hard_ttl</code> expires, the sandbox is deleted and later access returns not found. Resume starts a new runtime generation from the latest rootfs checkpoint only while the sandbox is paused and still within its hard TTL.
</Callout>

When an expiration path is disabled or unset, Sandbox responses return `null`
for the corresponding `expires_at` or `hard_expires_at` field. A disabled
expiration is never represented as a sentinel date.

**Example timeline (sandbox created with `ttl=300` and `hard_ttl=3600`):**
- `t=0`: Sandbox created
- `t=300`: TTL expires → sandbox auto-pauses
- `t=310`: User calls refresh → TTL reset to 300, Hard TTL reset to 3600 (new hard deadline at `t=3910`)
- `t=610`: TTL expires again → sandbox auto-pauses
- `t=3910`: Hard TTL expires → sandbox identity and durable state are deleted

<Tabs
  tabs={[
    {
      label: "Go",
      language: "go",
      code: `ctx := context.Background()
client, err := sandbox0.NewClient(
    sandbox0.WithToken(os.Getenv("SANDBOX0_TOKEN")),
    sandbox0.WithBaseURL(os.Getenv("SANDBOX0_BASE_URL")),
)
if err != nil {
    log.Fatal(err)
}

// Claim a sandbox from the "default" template
sandbox, err := client.ClaimSandbox(ctx, "default",
    sandbox0.WithSandboxHardTTL(300),
    sandbox0.WithSandboxMemory("512Mi"),
    sandbox0.WithSandboxEnvVars(map[string]string{
        "APP_ENV": "development",
    }),
)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Sandbox ID: %s\\n", sandbox.ID)
defer client.DeleteSandbox(ctx, sandbox.ID)`
    },
    {
      label: "Python",
      language: "python",
      code: `import os
from sandbox0 import Client
from sandbox0.apispec.models.sandbox_config import SandboxConfig

client = Client(
    token=os.environ["SANDBOX0_TOKEN"],
    base_url=os.environ.get("SANDBOX0_BASE_URL", "https://api.sandbox0.ai"),
)

# Claim a sandbox from the "default" template
sandbox = client.claim_sandbox(
    template="default",
    config=SandboxConfig.from_dict({
        "hard_ttl": 300,
        "env_vars": {"APP_ENV": "development"},
    }),
    memory="512Mi",
)
print(f"Sandbox ID: {sandbox.id}")

# Cleanup
client.delete_sandbox(sandbox.id)`
    },
    {
      label: "TypeScript",
      language: "typescript",
      code: `import { Client } from 'sandbox0';

const client = new Client({
    token: process.env.SANDBOX0_TOKEN!,
    baseUrl: process.env.SANDBOX0_BASE_URL || 'https://api.sandbox0.ai',
});

// Claim a sandbox from the "default" template
const sandbox = await client.sandboxes.claim('default', {
    hardTtl: 300,
    memory: '512Mi',
    envVars: { APP_ENV: 'development' },
});
console.log('Sandbox ID:', sandbox.id);

// Cleanup
await client.sandboxes.delete(sandbox.id);`
    },
    {
      label: "CLI",
      language: "bash",
      code: `# Claim a sandbox from the "default" template
s0 sandbox create --template default --hard-ttl 300 --memory 512Mi`
    }
  ]}
/>

---

## Get Sandbox Details

Retrieve full details about a sandbox.

<Endpoint method="GET">
/api/v1/sandboxes/{'{id}'}
</Endpoint>

<Tabs
  tabs={[
    {
      label: "Go",
      language: "go",
      code: `sb, err := client.GetSandbox(ctx, sandbox.ID)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Status: %s\\n", sb.Status)
fmt.Printf("Template: %s\\n", sb.TemplateID)
fmt.Printf("Expires at: %s\\n", sb.ExpiresAt)`
    },
    {
      label: "Python",
      language: "python",
      code: `sb = client.get_sandbox(sandbox.id)
print(f"Status: {sb.status}")
print(f"Template: {sb.template_id}")
print(f"Expires at: {sb.expires_at}")`
    },
    {
      label: "TypeScript",
      language: "typescript",
      code: `const sb = await client.sandboxes.get(sandbox.id);
console.log('Status:', sb.status);
console.log('Template:', sb.templateId);
console.log('Expires at:', sb.expiresAt);`
    },
    {
      label: "CLI",
      language: "bash",
      code: `s0 sandbox get sb_abc123`
    }
  ]}
/>

---

## Get Sandbox Status

Get the current status of a sandbox (lighter weight than full details).

<Endpoint method="GET">
/api/v1/sandboxes/{'{id}'}/status
</Endpoint>

<Tabs
  tabs={[
    {
      label: "Go",
      language: "go",
      code: `status, err := client.StatusSandbox(ctx, sandbox.ID)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Status: %s\\n", status.Status.Value)`
    },
    {
      label: "Python",
      language: "python",
      code: `status = client.sandboxes.status(sandbox.id)
print(f"Status: {status.status}")`
    },
    {
      label: "TypeScript",
      language: "typescript",
      code: `const status = await client.sandboxes.status(sandbox.id);
console.log('Status:', status.status);`
    },
    {
      label: "CLI",
      language: "bash",
      code: `s0 sandbox status sb_abc123`
    }
  ]}
/>

---

## Observability

Sandbox0 exposes runtime metrics, historical logs, and signed audit events without waking a paused sandbox.

| Data | Use it for | Endpoint |
|------|------------|----------|
| Runtime metrics | Chart-ready CPU, memory, network, process, and rootfs series | <code>GET /api/v1/sandboxes/{'{id}'}/metrics</code> |
| Historical logs | Retained stdout, stderr, and PTY output | <code>GET /api/v1/sandboxes/{'{id}'}/observability/logs</code> |
| Audit events | Canonical signed activity records | <code>GET /api/v1/sandboxes/{'{id}'}/observability/events</code> |

Audit ingestion and audit queries require the enterprise `sandbox_audit` feature. Logs, runtime metrics, and the metric catalog do not.

See [Observability](/docs/sandbox/observability) for metric names, query filters, watch streams, audit integrity, delivery modes, and coverage limits.

---

## List Sandboxes

List all sandboxes with optional filters.

<Endpoint method="GET">
/api/v1/sandboxes
</Endpoint>

### Query Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `status` | string | Filter by status (`starting`, `running`, `paused`, `terminating`, `failed`) |
| `template_id` | string | Filter by template ID |
| `paused` | boolean | Filter by paused state independently of `status` |
| `limit` | integer | Max results per page (default: 50, max: 200) |
| `offset` | integer | Pagination offset (default: 0) |

<Tabs
  tabs={[
    {
      label: "Go",
      language: "go",
      code: `limit := 10
sandboxes, err := client.ListSandboxes(ctx, &sandbox0.ListSandboxesOptions{
    Status:     "running",
    TemplateID: "default",
    Limit:      &limit,
})
if err != nil {
    log.Fatal(err)
}
for _, sb := range sandboxes.Sandboxes {
    fmt.Printf("- %s (status: %s)\\n", sb.ID, sb.Status)
}`
    },
    {
      label: "Python",
      language: "python",
      code: `sandboxes = client.sandboxes.list(
    status="running",
    template_id="default",
    limit=10,
)
for sb in sandboxes:
    print(f"- {sb.id} (status: {sb.status})")`
    },
    {
      label: "TypeScript",
      language: "typescript",
      code: `const { sandboxes } = await client.sandboxes.list({
    status: 'running',
    templateId: 'default',
    limit: 10,
});
for (const sb of sandboxes) {
    console.log(\`- \${sb.id} (status: \${sb.status})\`);
}`
    },
    {
      label: "CLI",
      language: "bash",
      code: `s0 sandbox list --status running --template-id default`
    }
  ]}
/>

---

## Update Sandbox

Update durable lifecycle or service configuration without replacing the runtime allocation.

<Endpoint method="PUT">
/api/v1/sandboxes/{'{id}'}
</Endpoint>

### Updatable Fields

Only the following fields can be updated at runtime:

| Field | Type | Description |
|-------|------|-------------|
| `ttl` | integer | Time to live in seconds (soft limit) |
| `hard_ttl` | integer | Hard sandbox lifetime in seconds |
| `auto_resume` | boolean | Auto-resume when accessed |
| `services` | array | Sandbox Services for public HTTP routes, including Sandbox Functions |

<Callout variant="warning">
Use `PUT /api/v1/sandboxes/{'{id}'}/network` for network policy changes.
Environment, resource, and webhook changes require a new runtime and are not
accepted by this endpoint.
</Callout>

```bash
curl -X PUT "$SANDBOX0_API_URL/api/v1/sandboxes/$SANDBOX_ID" \
    -H "Authorization: Bearer $SANDBOX0_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"config":{"ttl":600,"hard_ttl":3600,"auto_resume":true}}'
```

---

## Pause And Resume

Pause and resume is covered in a dedicated page because it affects TTL, `auto_resume`, service routes, SSH, and webhook behavior.

See [Pause And Resume](/docs/sandbox/pause-resume) for explicit pause, state inspection, resume, and auto-resume behavior.

See [Snapshot And Restore](/docs/sandbox/snapshot-restore) for named rootfs snapshots, restore, and fork operations. Snapshot and fork accept a running or paused source sandbox; restore requires a paused target sandbox.

---

## Refresh Sandbox TTL

Extend the sandbox time-to-live. This resets both `ttl` and `hard_ttl` (if configured) from the current time while the sandbox has a runtime.

<Endpoint method="POST">
/api/v1/sandboxes/{'{id}'}/refresh
</Endpoint>

### Request Body

| Field | Type | Description |
|-------|------|-------------|
| `duration` | integer | Duration to extend TTL in seconds (optional, defaults to original TTL) |

<Callout variant="info">
If <code>duration</code> is not specified, both <code>ttl</code> and <code>hard_ttl</code> are reset to their original configured values. Use this to keep a sandbox alive as long as the user is actively using it.
</Callout>

<Tabs
  tabs={[
    {
      label: "Go",
      language: "go",
      code: `// Refresh with default duration (original TTL)
resp, err := client.RefreshSandbox(ctx, sandbox.ID, nil)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("New expires at: %s\\n", resp.ExpiresAt)

// Refresh with custom duration (e.g., 1 minute)
resp, err = client.RefreshSandbox(ctx, sandbox.ID, &apispec.SandboxRefreshRequest{
    Duration: apispec.NewOptInt32(60),
})
if err != nil {
    log.Fatal(err)
}
fmt.Printf("New expires at: %s\\n", resp.ExpiresAt)`
    },
    {
      label: "Python",
      language: "python",
      code: `from sandbox0.apispec.models.sandbox_refresh_request import SandboxRefreshRequest

# Refresh with default duration (original TTL)
resp = client.sandboxes.refresh(sandbox.id)
print(f"New expires at: {resp.expires_at}")

# Refresh with custom duration (e.g., 1 minute)
resp = client.sandboxes.refresh(sandbox.id, SandboxRefreshRequest(duration=60))
print(f"New expires at: {resp.expires_at}")`
    },
    {
      label: "TypeScript",
      language: "typescript",
      code: `// Refresh with default duration (original TTL)
const resp = await client.sandboxes.refresh(sandbox.id);
console.log('New expires at:', resp.expiresAt);

// Refresh with custom duration (e.g., 1 minute)
const resp2 = await client.sandboxes.refresh(sandbox.id, { duration: 60 });
console.log('New expires at:', resp2.expiresAt);`
    },
    {
      label: "CLI",
      language: "bash",
      code: `# Refresh with default duration
s0 sandbox refresh sb_abc123

# Output includes the new expiry timestamp`
    }
  ]}
/>

---

## Delete Sandbox

Terminate and delete a sandbox. This action is irreversible.

<Endpoint method="DELETE">
/api/v1/sandboxes/{'{id}'}
</Endpoint>

A successful response means the deletion intent is durable. Runtime teardown and persistent RootFS cleanup finish asynchronously. For Nomad-backed Sandboxes, the manager fences the exact allocation and its writer authority before deleting the logical record; it does not depend on the Nomad task-driver process remaining available.

<Tabs
  tabs={[
    {
      label: "Go",
      language: "go",
      code: `_, err = client.DeleteSandbox(ctx, sandbox.ID)
if err != nil {
    log.Fatal(err)
}
fmt.Println("Sandbox deleted")`
    },
    {
      label: "Python",
      language: "python",
      code: `client.sandboxes.delete(sandbox.id)
print("Sandbox deleted")`
    },
    {
      label: "TypeScript",
      language: "typescript",
      code: `await client.sandboxes.delete(sandbox.id);
console.log('Sandbox deleted');`
    },
    {
      label: "CLI",
      language: "bash",
      code: `s0 sandbox delete sb_abc123`
    }
  ]}
/>

---

## Next Steps

<CardGroup>
  <Card title="Pause And Resume" href="/docs/sandbox/pause-resume" cta="Continue">
    Control checkpointed pause and resume behavior before wiring long-lived workflows.
  </Card>

  <Card title="Snapshot And Restore" href="/docs/sandbox/snapshot-restore" cta="Continue">
    Create named rootfs snapshots, restore paused sandboxes, and fork sandbox state.
  </Card>

  <Card title="Contexts" href="/docs/sandbox/contexts" cta="Continue">
    Run REPL and command contexts inside a sandbox and stream process output.
  </Card>

  <Card title="Supervised Sessions" href="/docs/sandbox/session-supervisor" cta="Continue">
    Run reconnect-safe processes with stable identities, attempts, and retained events.
  </Card>
</CardGroup>
