Skip to content

Python SDK

Terminal window
pip install zoo-sdk # or: pip install ./sdk/python
pip install 'zoo-sdk[cli]' # plus the zoo command
from zoo_sdk import Zoo
zoo = Zoo() # reads ZOO_API_KEY and ZOO_URL
with zoo.sandbox(kind="desktop") as box: # deleted on exit, also on error
for chunk in box.exec("uname -a", stream=True):
print(chunk.data, end="")

AsyncZoo is the asyncio twin (async with await zoo.sandbox(kind=...) as box). Every call has a timeout (30 s unless it passes timeout=) and is retried with exponential backoff when that is safe: connection failures and 429 always; 5xx and timeouts for reads and for creates, which carry an automatic Idempotency-Key. Errors are ZooError subclasses with the API’s error code. The TypeScript SDK (npm install @zoo/sdk) has the same methods in camelCase; see sdk/typescript/README.md.

AsyncZoo(self, api_key: str | None=None, base_url: str | None=None, *, timeout: float | None=None, max_retries: int | None=None, workspace: str | None=None, http_client: httpx.AsyncClient | None=None)
AsyncSandbox(self, client: AsyncZoo, data: SandboxData, owned: bool=False)
Method
id()
name()
kind()
status()
error_message()
labels()
exec()
exec()
exec()await box.exec(cmd) for the result, or async for chunk in box.exec(cmd, stream=True).
id(self) -> str
name(self) -> str
kind(self) -> str
status(self) -> str

