Catalog PyPI
PACKAGE EXPLORERView in registry

0bull

PyPI
0bullapi-clientphone-automationsdkvideowebsocket
Versions collected1
Direct dependencies3From release 0.3.0
Last observed20 Oct, 15:01 UTC

README

Version 0.3.0 documentation

0bull

0bull Python SDK

A typed Python client for the 0bull API: manage posting accounts, publish videos, and control phones from Python. See 0bull.net for the product and docs.0bull.net for the full API reference.

Install

pip install 0bull
# or
uv add 0bull

The package is imported as zerobull and requires Python 3.10+.

Authentication

Create a token at 0bull.net/settings/api-tokens, scoped to the abilities your code needs:

AbilityGrants
phones:readList phones, snapshots, OCR
phones:controlTap, swipe, hotkey, type, runs
accountsList, create, update, delete posting accounts
submissionsList, create, cancel, delete submissions; uploads
billingBilling summary, rentals, phone count requests

Pass the token explicitly or set ZEROBULL_API_TOKEN:

from zerobull import ZeroBull

client = ZeroBull(api_token="...")
# or, with ZEROBULL_API_TOKEN set in the environment:
client = ZeroBull()

A request made with a token missing the required ability raises PermissionDeniedError.

Quickstart (sync)

from zerobull import ZeroBull

with ZeroBull() as client:
    phones = client.phones.list()
    print(phones[0].slot, phones[0].name)

Quickstart (async)

import asyncio
from zerobull import AsyncZeroBull


async def main() -> None:
    async with AsyncZeroBull() as client:
        phones = await client.phones.list()
        print(phones[0].slot, phones[0].name)


asyncio.run(main())

Phones

Coordinates for tap, swipe, and run_command's brightness level are 0-1 fractions of the screen; snapshot and ocr accept an optional width between 120 and 2000. hotkey accepts home, app_switcher, control_center, notifications, back, run_shortcut, enter, backspace, copy, cut, paste, and select_all.

phones = client.phones.list()
slot = phones[0].slot

jpeg: bytes = client.phones.snapshot(slot, width=600)
text: str = client.phones.ocr(slot)

client.phones.tap(slot, fx=0.5, fy=0.9)
client.phones.swipe(slot, fx1=0.5, fy1=0.8, fx2=0.5, fy2=0.2, steps=30)
client.phones.type(slot, "hello")
client.phones.hotkey(slot, "enter")

Runs

Commands, macros, and agent tasks all queue a Run:

run = client.phones.run_command(slot, "brightness", level=0.5)
run = client.phones.run_macro(slot, workflow="post-to-story", params={"caption": "hi"})
run = client.phones.run_agent(slot, "Open Settings and turn on Wi-Fi")

run = client.runs.wait(run, timeout=300)
print(run.status, run.result)

for run in client.runs.list(slot).iter_all():
    print(run.id, run.status)

wait() polls until the run reaches a terminal status (succeeded, failed, cancelled) and raises WaitTimeoutError if timeout elapses first.

Accounts and submissions

from pathlib import Path

account = client.accounts.create(platform="tiktok", handle="@me", slot=slot)

submission = client.submissions.create(account_id=account.id, video=Path("clip.mp4"), caption="hi")
# or video_url="https://...", or upload_id from client.uploads.upload(...)

submission = client.submissions.wait(submission, timeout=900)
client.submissions.cancel(submission.id)
client.accounts.delete(account.id)

for account in client.accounts.list().iter_all():
    print(account.id, account.handle)

submissions.create takes exactly one of video (a local file, ≤200 MB, MP4/MOV), video_url, or upload_id. submissions.wait() polls until a terminal status (published, drafted, failed, cancelled).

.list() returns a Page; iter_all() walks every page, sync or async, without manual paging.

Uploads

Upload a local file once and reuse the resulting id, for example to submit the same video to several accounts:

upload_id = client.uploads.upload("clip.mp4")
submission = client.submissions.create(account_id=account.id, upload_id=upload_id, caption="hi")

Billing

