Sandboxes
Create, list, rename, stop, start, resize, archive, and delete sandboxes.
A sandbox is one isolated microVM, created from a template. Use sandboxes to run untrusted code, agents, or full dev workloads. Manage them in the dashboard or via the API (reference).
Sandboxes are persistent by default. stop keeps the disk and the spec, and start brings the sandbox back. A sandbox is removed only when you delete it, or automatically for ephemeral sandboxes. See Lifecycle.
Create
import { Platinum } from "@platinum-dev/sdk";
const client = new Platinum({ url: process.env.PT_API_URL!, token: process.env.PT_TOKEN! });
const sbx = await client.sandboxes.create(
{ template: "pt-base", cpu: 2, ram_mb: 1024 },
{ waitForRunning: true },
);import os
from platinum import Platinum
client = Platinum(token=os.environ["PT_TOKEN"], api_url=os.environ["PT_API_URL"])
sbx = client.sandboxes.create(template="pt-base", cpu=2, ram_mb=1024,
wait_for_running=True)pt sandbox create -t pt-base --cpu 2 --ram 1024curl -sS -X POST "$PT_API_URL/v1/sandboxes?wait_for_state=running" \
-H "Authorization: Bearer $PT_TOKEN" -H "Content-Type: application/json" \
-d '{"template":"pt-base","cpu":2,"ram_mb":1024}'Common create fields:
| Field | Default | What it does |
|---|---|---|
template | pt-base | Template to boot from. Or pass image for an inline build. Set one, not both. |
type | persistent | persistent never auto-stops. ephemeral applies the auto-lifecycle defaults. See Retention. |
cpu · ram_mb · disk_gb | template defaults | Resource shape. Template minimums are enforced. |
env | {} | Environment variables set in the sandbox at boot. |
expose | — | Ports to publish in the create call. See Networking. |
region | template's, else any | Placement. See Regions. |
metadata | {} | Arbitrary JSON (≤ 16 KiB), echoed back on reads. |
The create body also accepts ssh_keys (Terminal), volume_ids (Volumes), and gpus. Full options, with types and validation rules, are in the reference for your surface:
- TypeScript:
sandboxes.createoptions - Python:
sandboxes.createkwargs - Raw HTTP:
POST /v1/sandboxes
Safe retries
POST /v1/sandboxes accepts an optional Idempotency-Key header. Send the same
key on a retry and you get the original sandbox back instead of a second VM (and
a second billing stream). Without the header nothing changes: the request behaves
exactly as it always has.
KEY=$(uuidgen)
curl -sS -X POST "$PT_API_URL/v1/sandboxes" \
-H "Authorization: Bearer $PT_TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d '{"template":"pt-base","cpu":2,"ram_mb":1024}'Rules worth knowing:
- The key must be 8-255 characters. Generate a fresh one per logical create (a UUID is ideal); do not reuse a key across different requests.
- A replay returns 200 with
"replayed": trueand the originalid, where the first call returned 201. - The key is bound to the request: the same key with a different body, or a
different
wait_for_state/wait_timeout_ms, returns 422idempotency_key_reused. - Keys are scoped to the API key (or user) that created them, and expire after 24 hours.
- A replay does not return the
exposedarray, because preview tokens are not stored. CallPOST /v1/sandboxes/:id/exposeagain if you need the URLs (it is idempotent). - If the sandbox was deleted or failed to start, the key is dropped and the retry creates a fresh sandbox rather than handing back a dead one.
- A retry still counts against the spawn rate limit, so a retry storm can still get a 429.
- The header is honoured only on create.
POST /:id/cloneandPOST /:id/forkignore it, and the vendor-compatibility endpoints cannot send it.
Named sandboxes are already duplicate-safe.
name is unique per org
across live sandboxes, so a retried create with the same name returns 409
name_taken instead of making a second box.
Lifecycle
| State | What it means |
|---|---|
pending | The create request is accepted and the sandbox is queued. The first boot has not started. |
provisioning | The sandbox boots for the first time. It becomes running on success, or failed-start on failure. |
starting | A stopped sandbox comes back up. It becomes running when it is reachable. |
resuming | The sandbox boots from a snapshot after restore, clone, or fork. Snapshots hold disk only, so processes do not carry over. |
unarchiving | An archived sandbox returns from cold storage before it starts. |
restoring | The sandbox cold-boots from a filesystem backup. This also runs automatically after a host is lost. |
running | The sandbox is live. It accepts commands, code execution, and file operations. Snapshots start from this state. |
stopping | The sandbox shuts down and writes its state to disk. It becomes stopped. |
stopped | The sandbox is frozen to disk. The filesystem is preserved, and start brings it back. |
archiving | The stopped sandbox's disk moves to cold storage. It becomes archived. |
archived | The disk is in cold storage and the local disk is freed. start restores the sandbox. |
failed-start | The sandbox did not come up. If it ran before, start retries it. |
deleted | The sandbox and its disk are torn down. This state is final; the record remains for the audit log. |
Only running bills compute. stopped bills local storage. archived bills nothing. See Billing.
Retention
type sets when a sandbox auto-stops, auto-archives, and auto-deletes:
type | idle → stop | stopped → archive | stopped → delete |
|---|---|---|---|
persistent | never | 7 days | never |
ephemeral | 15 min | 7 days | 30 days |
These are defaults. Pass auto_stop_minutes, auto_archive_days, or auto_delete_days at create to override one. 0 disables that timer.
List
const { rows } = await client.sandboxes.list({ state: "running", limit: 100 });
for await (const s of client.sandboxes.iterate({ state: "running" })) { /* auto-paginate */ }rows = client.sandboxes.list(state="running", limit=100)["rows"]
for s in client.sandboxes.iter(state="running"):
...pt sandbox list --state running --limit 100curl "$PT_API_URL/v1/sandboxes?state=running&limit=100" -H "Authorization: Bearer $PT_TOKEN"Query params: state, limit (default 50, max 200), offset, and paginated=true. Results are org-scoped.
Parent and child sandboxes
A sandbox can be the child of another sandbox. Set parent_id at create time. A sandbox without a parent_id is a top-level sandbox.
Use this to group related sandboxes. An agent working inside a sandbox can create child sandboxes and manage only those.
const child = await client.sandboxes.create({ template: "pt-base", parent_id: parent.id });
const kids = await client.sandboxes.children(parent.id);child = client.sandboxes.create(template="pt-base", parent_id=parent["id"])
kids = client.sandboxes.children(parent["id"])pt sandbox create -t pt-base --parent sbx_01K…
pt sandbox children sbx_01K…curl -X POST "$PT_API_URL/v1/sandboxes" -H "Authorization: Bearer $PT_TOKEN" \
-H 'Content-Type: application/json' -d '{"template":"pt-base","parent_id":"sbx_01K…"}'
curl "$PT_API_URL/v1/sandboxes/$SBX/children" -H "Authorization: Bearer $PT_TOKEN"The parent must be running and in the same organization. A missing parent returns 404.
parent_id is set once at create. You cannot move a sandbox to a different parent.
List sandboxes by position in the tree with parent_id:
const children = await client.sandboxes.list({ parent_id: parent.id });
const roots = await client.sandboxes.list({ rootsOnly: true });children = client.sandboxes.list(parent_id=parent["id"])["rows"]
roots = client.sandboxes.list(roots_only=True)["rows"]pt sandbox list --parent sbx_01K…
pt sandbox list --rootscurl "$PT_API_URL/v1/sandboxes?parent_id=$SBX" -H "Authorization: Bearer $PT_TOKEN"
curl "$PT_API_URL/v1/sandboxes?parent_id=none" -H "Authorization: Bearer $PT_TOKEN"The default list is unchanged. Without parent_id you get every sandbox in the organization.
Warning: delete children before the parent. Deleting a parent that still has live children returns 409. The response names the blocking children.
Deleting a sandbox never deletes its children.
Get
const s = await client.sandboxes.get("sbx_01K…");
const sbx = await client.sandboxes.connect("sbx_01K…");s = client.sandboxes.get("sbx_01K…")
sbx = client.sandboxes.connect("sbx_01K…")pt sandbox get sbx_01K…curl "$PT_API_URL/v1/sandboxes/$SBX" -H "Authorization: Bearer $PT_TOKEN"get returns the record; connect returns a handle for further calls. Live metrics and billed usage are separate endpoints: GET /v1/sandboxes/:id/metrics and GET /v1/sandboxes/:id/usage.
Rename
Pass null (Python: None) to clear the name.
await sbx.rename("ci-runner-2");sbx.rename("ci-runner-2")pt sandbox rename $SBX ci-runner-2
# clear the name:
pt sandbox rename $SBX --clearcurl -X PATCH "$PT_API_URL/v1/sandboxes/$SBX" -H "Authorization: Bearer $PT_TOKEN" \
-H "Content-Type: application/json" -d '{"name":"ci-runner-2"}'Stop and start
stop saves memory where it can, so start resumes with processes intact. start also unarchives an archived sandbox first.
await sbx.stop({ waitForStopped: true });
await sbx.start({ waitForRunning: true });sbx.stop(wait_for_stopped=True)
sbx.start(wait_for_running=True)pt sandbox stop sbx_01K…
pt sandbox start sbx_01K…curl -X POST "$PT_API_URL/v1/sandboxes/$SBX/stop?wait_for_state=stopped" -H "Authorization: Bearer $PT_TOKEN"
curl -X POST "$PT_API_URL/v1/sandboxes/$SBX/start?wait_for_state=running" -H "Authorization: Bearer $PT_TOKEN"Resize
Resize a stopped sandbox only. disk_gb can only grow. The resize cold-boots the sandbox: the disk survives, in-memory state does not.
await sbx.resize({ cpu: 4, ram_mb: 8192, disk_gb: 8 });sbx.resize(cpu=4, ram_mb=8192, disk_gb=8)pt sandbox resize sbx_01K… --cpu 4 --ram 8192 --disk 8curl -X POST "$PT_API_URL/v1/sandboxes/$SBX/resize" -H "Authorization: Bearer $PT_TOKEN" \
-H "Content-Type: application/json" -d '{"cpu":4,"ram_mb":8192,"disk_gb":8}'Archive
Archive a stopped sandbox only. Archiving moves the disk to cold storage and frees the local disk. An archived sandbox bills nothing. start restores it; there is no separate unarchive endpoint.
await sbx.archive();sbx.archive()pt sandbox archive $SBXcurl -X POST "$PT_API_URL/v1/sandboxes/$SBX/archive" -H "Authorization: Bearer $PT_TOKEN"Delete
Delete removes the sandbox and its disk. The call returns immediately and tears down in the background.
await sbx.delete();sbx.delete()pt sandbox rm sbx_01K…curl -X DELETE "$PT_API_URL/v1/sandboxes/$SBX" -H "Authorization: Bearer $PT_TOKEN"Snapshot and fork
A running sandbox also supports snapshot, restore, clone, fork, and off-host backup. See Snapshots.