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}'
FieldTypeDefaultRules
sizestringsmallOne of small, medium, large.
namestringnullYour own label, at most 100 characters. Never interpreted by the platform.
timeout_minutesinteger15Between 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

SizevCPUMemoryDiskPrice
small24 GB40 GB$0.09/hour
medium48 GB80 GB$0.28/hour
large816 GB160 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

statusMeaningCan run commands?
provisioningThe 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
runningReady. This is the only state in which commands execute.Yes
deletingTeardown started but has not finished — the platform retries it in the background.No — 409
deletedGone. The machine and its disk no longer exist. The record remains for your history and billing.No — 409
failedProvisioning 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_minutes defaults 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. The timeout_minutes field keeps its original value, so read expires_at to know the real deadline.
  • Extending a sandbox that is not running returns 409 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"
  • 200 with status: "deleted" — the machine is gone and billing has stopped.
  • 202 with status: "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 stdout from 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.