Python SDK
pip install zoo-sdk # or: pip install ./sdk/pythonpip install 'zoo-sdk[cli]' # plus the zoo commandfrom zoo_sdk import Zoo
zoo = Zoo() # reads ZOO_API_KEY and ZOO_URLwith 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.
zoo_sdk.aio.AsyncZoo
Section titled “zoo_sdk.aio.AsyncZoo”AsyncZoo.__init__
Section titled “AsyncZoo.__init__”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)zoo_sdk.aio.AsyncSandbox
Section titled “zoo_sdk.aio.AsyncSandbox”AsyncSandbox.__init__
Section titled “AsyncSandbox.__init__”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). |
AsyncSandbox.id {#asyncsandboxid}
Section titled “AsyncSandbox.id {#asyncsandboxid}”id(self) -> strAsyncSandbox.name {#asyncsandboxname}
Section titled “AsyncSandbox.name {#asyncsandboxname}”name(self) -> strAsyncSandbox.kind {#asyncsandboxkind}
Section titled “AsyncSandbox.kind {#asyncsandboxkind}”kind(self) -> strAsyncSandbox.status {#asyncsandboxstatus}
Section titled “AsyncSandbox.status {#asyncsandboxstatus}”status(self) -> strAsyncSandbox.error_message {#asyncsandboxerror_message}
Section titled “AsyncSandbox.error_message {#asyncsandboxerror_message}”error_message(self) -> str | NoneAsyncSandbox.labels {#asyncsandboxlabels}
Section titled “AsyncSandbox.labels {#asyncsandboxlabels}”labels(self) -> dict[str, str]AsyncSandbox.exec {#asyncsandboxexec}
Section titled “AsyncSandbox.exec {#asyncsandboxexec}”exec(self, command: str, timeout: int=30, *, stream: Literal[False]=False) -> 'asyncio.Future[ExecResult]'AsyncSandbox.exec {#asyncsandboxexec}
Section titled “AsyncSandbox.exec {#asyncsandboxexec}”exec(self, command: str, timeout: int=30, *, stream: Literal[True]) -> AsyncIterator[ExecChunk]AsyncSandbox.exec {#asyncsandboxexec}
Section titled “AsyncSandbox.exec {#asyncsandboxexec}”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_sdk.client.Zoo
Section titled “zoo_sdk.client.Zoo”Zoo.__init__
Section titled “Zoo.__init__”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”). |
Zoo.close {#zooclose}
Section titled “Zoo.close {#zooclose}”close(self)Zoo.request {#zoorequest}
Section titled “Zoo.request {#zoorequest}”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.ResponseZoo.sandboxes {#zoosandboxes}
Section titled “Zoo.sandboxes {#zoosandboxes}”The workspace’s sandboxes, only those with all of labels when given.
sandboxes(self, labels: dict[str, str] | None=None) -> list['Sandbox']Zoo.iter_sandboxes {#zooiter_sandboxes}
Section titled “Zoo.iter_sandboxes {#zooiter_sandboxes}”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']Zoo.get {#zooget}
Section titled “Zoo.get {#zooget}”get(self, sandbox_id: str) -> 'Sandbox'Zoo.sandbox {#zoosandbox}
Section titled “Zoo.sandbox {#zoosandbox}”sandbox(self, sandbox_id: str, /) -> 'Sandbox'Zoo.sandbox {#zoosandbox}
Section titled “Zoo.sandbox {#zoosandbox}”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'Zoo.sandbox {#zoosandbox}
Section titled “Zoo.sandbox {#zoosandbox}”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'Zoo.create {#zoocreate}
Section titled “Zoo.create {#zoocreate}”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'Zoo.servers {#zooservers}
Section titled “Zoo.servers {#zooservers}”servers(self) -> list[dict]Zoo.profiles {#zooprofiles}
Section titled “Zoo.profiles {#zooprofiles}”profiles(self) -> list[dict]Zoo.tools {#zootools}
Section titled “Zoo.tools {#zootools}”tools(self) -> list[dict]Zoo.events {#zooevents}
Section titled “Zoo.events {#zooevents}”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]Zoo.usage {#zoousage}
Section titled “Zoo.usage {#zoousage}”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]zoo_sdk.client.Sandbox
Section titled “zoo_sdk.client.Sandbox”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.__init__
Section titled “Sandbox.__init__”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). |
Sandbox.id {#sandboxid}
Section titled “Sandbox.id {#sandboxid}”id(self) -> strSandbox.name {#sandboxname}
Section titled “Sandbox.name {#sandboxname}”name(self) -> strSandbox.kind {#sandboxkind}
Section titled “Sandbox.kind {#sandboxkind}”kind(self) -> strSandbox.status {#sandboxstatus}
Section titled “Sandbox.status {#sandboxstatus}”status(self) -> strSandbox.server_id {#sandboxserver_id}
Section titled “Sandbox.server_id {#sandboxserver_id}”server_id(self) -> str | NoneSandbox.error_message {#sandboxerror_message}
Section titled “Sandbox.error_message {#sandboxerror_message}”error_message(self) -> str | NoneSandbox.labels {#sandboxlabels}
Section titled “Sandbox.labels {#sandboxlabels}”labels(self) -> dict[str, str]Sandbox.refresh {#sandboxrefresh}
Section titled “Sandbox.refresh {#sandboxrefresh}”refresh(self) -> 'Sandbox'Sandbox.wait {#sandboxwait}
Section titled “Sandbox.wait {#sandboxwait}”Waits until the sandbox runs; raises if it fails or takes longer than timeout seconds.
wait(self, timeout: float=120) -> 'Sandbox'Sandbox.tool {#sandboxtool}
Section titled “Sandbox.tool {#sandboxtool}”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) -> AnySandbox.exec {#sandboxexec}
Section titled “Sandbox.exec {#sandboxexec}”exec(self, command: str, timeout: int=30, *, stream: Literal[False]=False) -> ExecResultSandbox.exec {#sandboxexec}
Section titled “Sandbox.exec {#sandboxexec}”exec(self, command: str, timeout: int=30, *, stream: Literal[True]) -> Iterator[ExecChunk]Sandbox.exec {#sandboxexec}
Section titled “Sandbox.exec {#sandboxexec}”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]Sandbox.claude {#sandboxclaude}
Section titled “Sandbox.claude {#sandboxclaude}”claude(self, prompt: str, cwd: str='~/work', timeout: int=900, args: str='') -> dictSandbox.screenshot {#sandboxscreenshot}
Section titled “Sandbox.screenshot {#sandboxscreenshot}”screenshot(self) -> bytesSandbox.viewer_link {#sandboxviewer_link}
Section titled “Sandbox.viewer_link {#sandboxviewer_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.
viewer_link(self, mode: str='view', expires_in: int=60, end_user: str | None=None) -> dictSandbox.set_labels {#sandboxset_labels}
Section titled “Sandbox.set_labels {#sandboxset_labels}”Replaces the sandbox’s labels.
set_labels(self, labels: dict[str, str]) -> 'Sandbox'Sandbox.executions {#sandboxexecutions}
Section titled “Sandbox.executions {#sandboxexecutions}”The latest tool calls on this sandbox, newest first.
executions(self, limit: int=50) -> list[dict]Sandbox.hotkey {#sandboxhotkey}
Section titled “Sandbox.hotkey {#sandboxhotkey}”hotkey(self, *keys: str) -> dictSandbox.start {#sandboxstart}
Section titled “Sandbox.start {#sandboxstart}”start(self, wait: bool=True) -> 'Sandbox'Sandbox.stop {#sandboxstop}
Section titled “Sandbox.stop {#sandboxstop}”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'Sandbox.delete {#sandboxdelete}
Section titled “Sandbox.delete {#sandboxdelete}”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)Sandbox.set_secret {#sandboxset_secret}
Section titled “Sandbox.set_secret {#sandboxset_secret}”set_secret(self, name: str, value: str) -> dictSandbox.set_permission {#sandboxset_permission}
Section titled “Sandbox.set_permission {#sandboxset_permission}”set_permission(self, permission: str, action: str, effect: str) -> dictSandbox.set_network {#sandboxset_network}
Section titled “Sandbox.set_network {#sandboxset_network}”set_network(self, default_action: str='allow', allow_dns: bool=True) -> dictSandbox.add_rule {#sandboxadd_rule}
Section titled “Sandbox.add_rule {#sandboxadd_rule}”add_rule(self, rule_type: str, value: str, effect: str='allow') -> dictSandbox.read_file {#sandboxread_file}
Section titled “Sandbox.read_file {#sandboxread_file}”A file from the sandbox (absolute, or relative to the home folder).
read_file(self, path: str, timeout: float | None=300) -> bytesSandbox.write_file {#sandboxwrite_file}
Section titled “Sandbox.write_file {#sandboxwrite_file}”Writes a file into the sandbox, making its folder.
write_file(self, path: str, data: bytes, timeout: float | None=300) -> dictSandbox.backup {#sandboxbackup}
Section titled “Sandbox.backup {#sandboxbackup}”Exports the home folder as a tar file; snapshots (snapshot()) are the faster way to keep a copy.
backup(self, path: str)Sandbox.restore {#sandboxrestore}
Section titled “Sandbox.restore {#sandboxrestore}”restore(self, path: str)Sandbox.snapshots {#sandboxsnapshots}
Section titled “Sandbox.snapshots {#sandboxsnapshots}”snapshots(self) -> list[dict]Sandbox.snapshot {#sandboxsnapshot}
Section titled “Sandbox.snapshot {#sandboxsnapshot}”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) -> dictSandbox.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)Sandbox.move {#sandboxmove}
Section titled “Sandbox.move {#sandboxmove}”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) -> dictSandbox.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) -> dictzoo_sdk.errors.ZooError
Section titled “zoo_sdk.errors.ZooError”An error from the Zoo API, or the SDK giving up on a request.
ZooError.__init__
Section titled “ZooError.__init__”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)zoo_sdk.errors.AuthenticationError
Section titled “zoo_sdk.errors.AuthenticationError”401: no or invalid API key.
zoo_sdk.errors.PermissionDeniedError
Section titled “zoo_sdk.errors.PermissionDeniedError”403: the key’s role or scope, or a sandbox policy, doesn’t allow it.
zoo_sdk.errors.NotFoundError
Section titled “zoo_sdk.errors.NotFoundError”404: no such sandbox (or other object) in this workspace.
zoo_sdk.errors.ConflictError
Section titled “zoo_sdk.errors.ConflictError”409: the object’s state doesn’t allow it, a quota is used up, or an idempotency key clashed.
zoo_sdk.errors.ValidationError
Section titled “zoo_sdk.errors.ValidationError”422: a field didn’t match the schema.
zoo_sdk.errors.RateLimitError
Section titled “zoo_sdk.errors.RateLimitError”429: too many requests; retried automatically before this is raised.
zoo_sdk.errors.ServerError
Section titled “zoo_sdk.errors.ServerError”5xx: Zoo, or a sandbox it talks to, failed.
zoo_sdk.errors.APIConnectionError
Section titled “zoo_sdk.errors.APIConnectionError”The server couldn’t be reached.
zoo_sdk.errors.APITimeoutError
Section titled “zoo_sdk.errors.APITimeoutError”The server didn’t answer within the timeout.
zoo_sdk.types.SandboxData
Section titled “zoo_sdk.types.SandboxData”zoo_sdk.types.ExecResult
Section titled “zoo_sdk.types.ExecResult”zoo_sdk.types.ExecChunk
Section titled “zoo_sdk.types.ExecChunk”A piece of a streamed command: output as it arrives (stdout/stderr, with data), then exit.
| Method | |
|---|---|
timed_out() |
ExecChunk.timed_out {#execchunktimed_out}
Section titled “ExecChunk.timed_out {#execchunktimed_out}”timed_out(self) -> boolErrors
Section titled “Errors”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).