Python SDK
The Python SDK wraps the REST API and the WebSocket behind typed resources. It ships sync and async clients with the same methods on both.
Install
Code
The package is 0bull on PyPI and imports as
zerobull. It needs Python 3.10 or newer. Source and changelog:
github.com/0bull/0bull-python-sdk.
This page documents 0.3.0.
Upgrading from 0.2.0: a phone-controller session phone is keyed slot, not id, and
ice_servers is gone from the session. Live video is not part of the API.
Authenticate
Create a token at 0bull.net/settings/api-tokens, scoped to the abilities your code needs. The ability table lists what each one grants.
Pass the token, or set ZEROBULL_API_TOKEN and pass nothing:
Code
A call made with a token that lacks the ability raises PermissionDeniedError.
| Option | Default | Environment |
|---|---|---|
api_token | required | ZEROBULL_API_TOKEN |
base_url | https://0bull.net/api | ZEROBULL_BASE_URL |
timeout | 30 seconds | |
max_retries | 2 | |
http_client | none; inject your own httpx.Client |
First call
Code
phones.list() returns Phone objects with slot, name, video_live, input_present,
can_control, model and os_version. The slot is the UUID every other phone method
takes. The name is a label, and passing it where a slot is wanted matches no phone.
The same code with the async client:
Code
Every method below exists on both clients with the same name and arguments. Await it on
AsyncZeroBull.
Phones
Coordinates are fractions of the screen, 0 to 1. snapshot and ocr take an optional
width between 120 and 2000.
Code
Input runs on the phone before the call returns, so there is nothing to poll.
hotkey accepts home, app_switcher, control_center, notifications, back,
run_shortcut, enter, backspace, copy, cut, paste and select_all. An unknown key
raises ValueError before any request is sent.
Runs
Commands, macros and agent tasks each queue a Run and return it right away:
Code
run_command ops are clipboard_set (needs text), clipboard_get, open_url (needs
url), reboot, clear_photos, get_ip, brightness (needs level, 0 to 1), and
wifi, airplane, cellular and flashlight (each needs on).
run_macro takes exactly one of workflow (with optional scalar params) or steps, up to
200 steps. run_agent takes a task of 1 to 2000 characters.
Waiting on a run
The run comes back before the phone has done the work. Poll it with runs.wait(), which
returns once the status is succeeded, failed or cancelled:
Code
get_ip and clipboard_get put what the phone reported in result["value"]. Every other
command finishes with result set to None, so check run.succeeded rather than the
result.
wait() raises WaitTimeoutError if timeout seconds pass first. Defaults are 300 seconds
with a 2 second interval. run.is_terminal tells you whether a run you already hold is
finished.
Read the history with runs.list(), which returns a Page. Call iter_all() to walk every
page:
Code
Accounts
Code
platform defaults to tiktok. TikTok accounts need a slot, YouTube accounts need a
google_email, and passing google_email on any other platform raises ValueError.
update changes only the fields you pass.
Submissions and uploads
Code
Pass exactly one of video (a path, bytes, or an open file), video_url, or upload_id.
On YouTube the caption is the Short's title, so it is required there and capped at 100
characters instead of 2200.
To post the same video to several accounts, upload it once and reuse the id:
Code
Publishing happens on a real phone after the call returns, so wait for it:
Code
wait() returns once the status is published, drafted, failed or cancelled, and
raises WaitTimeoutError otherwise. cancel(id) stops one that is still running, and
delete(id) removes it and its stored video.
Billing
Code
start_rental returns a checkout_url for the user to open. request_phone_count applies
straight away when it fits your standing allowance, otherwise it returns an approval_url
and stays pending until someone acts on it. Poll get_request for the answer.
accept_terms=True confirms you showed the user the terms and
privacy policy, including that a rental renews monthly, before
the charge. False raises ValueError.
Events over WebSocket
client.socket() gives you the same resource methods over one connection that also pushes
status events, so you can stop polling:
Code
events() yields RunEvent, SubmissionEvent and BillingRequestEvent. The socket
reconnects on its own after an unexpected drop; calls that were in flight raise
SocketClosedError rather than being resent. user, session and any call carrying a local
file raise NotImplementedError on the socket. Use socket.call(fun, data) for a
fun the SDK does not model yet.
Errors
Everything the SDK raises on purpose subclasses ZeroBullError.
| Condition | Exception |
|---|---|
| 400 | BadRequestError |
| 401 | AuthenticationError |
| 403 | PermissionDeniedError |
| 404 | NotFoundError |
| 409 | ConflictError |
| 422 | ValidationError, with .errors |
| 429 | RateLimitError, with .retry_after |
| 502, 503 | UnavailableError |
| Any other 5xx | InternalServerError |
| Any other status | APIStatusError |
| Network failure | APIConnectionError |
| Timeout | APITimeoutError |
| Socket closed mid-call | SocketClosedError |
wait() timed out | WaitTimeoutError |
Code
Arguments the SDK can check itself raise ValueError before any request goes out. Only a
request that reached the server can raise ValidationError.
Rate limits
The API allows 250 requests a minute, and 60 a minute for ocr. A 429 is retried
automatically up to max_retries, honoring Retry-After, before RateLimitError is raised.
Nothing else is retried.

