# Sandboxes

> Isolated Linux machines to run code, commands and services, billed per second

A sandbox is an isolated Linux (Ubuntu) machine you start through the API or the dashboard, use, and stop when you are done. Use one to run code an agent wrote, build a project, preview a web app, or drive a browser for a few minutes.

The machine is private to that sandbox. It has no access to your Zavu account, to your other sandboxes, or to any secret you did not give it.

> **Note:**
> Sandboxes need a paid plan. On Free, creating or resuming one answers `402 paid_plan_required`. They also need a live (`zv_live_`) API key: test-mode keys can read, stop and delete sandboxes, but not start them or run commands.

  - [Quickstart](https://www.zavu.dev/docs/guides/sandboxes/quickstart): Start a sandbox, run code in it and stop it.
  - [Commands and files](https://www.zavu.dev/docs/guides/sandboxes/commands-and-files): Run shell commands, in the foreground or the background, and move files.
  - [Ports, SSH and the desktop](https://www.zavu.dev/docs/guides/sandboxes/access): Reach a service on a zavu.run URL, connect over SSH, or open its screen.
  - [Snapshots and environments](https://www.zavu.dev/docs/guides/sandboxes/snapshots-and-environments): Copy a sandbox, save its disk, share it, and start with saved variables.

## Pricing

Billed from your prepaid balance:

| Size | Machine | Per hour |
|------|---------|----------|
| `small` | 2 vCPU, 4 GB RAM | $0.0225 |
| `default` | 4 vCPU, 8 GB RAM | $0.045 |
| `large` | 8 vCPU, 16 GB RAM | $0.09 |

- **$0.01 per start**, each time a sandbox is created, resumed or copied. A sandbox that fails before it ever runs gets it back.
- **Machine time per second** while the sandbox is `starting` or `running`, with a one-minute minimum per run.
- **A stopped sandbox costs nothing.** Its disk is kept 14 days.
- **A saved snapshot** costs $0.07 per started UTC day while you keep it.

Machine time counts against your monthly spend cap, and appears as **Sandboxes** in your usage.

## Lifecycle

```
starting ──▶ running ──▶ stopping ──▶ stopped ──▶ (resume) ──▶ starting
    │            │                        │
    └──▶ failed ◀┘                        └──▶ 14 days later: disk deleted
```

| Status | Meaning |
|--------|---------|
| `starting` | Accepted. Commands and file calls made now wait for it to be ready. |
| `running` | Up, and billed. |
| `stopping` | A stop was requested. Billing has already ended. |
| `stopped` | Disk kept until `expiresAt`. Resume it to continue where you left off. |
| `failed` | Could not start, or failed on its host. See `failureReason`. |

### When it stops

A running sandbox is stopped for you when:

- its `timeoutSeconds` passes (default 1 hour, max 24 hours; checked every minute),
- your balance, or what is left of your monthly spend cap, no longer covers 15 more minutes of it,
- your team leaves its paid plan.

Send `"timeoutSeconds": null` on create or resume to turn the first one off: the sandbox runs until you stop it, and the other two still apply. It reads back as `null`, and it is billed for every second it runs, so stop it when you are done.

### Ephemeral sandboxes

Send `"ephemeral": true` on create for a sandbox whose disk is never saved. Stopping it erases the disk: it reads back as `stopped` with `expiresAt` equal to `stoppedAt`, and resume answers `409 sandbox_expired`. It cannot be copied or saved as a snapshot. Read what you need from it before you stop it.

## Webhooks

Set up the [project webhook](https://www.zavu.dev/docs/guides/receiving-messages/webhooks#project-webhooks) and subscribe to these events to know when a sandbox changes state without polling. They matter most for the stops you did not ask for: a timeout, a balance or spend cap that runs out, or the team leaving its paid plan.

| Event | When |
|-------|------|
| `sandbox.running` | It finished starting, after a create, resume or copy. Machine time is billed from the start. |
| `sandbox.stopped` | It is down and reads `stopped`, so it can be resumed. `stopReason` says why. |
| `sandbox.failed` | It could not start, or failed on its host. See `failureReason`. |
| `sandbox_snapshot.ready` | A saved snapshot can be started from. |
| `sandbox_snapshot.failed` | A snapshot could not be saved. Its first day is refunded. |

`data.sandbox` (or `data.snapshot`) is the object `GET` returns, read after the change, so you do not need a second call. `stopReason` is `requested` (you stopped it), `timeout`, `insufficient_balance`, `spend_cap_reached`, `plan` or `provider`, and `GET /v1/sandboxes/{sandboxId}` returns it too.

```typescript
await zavu.invitations.webhook.configure({
  url: "https://your-app.com/webhooks/zavu",
  events: ["sandbox.stopped", "sandbox.failed"],
});
```

```bash
curl -X POST https://api.zavu.dev/v1/invitations/webhook \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://your-app.com/webhooks/zavu", "events": ["sandbox.stopped", "sandbox.failed"]}'
```

This replaces the events the webhook had: send every event you want to keep. A failed resume is not announced, because the sandbox stays `stopped` and the call that resumed it already returned the error. Deliveries can repeat or arrive out of order: deduplicate by the event `id` and trust `status` over arrival order.

## In the dashboard

**Agents › Compute sandboxes** does everything in these guides on screen: start, stop, resume, copy and delete sandboxes, run commands, expose ports, open the desktop or SSH, and keep saved snapshots, share codes and environments. Anything you do there, you can do through the API, and the other way round.

## Limits

| | Paid plan |
|---|---|
| Sandboxes starting or running at once, per team | 10 (`403 plan_limit_reached` past it) |
| Starts per day, creates, resumes and copies together | 200 (`429 daily_limit_exceeded`, resets 00:00 UTC) |
| `timeoutSeconds` | 60 to 86,400, or `null` for no auto-stop |
| File size through the API | 5 MiB |
| Synchronous command | 120 s |
| Exposed ports per sandbox | 20 |
| Saved snapshots per team | 20 |
| Environments per project | 50 |

## Errors

| Status | Code | Meaning |
|--------|------|---------|
| `402` | `paid_plan_required` | The team is on Free. |
| `402` | `insufficient_balance` | The balance does not cover the $0.01 start and 15 minutes of machine time. |
| `402` | `spend_cap_reached` | The monthly spend cap does not leave that much room. |
| `404` | `not_found` | The sandbox, snapshot or environment is not in this project, or a share code was revoked. Nothing is charged. |
| `409` | `sandbox_not_running` | Commands, files, ports, SSH and the desktop need a starting or running sandbox. |
| `409` | `sandbox_not_ready` | Still starting, or the desktop stream is being prepared. Retry in a few seconds. |
| `409` | `port_limit_reached` | The sandbox already exposes 20 ports. |
| `409` | `sandbox_not_stopped` | Only a stopped sandbox can be resumed. |
| `409` | `sandbox_expired` | Its disk was deleted: 14 days after it stopped, or at the stop for an ephemeral sandbox. Create a new one. |
| `409` | `sandbox_ephemeral` | An ephemeral sandbox cannot be copied or saved. |
| `503` | `sandbox_unavailable` | No capacity right now. Nothing was charged for machine time; retry. |

Every field is in the Sandboxes section of the API reference, starting at [Create a sandbox](https://www.zavu.dev/docs/api-reference/sandboxes/create-a-sandbox).
