Sandboxes
A sandbox is a dedicated virtual machine that exists for as long as you need it and is then destroyed with its disk. This page covers the lifecycle, the timers, and what survives (nothing).
Creating one
curl -sS -X POST https://sandbox-as-a-service.com/v1/sandboxes \
-H "Authorization: Bearer $AAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: job-2f4a91" \
-d '{"size":"small","name":"nightly-etl","timeout_minutes":60}'
| Field | Type | Default | Rules |
|---|---|---|---|
size | string | small | One of small, medium, large. |
name | string | null | Your own label, at most 100 characters. Never interpreted by the platform. |
timeout_minutes | integer | 15 | Between 1 and 1440. The sandbox is destroyed automatically after this long. |
The call is synchronous: it returns 201 Created with a Location header once
the machine is up and ready to run commands, so the sandbox in the response already has
status: "running" and a ready_at timestamp. If provisioning fails you get a
503 with error.type: "provisioning_failed" and nothing is billed.
Sizes
| Size | vCPU | Memory | Disk | Price |
|---|---|---|---|---|
small | 2 | 4 GB | 40 GB | $0.09/hour |
medium | 4 | 8 GB | 80 GB | $0.28/hour |
large | 8 | 16 GB | 160 GB | $0.55/hour |
Size is fixed for the life of a sandbox; there is no resize. Pick small unless you
know you need more — it is the cheapest, and start-up time is the same for every size
(see limits).
Statuses
status | Meaning | Can run commands? |
|---|---|---|
provisioning | The machine is being created and booted. Because create is synchronous you normally never observe this state; it is visible if you fetch the sandbox from another request while the create call is still in flight. | No — 409 sandbox_not_running |
running | Ready. This is the only state in which commands execute. | Yes |
deleting | Teardown started but has not finished — the platform retries it in the background. | No — 409 |
deleted | Gone. The machine and its disk no longer exist. The record remains for your history and billing. | No — 409 |
failed | Provisioning did not complete. The sandbox is already torn down and is not billed; the record carries error: "Provisioning failed". | No |
Statuses only move forward. Nothing returns from deleted or failed to
running — create a new sandbox instead.
Expiry and extending
Every sandbox has an expires_at. A sweeper runs every minute and destroys sandboxes
past it. Expiry is a safety net against leaked machines, not a scheduler — it will cut off work in
progress, so size timeout_minutes to the job and destroy the sandbox yourself when done.
To push the deadline out while work is still running:
curl -sS -X POST https://sandbox-as-a-service.com/v1/sandboxes/$SBX/extend \
-H "Authorization: Bearer $AAS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"additional_minutes":30}'
additional_minutesdefaults to 15 and must be between 1 and 1440.- Extension is measured from the current
expires_at(or from now, if it has already passed), and is capped at 1440 minutes after creation. That ceiling is absolute — a sandbox can never live longer than 1440 minutes no matter how often you extend it. - Extending only moves
expires_at. Thetimeout_minutesfield keeps its original value, so readexpires_atto know the real deadline. - Extending a sandbox that is not
runningreturns409 sandbox_not_running.
For work that needs to outlive the ceiling, split it: persist results to your own storage from inside the sandbox (S3, a database, an HTTP endpoint you control) and start a fresh sandbox for the next stage.
Reading state
# One sandbox
curl -sS https://sandbox-as-a-service.com/v1/sandboxes/$SBX -H "Authorization: Bearer $AAS_API_KEY"
# All active sandboxes, newest first
curl -sS "https://sandbox-as-a-service.com/v1/sandboxes?limit=20" -H "Authorization: Bearer $AAS_API_KEY"
# Include destroyed ones (history)
curl -sS "https://sandbox-as-a-service.com/v1/sandboxes?include_deleted=true&limit=50" -H "Authorization: Bearer $AAS_API_KEY"
The list endpoint returns { object: "list", data: [...], has_more, next_cursor }.
limit defaults to 20 and is capped at 100. To page, pass the previous response's
next_cursor as starting_after; it is the created_at timestamp of the
last item, and paging walks backwards through time. Sandboxes belonging to other accounts are invisible:
a wrong id is a 404, never someone else's data.
Destroying
curl -sS -X DELETE https://sandbox-as-a-service.com/v1/sandboxes/$SBX -H "Authorization: Bearer $AAS_API_KEY"
200withstatus: "deleted"— the machine is gone and billing has stopped.202withstatus: "deleting"— teardown could not complete right now. The platform retries in the background until the machine is gone. Treat it as accepted; do not retry in a loop.- Deleting an already-deleted sandbox is a no-op that returns
200, so DELETE is safe to retry.
Idempotency
Creating a sandbox provisions a real machine, so a retried request that silently created a second
one would cost you money. Send an Idempotency-Key header (any string up to 255 characters,
unique per logical operation) and a repeat of the same request returns the original sandbox with
200 instead of 201:
KEY="build-$(git rev-parse --short HEAD)"
curl -sS -X POST https://sandbox-as-a-service.com/v1/sandboxes \
-H "Authorization: Bearer $AAS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d '{"size":"small"}' -w '\nstatus=%{http_code}\n'
# Same key again -> status=200 and the same sandbox id.import os
import time
import uuid
import requests
API = "https://sandbox-as-a-service.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['AAS_API_KEY']}"}
def create_sandbox(idempotency_key, **body):
"""Retry-safe create: the same key never provisions a second machine."""
for attempt in range(3):
r = requests.post(
f"{API}/sandboxes",
headers={**HEADERS, "Idempotency-Key": idempotency_key},
json=body,
timeout=600,
)
if r.status_code in (200, 201):
return r.json() # 201 = created, 200 = the key was reused
if r.status_code in (429, 500, 502, 503):
time.sleep(2 ** attempt) # transient: safe to retry with the same key
continue
r.raise_for_status()
raise RuntimeError("sandbox creation kept failing")
sandbox = create_sandbox(f"job-{uuid.uuid4()}", size="small", timeout_minutes=30)
Keys are scoped to your account and never expire, so a key you reuse next week still returns the sandbox from last week — including one that has since been destroyed. Derive keys from the unit of work (a job id, a commit SHA, a request id), not from a constant.
What persists: nothing
There is no persistent volume, no snapshot, and no image you can save. When a sandbox is destroyed — by you, by expiry, or because your balance hit zero — the machine and its disk are destroyed with it. Anything you want to keep has to leave the sandbox before then:
- Read it back through
stdoutfrom an exec call (capped at 1 MiB per stream). - Upload it from inside the sandbox to your own storage or an endpoint you control.
- Push it to a git remote you own.
Within a single sandbox's lifetime, the filesystem does persist across exec calls — a file written
by one command is there for the next. What does not carry over between commands is shell
state: each exec is a fresh non-login shell, so cd, export and activated
virtualenvs do not survive. See running commands.