Sign in

Use Sandboxes from Python or TypeScript

A small helper for each language that creates a sandbox, runs commands, moves files, forks it, and deletes it. No SDK to install.

On this page5

The Sandbox API is plain HTTPS with a bearer key, so one short file is enough: the Python helper uses only the standard library, and the TypeScript one only fetch. Copy one into your project and replace the demo at its end with your own steps. From a shell, the CLI does the same.

1. Export an API key

Make a key under Dashboard → Settings → API keys with Full access, or Custom with Sandboxes: Write (Open Sandboxes also makes one). The helpers read it from PRIZED_API_KEY; never paste a key into code.

export PRIZED_API_KEY=dcp_…

2. Save a helper

sandbox.py
import json, os, time, urllib.error, urllib.parse, urllib.request

API = "https://api.prized.dev/api/v1"
KEY = os.environ["PRIZED_API_KEY"]  # never hardcode a key
FREE = "That is a free-tier limit (https://prized.dev/docs/sandboxes#free-tier); a top-up or a plan lifts it."

def call(method, url, body=None, data=None):
    headers = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/octet-stream"}
    if body is not None:
        data, headers["Content-Type"] = json.dumps(body).encode(), "application/json"
    try:
        with urllib.request.urlopen(urllib.request.Request(url, data, headers, method=method), timeout=3660) as r:
            out = r.read()
            return json.loads(out) if r.headers.get_content_type() == "application/json" else out
    except urllib.error.HTTPError as e:
        out = e.read()
        err = json.loads(out) if out.startswith(b"{") else {"error": out[:200]}
        hint = FREE if err.get("tier") == "free" else ""
        e.msg = f"{err} {hint}"  # str(e) is now "HTTP Error 429: {'error': ...} That is a free-tier limit ..."
        raise

class Sandbox:
    def __init__(self, id):
        self.id = id

    @classmethod
    def create(cls, template="prized-base-v1"):
        body = {"template": template, "resources": {"vcpu": 2, "memoryMb": 4096}, "maxRuntimeMin": 60}
        return cls(call("POST", f"{API}/sandboxes", body)["sandbox"]["id"])

    def wait(self):  # until running and taking commands
        while (sbx := call("GET", f"{API}/sandboxes/{self.id}")["sandbox"])["state"] == "starting":
            time.sleep(1)
        if sbx["state"] != "running":
            raise RuntimeError(f"{self.id} is {sbx['state']}: {sbx['errorReason']}")
        self.toolbox = sbx["toolboxBase"]
        self.exec("true", retry=(409, 429, 502, 503))  # a no-op, so retrying even a 502 is safe
        return self

    def exec(self, command, retry=(409, 429), **opts):  # opts: cwd, env, timeout
        for i in range(6):
            try:
                return call("POST", f"{self.toolbox}/process/execute", {"command": command, **opts})
            except urllib.error.HTTPError as e:  # 409 and 429 come before the command runs
                if e.code not in retry or i == 5:
                    raise
                time.sleep(i + 1)

    def write(self, path, data: bytes):
        call("POST", f"{self.toolbox}/files/upload?path={urllib.parse.quote(path)}", data=data)

    def read(self, path) -> bytes:
        return call("GET", f"{self.toolbox}/files/download?path={urllib.parse.quote(path)}")

    def fork(self, count):
        out = call("POST", f"{API}/sandboxes/{self.id}/fork", {"count": count})
        return [Sandbox(s["id"]) for s in out["sandboxes"]]

    def delete(self):
        call("DELETE", f"{API}/sandboxes/{self.id}")

sbx, kids = Sandbox.create(), []
try:
    print(sbx.wait().exec("python3 --version")["stdout"], end="")
    sbx.write("/home/prized/work/hello.txt", b"hello from Python\n")
    print(sbx.read("/home/prized/work/hello.txt").decode(), end="")
    kids = sbx.fork(2)  # two live copies, hello.txt included
    for kid in kids:
        print(kid.id, kid.wait().exec("cat hello.txt")["stdout"], end="")
finally:
    for s in [sbx, *kids]:
        try:
            s.delete()
            print("deleted", s.id)
        except Exception as e:  # try the rest; maxRuntimeMin removes this one anyway
            print("could not delete", s.id, e)

3. Run it

The demo creates a 2 vCPU, 4 GB sandbox that deletes itself after 60 minutes, runs a command, writes and reads a file, forks two copies that already have the file, and deletes all three. Python needs 3.8 or later:

python3 sandbox.py

Node 22.18 or later runs the TypeScript file as it is; on Node 18 to 22.17, run npx tsx sandbox.mts instead. Older Node has no global fetch, so upgrade it first.

node sandbox.mts

A run of the Python file takes about 8 seconds:

Python 3.12.3
hello from Python
sbx_2f7f62ea3f77116f hello from Python
sbx_0426aba73aa4e2ff hello from Python
deleted sbx_723373199c11b589
deleted sbx_2f7f62ea3f77116f
deleted sbx_0426aba73aa4e2ff

The TypeScript file prints the same with v22.23.3 and hello from TypeScript.

What the helper handles

  • Waiting. A create answers while the sandbox is starting. wait() reads it until running, keeps its toolboxBase (where its commands and files go), then runs true until the sandbox takes commands: for a moment after running, it can still answer 409 or 502.
  • Retries. exec() retries 409 sandbox_not_running and 429 rate_limited, which come before a command runs, up to 5 times. It never retries 502 sandbox_unreachable, since the command may have run; for one that is safe to run twice, pass retry=(409, 429, 502) in Python or [409, 429, 502] as the third argument in TypeScript.
  • Cleanup. finally deletes the sandbox and each fork even when a step or another delete fails, and maxRuntimeMin: 60 deletes anything left behind.
  • Files. read() returns bytes, so archives and images arrive intact; decode text yourself, as the demo does.
  • Long commands. A command runs for up to 300 seconds unless you pass timeout (3,600 at most). Node's fetch stops waiting for an answer after 300 seconds, so in TypeScript run a job that may take that long in the background, such as nohup ./job.sh > job.log 2>&1 &, and read job.log with later commands.

Errors

An error carries the API's status and body: the error code, and fields such as reason or fits. When a free-tier limit refused the request, the body has "tier": "free" and the helper adds a link to the limits. The ones you are most likely to see (every code):

ErrorMeansDo
429 sandbox_quota_exceededA quota or free-tier limit, named in reason. Paused sandboxes countDelete sandboxes you are done with; on the free tier, a top-up or a plan lifts the limits
422 invalid_resourcesA shape outside the template's bounds, or the free tier's 2 vCPU and 4 GBAsk for less
409 fork_capacityThe parent's machine has room for only fits more copiesFork fits or fewer; at fits: 0, fork a new sandbox instead, or wait