Sandboxes
A sandbox is an isolated machine for an agent or any other program, run by its provider. It runs your image, or Sandbox Router's Default one, with the command, variables and size you give it. It stops by itself when idle, reaches only the hosts you allow, gets your connectors' keys added to its requests without ever holding them, serves its ports at its provider's own URLs, and takes snapshots on providers that keep them.
This guide covers a sandbox's whole life in the console, the CLI and the API. The CLI examples assume you've installed and configured sandboxrouter (see The CLI); the API examples send an API key as $SANDBOXROUTER_API_KEY (see The API).
Create a sandbox
In the console
Open Sandboxes and choose New sandbox. (The button shows for a workspace's owners and operators.)
Under 1. Choose a provider, keep Auto to let Sandbox Router pick where the sandbox runs, or pick a provider.
Under 2. Choose an image, pick Default or Your image (see Images). Optionally enter a Command for the sandbox to run, and Environment variables, one
NAME=valueper line.Under 3. Configure, pick a Size (see Sizes) and add Labels. A name is a label like any other (
name = build-agent).Under 4. Network, list the Allowed outbound hosts, one per line, or leave the field empty to allow every host (see Outbound access). Optionally enter a User: one of your own users, whose own accounts the sandbox's connectors then use (see Connectors). Under Connectors, check each connector the sandbox gets; each says whose account its requests carry.
Choose Create sandbox.
The create holds until the sandbox runs, and its page opens.
With the CLI
sandboxrouter sandboxes create --label name=build-agent --size l --egress api.github.com --connector githubEvery field of the sandbox has its own option; see Fields. sandboxrouter run is the shortcut that creates a sandbox and runs one command in it once it runs:
sandboxrouter run --label name=build-agent -- npm testWith the API
curl https://api.sandboxrouter.io/v1/sandboxes \
-H "Authorization: Bearer $SANDBOXROUTER_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "image": "default" }'{"image": "default"} is a whole request: every other field takes its default. The request holds until the sandbox runs and answers 201 Created with it, "state": "running", or "failed" with why in stateMessage. Sandbox Router sets no deadline of its own: how long to wait is yours to decide, and a create you stop waiting for goes on. Send the same Idempotency-Key again to retry safely: you get the same sandbox, never a second one.
Fields
A sandbox is one flat object: the fields you set and the fields its provider reports side by side.
| Field | Default | Rules | CLI option | Changes |
|---|---|---|---|---|
image | "default" | "default", or your image pinned to its digest; on Maritime, which has no Default image, the image Maritime boots; see Images | --image | Can't change |
command | null: the image's own entrypoint and command, if it has any | 1 to 32 arguments of up to 512 characters | --command "CMD" | Can't change |
env | {} | Up to 64 variables; names of letters, digits and _, not starting with a digit or SANDBOXROUTER_; values up to 4,096 characters. A provider that can't take variables when it creates a sandbox refuses them | --env NAME=VALUE | Can't change |
size | "m" | See Sizes | --size | Can't change |
egress | ["*"]: every host | ["*"], or up to 32 hosts; [] reaches none | --egress HOST, --no-egress | Yes |
idleStopMinutes | 15 | 5 to 240; on Maritime always 5, as Maritime stops an idle sandbox itself after five minutes | --idle-stop | Yes |
labels | {} | Up to 16 key: value (15 with a user); keys up to 63 lowercase letters, digits, ., _ and -, not user, run or grant; values up to 256 characters | --label KEY=VALUE, --no-labels | Yes, but not on E2B, which sets them only when it creates the sandbox |
provider | "auto" | Where the sandbox runs: auto (Sandbox Router tries the providers of its size in a fixed order until one runs it), or cloudflare (Native, Sandbox Router's own account), or maritime, daytona, freestyle or e2b on your own account, with your own key, or on Sandbox Router's where it offers one | --provider | Can't change |
connectors | [] | Up to 32 of your connectors' IDs: the connectors whose accounts its requests carry (see Connectors) | --connector ID | Can't change |
user | null | One of your own users, by your ID for them: 1 to 256 characters, without control characters; its connectors then use that user's own accounts | --user | Can't change |
A sandbox runs its command at every start, in place of the image's entrypoint and command. With command null, the image's own entrypoint and command run the same way, and an image without any, such as the Default image, runs nothing until you send it commands. Each command you run in the sandbox, such as the one sandboxrouter run takes after --, runs once beside it and is never its command.
And what its provider reports:
| Field | What it is |
|---|---|
id | The sandbox's ID, u- and 52 characters: every command and path takes it |
allocation | { cpu, memoryGiB, diskGB }: what the provider gave the sandbox for its size, its nearest shape at or above it, as the provider last reported it; null until it has, and a field the provider left out null. Sandbox Router bills the size |
state | starting, running, stopping, stopped or failed |
stateMessage | Why it's stopped or failed; null while it runs |
providerId | The provider's own ID for the sandbox, null until it exists |
currentProvider | The provider it runs on |
createdAt, startedAt, stoppedAt | Milliseconds since the Unix epoch |
A sandbox with a user carries the label user with the user's ID, which you can filter by but not set.
What the provider does to a sandbox on its own shows here too. Stopped, failed or deleted in the provider's own console, it reads stopped or failed (stopped at its provider, failed at its provider) or is deleted, within 5 minutes, or at once on Daytona and E2B, where connecting your key registers the provider's webhook (on Daytona, once Daytona has turned webhooks on for your organization) and removing it removes the webhook. On Daytona, Freestyle and E2B, which keep a sandbox's labels, labels are as the provider has them, and so is user, which Sandbox Router keeps as a label there: a label you change in its own console shows here.
Images
The Default image
The Default image is Alpine Linux with bash, coreutils, Node and Docker. It runs on any size and has no command of its own: without a command, the sandbox runs and waits for the commands you send it. It doesn't include your project's dependencies or a coding agent: install what you need with a command, or build your own image. To install Alpine packages with apk, allow dl-cdn.alpinelinux.org.
Your own image
You can run any linux/amd64 image you've pushed to your workspace's namespace of the registry, registry.caas.sh/<tenant-id>/NAME; Images and the registry shows how. A sandbox on your own image needs size M or larger. Without a command, the image's own entrypoint and command run.
In the console, pick Your image and enter the image by digest,
registry.caas.sh/<tenant-id>/NAME@sha256:…(docker pushprints the digest).The CLI also takes a tag, and pins it to its digest for you:
sandboxrouter sandboxes create --image agent:1.2.0.The API takes the digest reference:
"image": "registry.caas.sh/<tenant-id>/agent@sha256:…". Sandbox Router checks that it's in your registry when you create the sandbox.
Images on Maritime
Maritime has no Default image, and it pulls only public images, so it can't run one from your registry either. A sandbox on Maritime names the image Maritime boots, as Maritime names it: template:<id>, a template of your Maritime account, or a public image pinned by digest on Docker Hub, GHCR or Quay, such as "image": "ghcr.io/acme/agent@sha256:…". It runs on any size Maritime takes. A create on Maritime with no image, or "default", is refused with 400 and says so. With auto, a sandbox on the Default image or your own goes to another provider, and one on a Maritime image goes to Maritime.
Sizes
A size sets the vCPU, memory and disk the sandbox gets:
| Size | vCPU | Memory | Disk |
|---|---|---|---|
| XS | 1/16 | 256 MiB | 2 GB |
| S | 1/4 | 1 GiB | 4 GB |
| M (default) | 1/2 | 4 GiB | 8 GB |
| L | 1 | 6 GiB | 12 GB |
| XL | 2 | 8 GiB | 16 GB |
| 2XL | 4 | 12 GiB | 20 GB |
While it runs on Native, a sandbox bills for its vCPU, memory and disk, whether it's busy or waiting. On your own Daytona, Freestyle, Maritime or E2B account, that provider bills its compute and egress instead. See pricing for the rates. A sandbox's running time is on the Usage page, not in the sandbox.
Change a sandbox
A sandbox's outbound hosts, idle stop and labels can change; everything else is its create's for good. In the console, the sandbox's Network card has an Edit link for its outbound hosts (its connectors are listed there, read-only), and its Labels card changes its labels. With the CLI, sandboxrouter sandboxes update replaces each field you give, and a --no-… flag empties one:
sandboxrouter sandboxes update <sandbox> --egress api.github.com --egress pypi.org --idle-stop 60
sandboxrouter sandboxes update <sandbox> --label team=webWith the API, PATCH /v1/sandboxes/<sandbox> takes the fields to replace: egress, idleStopMinutes and labels. A sandbox's labels are yours on every provider; providerLabels shows what its provider holds. E2B sets a sandbox's labels only when it creates it, so an E2B sandbox's providerLabels stay as its create set them.
Start, stop and restart
Stop a sandbox with Stop on its page or in its row's menu in Sandboxes, sandboxrouter sandboxes stop <sandbox>, or POST /v1/sandboxes/<sandbox>/stop. Start it with Start, sandboxrouter sandboxes start <sandbox>, or POST …/start. restart stops it and starts it again. Each holds until the sandbox settles and answers with it.
What a sandbox keeps across a stop is what its provider keeps. A stopped sandbox bills for no vCPU, memory or disk.
Idle stop
A running sandbox stops by itself after idleStopMinutes (15 by default) with no call in flight: a command, a file transfer, a snapshot or a request to its MCP endpoint keeps it running while it lasts. Requests to its port URLs go to its provider, not through Sandbox Router.
A stopped sandbox stays stopped
A stopped sandbox starts only when you start it: a command or a file transfer sent to it gets 409 not_running. A request to the MCP server it hosts starts it first.
A sandbox also starts only while the workspace has a card on file and a balance above $0. At $0, its sandboxes stop until funds arrive; see Billing and usage.
Run commands
A command runs one executable with its arguments, without a shell, in the sandbox's working directory, with its variables and the ones you add. To use shell features, run a shell: sh -c 'npm ci && npm test'.
In the console
On the sandbox's page, Run a command takes a Command and a Time limit. Quote arguments that contain spaces. The result shows the command's state, exit code and output, with Refresh and, while it runs, Cancel.
With the CLI
sandboxrouter sandboxes exec <sandbox> -- node --version
sandboxrouter sandboxes exec <sandbox> --env NODE_ENV=test --workdir /workspace/app --timeout 1800 -- npm test
echo '{"q": 1}' | sandboxrouter sandboxes exec <sandbox> --stdin -- node read-input.jsexec waits for the command, prints its output, and exits with the command's exit code when it fails. --timeout takes 1 to 7,200 seconds (600 by default). With --detach, it returns at once with the exec's ID (and a detached command may run up to 86,400 seconds):
sandboxrouter sandboxes exec <sandbox> <exec-id>
sandboxrouter sandboxes exec <sandbox> <exec-id> --cancelWith the API
curl https://api.sandboxrouter.io/v1/sandboxes/<sandbox>/exec \
-H "Authorization: Bearer $SANDBOXROUTER_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: run-tests-0001" \
-d '{ "command": ["npm", "test"], "env": { "CI": "1" }, "timeoutSeconds": 1800 }'The body takes command, and optionally stdin (text up to 64 KiB), env, workdir, timeoutSeconds (1 to 7,200; without it, the command has no time limit) and detach. Resending the same Idempotency-Key never runs the command twice. The request holds until the command ends and answers 200, or, with "detach": true, answers 202 at once with its ID:
{ "execId": "…", "state": "succeeded", "createdAt": 1791797400000, "endedAt": 1791797412000,
"exitCode": 0, "signal": null, "stdout": "…", "stderr": "" }state is running, succeeded, failed, timed_out, canceled or lost. stdout and stderr are null until it ends, then the whole output. Read it again with GET …/exec/<exec-id>, and cancel it with DELETE …/exec/<exec-id>. A provider that can't take an exec's stdin, env, workdir or time limit refuses it, naming what it lacks.
Read the log
A sandbox's log holds what Sandbox Router recorded about it: each state it reached, with why. What its command writes is its provider's, and a command's output is in its exec. The console shows the log as Runtime output on the sandbox's page.
sandboxrouter sandboxes logs <sandbox>
sandboxrouter sandboxes logs <sandbox> --since 2026-10-11T09:00:00Z --followIn the API, GET /v1/sandboxes/<sandbox>/logs answers every entry at or after ?since= (milliseconds or an ISO 8601 time), oldest first: { "entries": [{ "at", "stream", "message", "kind" }] }. With ?follow=true, the answer is newline-delimited JSON: those entries, then each new one as it comes, for up to 10 minutes; follow on with since. Entries are kept for the sandbox's lifetime; once it's deleted, its log is not found, as the sandbox isn't.
Metrics
A sandbox's CPU, memory and disk over time are its provider's own measurements, kept as long as the provider keeps them, whether the sandbox runs or not, and once it's deleted too. Native, Daytona and E2B keep them; Freestyle and Maritime keep none, and the request says so. Native measures no total memory or disk, so those are null.
sandboxrouter sandboxes metrics <sandbox> --since 2026-10-11T09:00:00ZIn the API, GET /v1/sandboxes/<sandbox>/metrics answers the samples from ?from= to ?to= (each in milliseconds; without from, from the sandbox's create, and without to, until now), oldest first: { "samples": [{ "at", "cpuPct", "memUsedBytes", "memTotalBytes", "diskUsedBytes", "diskTotalBytes" }] }, CPU as a percentage of all its CPUs and the rest in bytes, null where the provider doesn't measure it.
Move files
The files API reads, writes and deletes one file of a running sandbox at a time, whole, with its provider's own file operations. A provider with no file API of its own does each with one command of the image's own sh, cat, mkdir and rm, so the image needs them there. A path is absolute, not under /proc, /sys or /dev. A provider that can't do one of them refuses it, naming what it lacks.
sandboxrouter sandboxes files put <sandbox> /workspace/config.json config.json
sandboxrouter sandboxes files get <sandbox> /workspace/config.json > config.json
sandboxrouter sandboxes files delete <sandbox> /workspace/config.json| Request | Does |
|---|---|
GET /v1/sandboxes/<sandbox>/files/<path> | A file's bytes |
PUT …/files/<path> | Writes the body as the file |
DELETE …/files/<path> | Deletes the file |
Percent-encode each segment of the path, as in …/files/workspace/my%20file.txt.
Outbound access
A sandbox's egress lists the hosts it may reach, enforced by its provider:
*allows every host. It's the default for a sandbox created without a list.A list of up to 32 hosts allows those hosts and blocks everything else. Write each as an exact host, such as
pypi.org, or as*.example.comfor the names under example.com (listexample.comas well if the sandbox calls it). A host is never an IP address, a port, a path or*inside a list. List the hosts of the connectors it uses too.An empty list reaches no host.
On your own Daytona account, a list (or an empty one) applies only in a Tier 3 or higher Daytona organization. Below that, Daytona's own network policy applies instead.
Where you set it:
Console: in New sandbox, list the hosts under Allowed outbound hosts, one per line, or leave it empty for every host. On an existing sandbox, choose Edit on its Network card and change the list there. Deploying an MCP server has the same field, where an empty list reaches no host.
CLI: pass
--egress HOSTonce per host tosandboxrouter sandboxes create,sandboxrouter sandboxes update,sandboxrouter runorsandboxrouter mcp create;--egress '*'allows every host, and--no-egressnone:shellsandboxrouter run --egress api.github.com -- node -e "fetch('https://api.github.com/zen').then(r => r.text()).then(console.log)"For a command that starts with
npx, the CLI addsregistry.npmjs.orgto a list that doesn't already cover it.API:
"egress": ["api.github.com", "*.npmjs.org"]onPOSTorPATCH /v1/sandboxes/<sandbox>.Files: list hosts under
egress:in an MCP server file.
A sandbox created with * stays on its provider's open network, and one created with a list on its fenced one, so a later change reaches at most what that network admits.
What a list allows
A sandbox with a list reaches its hosts over HTTP and HTTPS. With
*, it reaches any host on any port.A list never reaches an IP address,
localhostor an internal name, and reaches only hosts under these top-level domains: .ai, .app, .au, .biz, .br, .ca, .cc, .ch, .cloud, .cn, .co, .com, .de, .dev, .edu, .es, .eu, .fr, .gg, .gov, .in, .info, .io, .it, .jp, .kr, .ly, .me, .net, .nl, .org, .page, .pl, .run, .se, .sh, .so, .tech, .tv, .uk, .us and .xyz.
Owners and operators can change a sandbox's outbound hosts; everyone in the workspace can read them. The bytes a sandbox on Native sends to the hosts it's allowed to reach bill as egress; see pricing.
Port URLs
A sandbox's ports are served by its provider. Ask for a port's URL while the sandbox runs: Get URL on the sandbox's page, sandboxrouter sandboxes port <sandbox> 3000, or GET /v1/sandboxes/<sandbox>/ports/3000, which answers { "port", "url" }. Sandbox Router doesn't sit in between: who can open a URL is up to the provider, which may answer only requests that carry its own credentials for the sandbox, and Sandbox Router never shows those. E2B and Daytona give each port a URL; Native and Freestyle give none, and the request says so.
A request to a URL goes to the provider: Sandbox Router doesn't bill it.
Snapshots
A snapshot is the provider's own copy of a sandbox, its memory too where the provider keeps memory, kept until you delete it. A provider without snapshots refuses them.
Snapshots are taken of a running sandbox. Choose Take snapshot on the sandbox's page, or:
sandboxrouter sandboxes snapshots create <sandbox>
sandboxrouter sandboxes snapshots list <sandbox>
sandboxrouter sandboxes snapshots delete <sandbox> <snapshot-id>In the API, POST /v1/sandboxes/<sandbox>/snapshots with {}, GET …/snapshots, which answers { "snapshots": [{ "id", "image", "memory", "createdAt" }] }, newest first, and DELETE …/snapshots/<snapshot-id>. A snapshot's image is the one a sandbox created from it boots.
Delete a sandbox
Choose Delete in the sandbox's row menu in Sandboxes, run sandboxrouter sandboxes delete <sandbox>, or send DELETE /v1/sandboxes/<sandbox>. Deleting a sandbox removes it and its provider's snapshots of it for good. Deleting a sandbox that hosts an MCP server deletes the server too. Deleting one a workflow keeps between runs fails a step running in it; the workflow's next step on it creates it again.
List sandboxes
The console's Sandboxes page lists every sandbox of the workspace, those hosting MCP servers too, with filters by state and a search by ID, image or label.
sandboxrouter sandboxes list --label team=web --provider cloudflareGET /v1/sandboxes answers { "sandboxes", "nextCursor" }, newest first, 50 at a time (?limit= up to 100); pass nextCursor back as ?cursor= for the next page. Filter with ?label=key=value (repeat it; all must match; user=<user> finds a user's sandboxes) and ?provider=. The list catches up within seconds: a new sandbox shows once Sandbox Router hears it started, and each one as it last heard of it, without its variables (read the sandbox for those).
A web browser
With the Browser connector, the AI tools in a sandbox that names it get a web browser. A sandbox started for a user signs in to websites with that user's logins, and one without a user with your workspace's; the AI never sees their passwords, codes or passkeys.
Its browsers run outside the sandbox, so the sites they open needn't be among its outbound hosts, and they take nothing of its size.
A file a browser tool reads or writes, such as an upload or a screenshot, is a file of this sandbox, relative to
/workspace.Its browsers close when it stops or is deleted, and the next tool after a start opens a new one.
Browser time bills apart from the sandbox's own (see Browser time).
Limits
A workspace has no vCPU or memory quota: its balance and its providers' own limits bound the sandboxes it runs. These are the limits per sandbox and per command.
| Limit | Value |
|---|---|
| Command | 1 to 32 arguments of up to 512 characters each |
| Command time limit | 1 second to 2 hours (24 hours detached) |
| Standard input | 64 KiB |
| Variables | 64, values up to 4,096 characters |
| Outbound hosts | 32 |
| Connectors | 32 |
| Labels | 16 |
| Idle stop | 5 to 240 minutes |