Python SDK
The typed Python client for Platinum, in sync and async form.
platinum-sdk is the typed Python client for the Platinum API. It covers sandboxes, templates, volumes, and webhooks. Use it in server-side Python; the TypeScript SDK has the same surface.
Install
pip install platinum-sdkThe package requires Python 3.9+ and has one dependency (httpx). It is fully typed (PEP 561).
Quickstart
from platinum import Platinum
client = Platinum(token="pt_live_…", api_url="https://api.platinum.dev")
sbx = client.sandboxes.create(
template="pt-base",
expose=[{"port": 8000, "public": True}],
wait_for_running=True,
)
print(sbx.exposed_url(8000))
print(sbx.exec(["python3", "-c", "print(2 + 2)"]).check().stdout)
sbx.sh("echo $HOME && ls /")
sbx.delete()Client
The timeout argument bounds each request. The default is 60 seconds. Close the client with client.close().
Async
AsyncPlatinum mirrors Platinum one to one over httpx.AsyncClient. The methods and arguments are the same, awaited. Everything below applies to both clients.
from platinum import AsyncPlatinum
async with AsyncPlatinum() as client:
sbx = await client.sandboxes.create(template="pt-base", wait_for_running=True)
print((await sbx.exec(["uname", "-a"])).stdout)
async for row in client.sandboxes.iter(state="running"):
print(row["id"])
await sbx.delete()Close a standalone async client with await client.aclose(). The async client builds templates with await client.templates.build(tpl, name). The sync client uses tpl.build(client, name).
Sandboxes
create returns a Sandbox handle. Pass template (a prebuilt name or id) or image (a Template builder). Do not pass both.
sbx = client.sandboxes.create(
template="pt-base",
cpu=2, ram_mb=2048,
env={"STAGE": "prod"},
wait_for_running=True,
)create also accepts lifecycle timers, ssh_keys, volume_ids, gpus, and metadata. The first inline image build can exceed the default wait. Raise wait_timeout_ms for it. See regions, volumes, and networking.
Each expose entry is {"port": int, "public": bool}. Read the URLs from the handle with sbx.exposed_url(port).
Find and list:
| Method | Returns |
|---|---|
sandboxes.get(id) | raw row (dict) |
sandboxes.connect(id) | hydrated Sandbox handle (404 raises NotFoundError) |
sandboxes.list(limit=50, offset=0, state=None) | {rows, total, has_more} |
sandboxes.iter(state=None, page_size=100) | generator over every page |
sandboxes.rename(id, name) | set or clear the name (None clears; 409 if taken) |
sandboxes.delete(id) | delete (also sbx.delete()) |
Lifecycle
The Sandbox handle carries id, name, and exposed (ports exposed at create). See sandboxes.
| Method | Notes |
|---|---|
refresh() | re-fetch the row (includes state) |
wait_running(timeout=30, interval=0.25) | poll until running; raises on a terminal state |
wait_state(target, timeout=30, interval=0.25) | poll until any target state |
stop(wait_for_stopped=False) | stop the sandbox; identity and disk persist |
start(wait_for_running=False) | start a stopped or archived sandbox |
pause() / resume() | suspend and resume the running sandbox |
resize(cpu=, ram_mb=, disk_gb=) | stopped sandboxes only; disk_gb can only grow |
rename(name) | set or clear the name |
delete() / kill() | kill is an alias |
Run commands
The call returns output when the command exits. See exec.
sbx.exec(["ls", "-la", "/"]) # argv, direct execve
sbx.exec("echo hi && ls") # string, wrapped in sh -c
sbx.sh("for i in 1 2 3; do echo $i; done")
sbx.run_code("print(sum(range(10)))") # lang defaults to the sandbox language| Method | Notes |
|---|---|
exec(cmd, timeout_ms=30_000) | argv list or string; returns ExecResult; .check() raises on nonzero exit |
sh(script, timeout_ms=30_000) | sugar for exec(["sh", "-c", script]) |
run_code(code, lang=None, timeout_ms=30_000) | lang: python, bash, node, or sh; defaults to the create-time language |
Files
sbx.files reads and writes anywhere in the guest filesystem. See filesystem.
sbx.files.write("/workspace/app.py", "print('hi')")
data = sbx.files.read("/workspace/app.py") # bytes
for ev in sbx.files.watch("/workspace", max_seconds=60):
if ev["event"] == "change":
print(ev["data"]["type"], ev["data"]["path"])| Method | Notes |
|---|---|
read(path) / write(path, body) | read returns bytes; body is str or bytes |
delete(path, recurse=False) | |
list(path) / stat(path) / mkdir(path) | |
exists(path) | returns False instead of raising |
find(path, pattern, max=None) | filename glob; returns matched paths |
grep(path, pattern, max=None) | content search; returns file:line:content lines |
replace(path, find, replace, glob="*") | in-place sed -E across files |
watch(path, max_seconds=300) | generator of change events; the server ends it after max_seconds |
Networking
Expose a port through the edge proxy. See networking.
r = sbx.expose(8000) # private, HMAC-token URL
print(r["url"])
sbx.expose(8000, public=True) # public URL, no token| Method | Notes |
|---|---|
expose(port, public=None, ttl_seconds=None) | returns {url, …}; public=True drops the token |
unexpose(port) | tear the port down; kills all tokens ever minted |
exposed_url(port) | URL of an already-exposed port, else None |
revoke_expose_token(token, reason=None) | kill one leaked token; the port stays up |
set_egress_policy(mode, cidrs) | mode="allow": block the listed CIDRs. mode="deny": allow only the listed CIDRs |
add_ssh_keys(keys) | replace the full /root/.ssh/authorized_keys set |
Metrics: sbx.metrics() returns allocated resources plus live cpu/mem/disk. Live values are None until the first sample. sbx.usage(since_ms=None) returns cumulative billed usage.
Snapshots and forking
Disk snapshots and clones. See snapshots.
| Method | Notes |
|---|---|
snapshot(scrub_env=, clear_tmp=, clear_ssh_keys=) | point-in-time disk snapshot; the sandbox must run |
list_snapshots() / delete_snapshot(id) | snapshots of this sandbox |
restore(snapshot_id=None) | roll this sandbox back (latest when omitted) |
clone(snapshot_id=None) | boot a new sandbox from a snapshot; the source keeps running |
fork() | snapshot and clone in one call |
archive() / backup() / restore_from_backup() | cold storage |
Templates
Template builds a custom image. Use it inline in create(image=...), or build a named, reusable template. See templates and the declarative builder.
from platinum import Platinum, Template
client = Platinum()
image = (Template.from_python_image("3.12-slim")
.pip_install(["fastapi", "uvicorn"])
.workdir("/app"))
# Boot straight from the spec (cache-hit on repeat)
sbx = client.sandboxes.create(image=image, wait_for_running=True, wait_timeout_ms=600_000)
# Or build a reusable named template
result = image.build(client, name="my-agent")Start from from_image(), or a shortcut for Python, Node, Bun, Ubuntu, Debian, and Alpine. Chain steps to install packages, copy files, and set env, workdir, user, and entrypoint. Full step list: declarative builder.
build() waits until the build finishes. Pass wait_ms=0 to skip the wait and poll later.
Other client methods
| Method | |
|---|---|
client.volumes | create / list / get / delete / attach / detach (see volumes) |
client.webhooks | create / list / update / delete / test / deliveries / retry_delivery (see webhooks) |
client.templates | build / list / get / delete (see templates) |
client.regions.list() | regions that can place a sandbox now |
client.me() | identity and role of the current API key |
client.health.check() | API health ({status, ts}) |
client.request(method, path, ...) | escape hatch for any endpoint, with typed errors and timeouts |
client.close() | close the HTTP client |
API keys, audit, and billing have no typed wrapper. Call client.request() with routes from the API reference. The interactive terminal is a raw WebSocket; connect to it directly.
Errors
Every SDK exception subclasses PlatinumError. It carries .status, .body, and .code (the API's machine-readable code, also part of str(err)).
| Error | Raised when |
|---|---|
ValidationError | the request is invalid |
AuthenticationError | the API key is missing, invalid, or revoked |
ForbiddenError | the API key lacks the scope for that call |
NotFoundError | the sandbox, template, or volume does not exist |
ConflictError | the resource is in the wrong state for the call |
RateLimitError | too many requests; back off with .retry_after_seconds |
ServerError | the API failed |
PlatinumTimeoutError | a client deadline or a wait helper expired |
PlatinumConnectionError | the connection failed; the original error is on __cause__ |
The SDK does not retry. Handle RateLimitError yourself.
import time
from platinum import ConflictError, NotFoundError, RateLimitError
try:
sbx.exec("true")
except NotFoundError:
... # gone
except ConflictError:
sbx.start(wait_for_running=True) # wrong state, e.g. stopped
except RateLimitError as e:
time.sleep(e.retry_after_seconds or 1)Branch on .code for conflict detail, for example sandbox_not_running.