summary = client.billing.summary()
rental = client.billing.start_rental(accept_terms=True, phones=5, country="US")
change = client.billing.request_phone_count(accept_terms=True, add=2)
request = client.billing.get_request(change.request_id)

accept_terms=True confirms the caller has shown the user the terms and privacy policy, including that the rental renews monthly, before the charge is made.

WebSocket

client.socket() returns a Socket (or AsyncSocket) exposing the same resource methods as the REST client, over a connection that's authenticated and reconnected automatically:

from zerobull import RunEvent

with client.socket() as socket:
    socket.phones.tap(slot, fx=0.5, fy=0.5)
    run = socket.phones.run_macro(slot, workflow="post-to-story")
    for event in socket.events():
        if isinstance(event, RunEvent) and event.run.id == run.id and event.run.is_terminal:
            break
async with client.socket() as socket:
    phones = await socket.phones.list()
    async for event in socket.events():
        ...

events() yields RunEvent, SubmissionEvent, and BillingRequestEvent. A local file passed to submissions.create over the socket is uploaded through a signed URL on the owning client's HTTP transport first, then sent as upload_id. user, session, and any request carrying a local file upload are REST-only and raise NotImplementedError on the socket.

On an unexpected disconnect, the socket reconnects with a fresh session URL and exponential backoff, up to max_reconnect_attempts; calls in flight at the time of the disconnect raise SocketClosedError rather than being resent. Use socket.call(fun, data) to invoke a fun the SDK doesn't model yet; it gets the same error mapping as every other call.

Errors

All exceptions subclass ZeroBullError.

ConditionException
400BadRequestError
401AuthenticationError
403PermissionDeniedError
404NotFoundError
409ConflictError
422ValidationError (with .errors)
429RateLimitError (after retries, with .retry_after)
502, 503UnavailableError
≥ 500 otherwiseInternalServerError
any other statusAPIStatusError (base of all status errors)
network failureAPIConnectionError
request or call timeoutAPITimeoutError
socket closed with calls in flightSocketClosedError
wait() timeoutWaitTimeoutError
from zerobull import APIStatusError, ValidationError

try:
    client.submissions.create(
        account_id=account.id, video_url="https://example.com/missing.mp4", caption="hi"
    )
except ValidationError as e:
    print(e.errors)
except APIStatusError as e:
    print(e.status, e.message)

Arguments the SDK can validate locally, such as a missing google_email on a YouTube account, raise ValueError before any request is sent; only a request that reaches the server can raise ValidationError or another APIStatusError.

A REST 429 is retried automatically, honoring Retry-After, up to max_retries before RateLimitError is raised. Nothing else is retried automatically.

Configuration

ZeroBull / AsyncZeroBull:

OptionDefaultEnvironment
api_tokenrequiredZEROBULL_API_TOKEN
base_urlhttps://0bull.net/apiZEROBULL_BASE_URL
timeout30s
max_retries (REST 429)2
http_clientnone; an injected httpx.Client / httpx.AsyncClient

client.socket():

OptionDefault
call_timeout60s
auto_reconnectTrue
max_reconnect_attempts5
ping_interval20s

Versioning and support

This SDK follows semantic versioning. Before 1.0.0, minor versions may include breaking changes. See CHANGELOG.md for what changed in each release.

Development

uv sync
uv run ruff check .
uv run ruff format .
uv run mypy
uv run pytest

License

MIT

Downloads & popularity

Awaiting observations

No metric observations yet

Download counts and popularity will appear here as the collector receives them.

Versions 1

Collected releases
VersionPublishedDirect dependenciesOpen version
0.3.018 Sept 20263

Package advisories

0
No matching advisories collected

This describes our collected data; it is not a security assessment.

Repository

PENDING
0bull/0bull-python-sdk

Shared repository data · GitHub

License detected in repository
Not collected

Metadata history

Meaningful changes
Current collected state Current

Observed since 20 Oct, 15:01 UTC

No superseded metadata states collected. Unchanged observations do not create new revisions.

Record identity

package:a383b73ecf83da7036dd32d60631019ae68b7b0ffd4a8979cb45d8b232139f93