AsyncSandbox.error_message {#asyncsandboxerror_message}

Section titled “AsyncSandbox.error_message {#asyncsandboxerror_message}”
error_message(self) -> str | None
labels(self) -> dict[str, str]
exec(self, command: str, timeout: int=30, *, stream: Literal[False]=False) -> 'asyncio.Future[ExecResult]'
exec(self, command: str, timeout: int=30, *, stream: Literal[True]) -> AsyncIterator[ExecChunk]

await box.exec(cmd) for the result, or async for chunk in box.exec(cmd, stream=True).

exec(self, command: str, timeout: int=30, *, stream: bool=False) -> 'asyncio.Future[ExecResult] | AsyncIterator[ExecChunk]'
Zoo(self, api_key: str | None=None, base_url: str | None=None, *, timeout: float | None=None, max_retries: int | None=None, workspace: str | None=None, http_client: httpx.Client | None=None)
Method
close()
request()Sends a request to /v1path, retrying when safe, and raises a ZooError subclass for an error response. A streamed response must be closed by the caller (or read to the end).
sandboxes()The workspace’s sandboxes, only those with all of labels when given.
iter_sandboxes()The workspace’s sandboxes, newest first, a page at a time.
get()
sandbox()
sandbox()
sandbox()sandbox(id) is an existing sandbox; sandbox(kind=...) creates one, which with deletes on exit:
create()Creates a sandbox and, by default, waits until it runs. If it fails to start it is deleted, and the error raised.
servers()
profiles()
tools()
events()Follows the workspace’s events (sandbox.state) as they happen, from after after (a seq) or from now. Pass the last event’s seq back to resume after a disconnect.
usage()Metered usage between two ISO times, by sandbox, day or a label (group_by=“label:end_user”).
close(self)

Sends a request to /v1path, retrying when safe, and raises a ZooError subclass for an error response. A streamed response must be closed by the caller (or read to the end).

request(self, method: str, path: str, *, json: Any=None, params: Any=None, content: bytes | None=None, headers: dict[str, str] | None=None, timeout: float | None=None, stream: bool=False) -> httpx.Response

The workspace’s sandboxes, only those with all of labels when given.

sandboxes(self, labels: dict[str, str] | None=None) -> list['Sandbox']

The workspace’s sandboxes, newest first, a page at a time.

iter_sandboxes(self, labels: dict[str, str] | None=None, page_size: int=100) -> Iterator['Sandbox']
get(self, sandbox_id: str) -> 'Sandbox'
sandbox(self, sandbox_id: str, /) -> 'Sandbox'
sandbox(self, sandbox_id: None=None, /, *, kind: SandboxKind='desktop', name: str | None=None, labels: dict[str, str] | None=None, server_id: str | None=None, profile_ids: list[str] | None=None, wait: bool=True, timeout: float=180) -> 'Sandbox'

sandbox(id) is an existing sandbox; sandbox(kind=...) creates one, which with deletes on exit:

sandbox(self, sandbox_id: str | None=None, /, **create: Any) -> 'Sandbox'

Creates a sandbox and, by default, waits until it runs. If it fails to start it is deleted, and the error raised.

create(self, name: str | None=None, kind: SandboxKind='desktop', server_id: str | None=None, profile_ids: list[str] | None=None, wait: bool=True, timeout: float=180, labels: dict[str, str] | None=None) -> 'Sandbox'
servers(self) -> list[dict]
profiles(self) -> list[dict]
tools(self) -> list[dict]

Follows the workspace’s events (sandbox.state) as they happen, from after after (a seq) or from now. Pass the last event’s seq back to resume after a disconnect.

events(self, after: int | None=None, sandbox_id: str | None=None, type: str | None=None) -> Iterator[dict]

Metered usage between two ISO times, by sandbox, day or a label (group_by=“label:end_user”).

usage(self, since: str, until: str, group_by: str='sandbox', metric: str | None=None) -> list[dict]

One sandbox. Tools without a method of their own are attributes: box.click(x=10, y=20) calls the click tool. Used with with, a sandbox this client created is deleted on exit.

Sandbox(self, client: Zoo, data: SandboxData, owned: bool=False)
Method
id()
name()
kind()
status()
server_id()
error_message()
labels()
refresh()
wait()Waits until the sandbox runs; raises if it fails or takes longer than timeout seconds.
tool()Calls a tool by name with its arguments; request_timeout (seconds) is for the HTTP call.
exec()
exec()
exec()Runs a shell command (at most timeout seconds). Returns its exit code and output, or with stream=True yields its output as it arrives, ending with an exit chunk.
claude()
screenshot()
viewer_link()A link to this sandbox’s screen for someone without a Zoo account: “view” only watches, “control” can type and click. Its “url” opens in a browser or an iframe; expires_in is in minutes.
set_labels()Replaces the sandbox’s labels.
executions()The latest tool calls on this sandbox, newest first.
hotkey()
start()
stop()Stops the sandbox. Stopping runs as a job on the server, so by default this waits until it has stopped.
delete()Deletes the sandbox. Deleting runs as a job on the server, so by default this waits until it is gone.
set_secret()
set_permission()
set_network()
add_rule()
read_file()A file from the sandbox (absolute, or relative to the home folder).
write_file()Writes a file into the sandbox, making its folder.
backup()Exports the home folder as a tar file; snapshots (snapshot()) are the faster way to keep a copy.
restore()
snapshots()
snapshot()Snapshots the home disk on the sandbox’s server (a VM must be stopped). Waits until it is ready.
restore_snapshot()Replaces the home disk with a snapshot’s. The sandbox must be stopped.
delete_snapshot()
move()
save_profile()Saves the app’s profile from this sandbox. Saving under an existing name (or a profile_id) adds a version.
apply_profile()Loads a profile, the latest version unless one is given. The app must not be running (409).
id(self) -> str
name(self) -> str
kind(self) -> str
status(self) -> str
server_id(self) -> str | None

Sandbox.error_message {#sandboxerror_message}

Section titled “Sandbox.error_message {#sandboxerror_message}”
error_message(self) -> str | None
labels(self) -> dict[str, str]
refresh(self) -> 'Sandbox'

Waits until the sandbox runs; raises if it fails or takes longer than timeout seconds.

wait(self, timeout: float=120) -> 'Sandbox'

Calls a tool by name with its arguments; request_timeout (seconds) is for the HTTP call.

tool(self, name: str, request_timeout: float | None=None, **args: Any) -> Any
exec(self, command: str, timeout: int=30, *, stream: Literal[False]=False) -> ExecResult
exec(self, command: str, timeout: int=30, *, stream: Literal[True]) -> Iterator[ExecChunk]

Runs a shell command (at most timeout seconds). Returns its exit code and output, or with stream=True yields its output as it arrives, ending with an exit chunk.

exec(self, command: str, timeout: int=30, *, stream: bool=False) -> ExecResult | Iterator[ExecChunk]
claude(self, prompt: str, cwd: str='~/work', timeout: int=900, args: str='') -> dict
screenshot(self) -> bytes

A link to this sandbox’s screen for someone without a Zoo account: “view” only watches, “control” can type and click. Its “url” opens in a browser or an iframe; expires_in is in minutes.

viewer_link(self, mode: str='view', expires_in: int=60, end_user: str | None=None) -> dict

Replaces the sandbox’s labels.

set_labels(self, labels: dict[str, str]) -> 'Sandbox'

The latest tool calls on this sandbox, newest first.

executions(self, limit: int=50) -> list[dict]
hotkey(self, *keys: str) -> dict
start(self, wait: bool=True) -> 'Sandbox'

Stops the sandbox. Stopping runs as a job on the server, so by default this waits until it has stopped.

stop(self, wait: bool=True, timeout: float=120) -> 'Sandbox'

Deletes the sandbox. Deleting runs as a job on the server, so by default this waits until it is gone.

delete(self, wait: bool=True, timeout: float=120)
set_secret(self, name: str, value: str) -> dict

Sandbox.set_permission {#sandboxset_permission}

Section titled “Sandbox.set_permission {#sandboxset_permission}”
set_permission(self, permission: str, action: str, effect: str) -> dict
set_network(self, default_action: str='allow', allow_dns: bool=True) -> dict
add_rule(self, rule_type: str, value: str, effect: str='allow') -> dict

A file from the sandbox (absolute, or relative to the home folder).

read_file(self, path: str, timeout: float | None=300) -> bytes

Writes a file into the sandbox, making its folder.

write_file(self, path: str, data: bytes, timeout: float | None=300) -> dict

Exports the home folder as a tar file; snapshots (snapshot()) are the faster way to keep a copy.

backup(self, path: str)
restore(self, path: str)
snapshots(self) -> list[dict]

Snapshots the home disk on the sandbox’s server (a VM must be stopped). Waits until it is ready.

snapshot(self, name: str='', wait: bool=True, timeout: float=1800) -> dict

Sandbox.restore_snapshot {#sandboxrestore_snapshot}

Section titled “Sandbox.restore_snapshot {#sandboxrestore_snapshot}”

Replaces the home disk with a snapshot’s. The sandbox must be stopped.

restore_snapshot(self, snapshot_id: str, wait: bool=True, timeout: float=1800) -> 'Sandbox'

Sandbox.delete_snapshot {#sandboxdelete_snapshot}

Section titled “Sandbox.delete_snapshot {#sandboxdelete_snapshot}”
delete_snapshot(self, snapshot_id: str)
move(self, server_id: str | None) -> 'Sandbox'

Sandbox.save_profile {#sandboxsave_profile}

Section titled “Sandbox.save_profile {#sandboxsave_profile}”

Saves the app’s profile from this sandbox. Saving under an existing name (or a profile_id) adds a version.

save_profile(self, name: str, app: str='firefox', profile_id: str | None=None) -> dict

Sandbox.apply_profile {#sandboxapply_profile}

Section titled “Sandbox.apply_profile {#sandboxapply_profile}”

Loads a profile, the latest version unless one is given. The app must not be running (409).

apply_profile(self, profile_id: str, version: int | None=None) -> dict

An error from the Zoo API, or the SDK giving up on a request.

ZooError(self, message: str, *, status: int | None=None, code: str='error', hint: str | None=None, docs_url: str | None=None, request_id: str | None=None)

401: no or invalid API key.

403: the key’s role or scope, or a sandbox policy, doesn’t allow it.

404: no such sandbox (or other object) in this workspace.

409: the object’s state doesn’t allow it, a quota is used up, or an idempotency key clashed.

422: a field didn’t match the schema.

429: too many requests; retried automatically before this is raised.

5xx: Zoo, or a sandbox it talks to, failed.

The server couldn’t be reached.

The server didn’t answer within the timeout.

A piece of a streamed command: output as it arrives (stdout/stderr, with data), then exit.

Method
timed_out()
timed_out(self) -> bool

Any non-2xx response raises zoo_sdk.ZooError with the status code and the API’s stable code, message, hint, docs_url and request_id; see Errors. Any tool can be called as a method (box.window_focus(window_id=...)) or with box.tool(name, **args).