# Metalboxes
URL: https://docs.munito.ai/sandboxes

<Callout type="idea">
  Metalboxes is in preview. [Talk to an engineer](https://munito.ai/contact) to turn it on for your organization. The
  API, the limits and the [prices](https://munito.ai/sandboxes) can change before general availability.
</Callout>

A sandbox is a Linux microVM for one program. Your code creates it with the API, runs commands in it, reads and writes its files, and deletes it. Each sandbox has its own kernel, disk and network, so code in one sandbox cannot reach another sandbox.

Use a sandbox for code that you do not trust, such as code that an AI agent writes, and for a test or a build that must start clean.

|              | Value                                                                                                                                                             |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Base URL     | `https://api.munito.ai/sandboxes/v1`                                                                                                                              |
| Key variable | `MUNITO_API_KEY`                                                                                                                                                  |
| Templates    | `base`: Debian, Python 3.12, Node 24, git, curl and build tools. `ubuntu`: Ubuntu 24.04 with Docker, Claude Code, Codex and OpenCode. See [Templates](#templates) |
| Work folder  | `/workspace`, where each command starts unless you set `cwd`                                                                                                      |

## Quick start [#quick-start]

1. Open [API keys in the console](https://console.munito.ai/keys).
2. Create a key with the **Inference** permission.
3. Run each step below in a terminal. Each step is one request with curl.

### Create a sandbox [#create-a-sandbox]

```bash
export MUNITO_API_KEY="munitoai_..."

curl -sS --fail-with-body https://api.munito.ai/sandboxes/v1/sandboxes \
  -H "Authorization: Bearer $MUNITO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"size": "small", "timeout": 600}'
```

The answer is the sandbox:

```json
{
  "id": "sbx_01k8f6x7g3d2b9v5n4m1q0w8e6",
  "object": "sandbox",
  "status": "running",
  "template": "base",
  "size": "small",
  "vcpus": 1,
  "memory_mib": 1024,
  "disk_gib": 10,
  "reserved": false,
  "internet": true,
  "persistent": false,
  "metadata": {},
  "created_at": 1793016000,
  "started_at": 1793016000,
  "expires_at": 1793016600,
  "ended_at": null,
  "end_reason": null,
  "command": null
}
```

Keep its `id` for the next steps:

```bash
export SANDBOX="sbx_01k8f6x7g3d2b9v5n4m1q0w8e6"
```

### Run a command [#run-a-command]

```bash
curl -sS --fail-with-body "https://api.munito.ai/sandboxes/v1/sandboxes/$SANDBOX/exec" \
  -H "Authorization: Bearer $MUNITO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"command": "python3 -c \"print(6 * 7)\""}'
```

```json
{ "exit_code": 0, "stdout": "42\n", "stderr": "", "duration_ms": 35, "timed_out": false, "truncated": false }
```

A string runs in `bash -lc`. A list, such as `["python3", "-V"]`, is the argument vector itself. Set `"stream": true` to get NDJSON events while the command writes: `stdout` and `stderr` events, then one `exit` event.

To start a sandbox and run one command in one request, set `command` in the create. The sandbox runs it in `/workspace` with its `env`, for at most 60 seconds, and the answer has the result in `command`:

```bash
curl -sS --fail-with-body https://api.munito.ai/sandboxes/v1/sandboxes \
  -H "Authorization: Bearer $MUNITO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"size": "small", "command": "python3 -c \"print(6 * 7)\""}'
```

`command` is null when the command could not run, and for a retry with the same `Idempotency-Key`. The sandbox runs in each case, until its timeout or a delete.

### Write, read and list files [#write-read-and-list-files]

```bash
curl -sS --fail-with-body -X PUT \
  "https://api.munito.ai/sandboxes/v1/sandboxes/$SANDBOX/files/content?path=/workspace/hello.py" \
  -H "Authorization: Bearer $MUNITO_API_KEY" \
  -H "Content-Type: application/octet-stream" \
  --data-binary 'print("Hello from the sandbox")'

curl -sS --fail-with-body \
  "https://api.munito.ai/sandboxes/v1/sandboxes/$SANDBOX/files/content?path=/workspace/hello.py" \
  -H "Authorization: Bearer $MUNITO_API_KEY"

curl -sS --fail-with-body \
  "https://api.munito.ai/sandboxes/v1/sandboxes/$SANDBOX/files?path=/workspace" \
  -H "Authorization: Bearer $MUNITO_API_KEY"
```

A write creates the parent folders, and replaces a file that exists.

### Delete the sandbox [#delete-the-sandbox]

```bash
curl -sS --fail-with-body -X DELETE "https://api.munito.ai/sandboxes/v1/sandboxes/$SANDBOX" \
  -H "Authorization: Bearer $MUNITO_API_KEY"
```

The sandbox stops at once, and its files are deleted. You can still read the sandbox: its `status` is `stopped` and its `end_reason` is `deleted`.

## Templates [#templates]

Set `template` in the create. A template is the root file system that each new sandbox of it starts with.

| Template | System                                                 | Tools                                                                                                             |
| -------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
| `base`   | Debian 12. The template of a create without `template` | Python 3.12, Node 24, git, curl, jq and build tools                                                               |
| `ubuntu` | Ubuntu 24.04 with systemd                              | Docker with Compose and Buildx, Python 3.12, Node 24, git, curl, jq, build tools, Claude Code, Codex and OpenCode |

`pip install` installs into the system Python in both templates.

### Docker [#docker]

In an `ubuntu` sandbox, Docker runs when the sandbox starts:

```bash
curl -sS --fail-with-body https://api.munito.ai/sandboxes/v1/sandboxes \
  -H "Authorization: Bearer $MUNITO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"template": "ubuntu", "size": "large", "command": "docker run --rm hello-world"}'
```

Use the `default` size or a larger one, because images take memory and disk. Docker Hub limits the image pulls of a user that is not logged in, so run `docker login` in the sandbox when you pull many images.

The `base` template has no Docker. Install it and start its daemon in the sandbox, one command at a time:

```bash
apt-get update && apt-get install -y docker.io
setsid dockerd --iptables=false --bridge=none >/var/log/dockerd.log 2>&1 < /dev/null &
docker run --rm --network none hello-world
```

The daemon keeps running after its command ends, so later commands can use it. These containers have no bridge network: use `--network host` for internet access, or `--network none`.

### Agent harnesses [#agent-harnesses]

Claude Code (`claude`), Codex (`codex`) and OpenCode (`opencode`) are installed in `ubuntu`. Give each one its key in `env`, or use them with the open models of Munito AI: see [Claude Code](https://docs.munito.ai/claude-code), [Codex](https://docs.munito.ai/codex) and [OpenCode](https://docs.munito.ai/opencode).

## Sizes and prices [#sizes-and-prices]

A size has a ceiling and a floor. The ceiling is the vCPUs and the memory that the sandbox sees. The floor is the smallest share that the sandbox pays for. Each second, a sandbox pays for the higher of its floor and its real use, for the CPU and for the memory.

| Preset    | Ceiling       | Floor              | Idle, per hour | Busy at the ceiling, per hour |
| --------- | ------------- | ------------------ | -------------- | ----------------------------- |
| `small`   | 1 vCPU, 1 GiB | 0.25 vCPU, 256 MiB | $0.0072        | $0.0288                       |
| `default` | 2 vCPU, 4 GiB | 0.5 vCPU, 1 GiB    | $0.0162        | $0.0648                       |
| `large`   | 4 vCPU, 8 GiB | 1 vCPU, 2 GiB      | $0.0324        | $0.1296                       |

| Resource | Price                 |
| -------- | --------------------- |
| CPU      | $0.0252 per vCPU-hour |
| Memory   | $0.0036 per GiB-hour  |

- A sandbox without `size` gets the `default` size.
- For another size, set `vcpus` and `memory_mib` in place of `size`. The floor of a custom size is a quarter of its ceiling.
- A preset starts from a snapshot of its template in less than a second. A custom size boots its template, which takes some seconds.
- For a larger root disk, set `disk_gib`, such as `{"size": "large", "disk_gib": 40}`. The root file system grows to that size when the sandbox starts.
- Set `"reserved": true` to pay for the ceiling all the time. The floor is then the ceiling.
- The Metalboxes page of the console shows the vCPU-hours, the GiB-hours and the spend of the month.

## Limits [#limits]

| Limit               | Value                                                                                                               |
| ------------------- | ------------------------------------------------------------------------------------------------------------------- |
| vCPUs               | 1 to 16                                                                                                             |
| Memory              | 512 to 32,768 MiB, in steps of 256 MiB, and at least 512 MiB for each vCPU                                          |
| Disk                | 10 to 100 GiB, set with `disk_gib`. The default is 10                                                               |
| Life of a sandbox   | 10 to 86,400 seconds. The default is 3,600, one hour                                                                |
| Time of a command   | 1 to 1,800 seconds. The default is 60                                                                               |
| Output of a command | 1 MiB for each stream when you wait for the result. `truncated` is true when output is cut                          |
| File                | 100 MiB                                                                                                             |
| Environment         | 100 variables. A value is at most 32 KiB                                                                            |
| Metadata            | 16 keys. A key is at most 64 characters, and a value at most 512                                                    |
| Running sandboxes   | 50 at once for each organization. Above it, a create gets 429 `sandbox_limit_reached`. Talk to an engineer for more |
| Creates             | No limit on their number. API requests from one IP address are limited to 8,000 a minute                            |

A sandbox has outbound internet access only. It cannot reach your other sandboxes or private addresses, and it cannot send mail over SMTP. Set `"internet": false` for a sandbox with no network. Inbound connections are not available yet.

## Lifecycle and timeouts [#lifecycle-and-timeouts]

| Status    | Meaning                                                                   |
| --------- | ------------------------------------------------------------------------- |
| `pending` | The sandbox starts, or a persistent sandbox resumes.                      |
| `running` | The sandbox runs commands.                                                |
| `paused`  | A persistent sandbox keeps its files and its processes, and runs nothing. |
| `stopped` | The sandbox ended.                                                        |
| `failed`  | The sandbox did not start, or an error stopped it.                        |

A sandbox stops at the first of these events:

- Its timeout ends. `expires_at` gives the time, and `end_reason` is `timeout`.
- You delete it. `end_reason` is `deleted`.
- An error stops it. `end_reason` is `error`.

The timeout counts from the create, or from the last change. To keep a sandbox longer, send a new `timeout` in seconds to `POST /sandboxes/{sandbox_id}/timeout`. A command does not change the timeout.

When a sandbox stops, its processes and its files are gone. The API keeps its record, so `GET /sandboxes/{sandbox_id}` and `GET /sandboxes` still show it.

## Persistent sandboxes [#persistent-sandboxes]

Create a sandbox with `"persistent": true` to keep its files and its processes across a pause. A pause saves the whole sandbox, its memory and its disk, and stops its machine. A resume starts it again where it was: the same files, the same processes, and the same data in memory. A program that took a minute to load its state answers at once after a resume.

```bash
curl -sS --fail-with-body https://api.munito.ai/sandboxes/v1/sandboxes \
  -H "Authorization: Bearer $MUNITO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"size": "default", "persistent": true}'

curl -sS --fail-with-body -X POST "https://api.munito.ai/sandboxes/v1/sandboxes/$SANDBOX/pause" \
  -H "Authorization: Bearer $MUNITO_API_KEY"

curl -sS --fail-with-body "https://api.munito.ai/sandboxes/v1/sandboxes/$SANDBOX/resume" \
  -H "Authorization: Bearer $MUNITO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"timeout": 3600}'
```

- The timeout of a persistent sandbox pauses it, and does not stop it. A resume sets a new timeout.
- A paused sandbox pays no machine time. During the preview, the storage of its saved state is free.
- Its open network connections do not survive a pause. A program that serves or downloads opens them again.
- A delete of a paused sandbox removes its saved state.

A fork starts new sandboxes from a persistent sandbox, as it is now. A running sandbox stops for a moment and then runs on. A paused one stays paused. Each new sandbox is persistent unless you set `"persistent": false`, and each one is billed on its own:

```bash
curl -sS --fail-with-body "https://api.munito.ai/sandboxes/v1/sandboxes/$SANDBOX/fork" \
  -H "Authorization: Bearer $MUNITO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"count": 4, "timeout": 600}'
```

The answer lists the new sandboxes. They all start, or none does. A fork starts at most 16 sandboxes.

A persistent sandbox and its forks keep the random numbers that their programs held in memory, as a copy of a machine does. The kernel of each one gets fresh random data when it starts, so a program that reads `/dev/urandom` after the start gets its own numbers.

## API reference [#api-reference]

An organization with Metalboxes on sees the reference page of each operation in the console docs.

| Operation        | Request                                     |
| ---------------- | ------------------------------------------- |
| Create a sandbox | `POST /sandboxes`                           |
| List sandboxes   | `GET /sandboxes`                            |
| Get a sandbox    | `GET /sandboxes/{sandbox_id}`               |
| Delete a sandbox | `DELETE /sandboxes/{sandbox_id}`            |
| Set the timeout  | `POST /sandboxes/{sandbox_id}/timeout`      |
| Pause a sandbox  | `POST /sandboxes/{sandbox_id}/pause`        |
| Resume a sandbox | `POST /sandboxes/{sandbox_id}/resume`       |
| Fork a sandbox   | `POST /sandboxes/{sandbox_id}/fork`         |
| Run a command    | `POST /sandboxes/{sandbox_id}/exec`         |
| List files       | `GET /sandboxes/{sandbox_id}/files`         |
| Read a file      | `GET /sandboxes/{sandbox_id}/files/content` |
| Write a file     | `PUT /sandboxes/{sandbox_id}/files/content` |
| Delete a file    | `DELETE /sandboxes/{sandbox_id}/files`      |
