TypeScript SDK
The TypeScript SDK wraps the REST API and the WebSocket behind typed resources. It is async only, ESM only, and has no runtime dependencies.
Install
Code
The package is @0bull/sdk on npm and needs Node
22 or newer. Source and changelog:
github.com/0bull/0bull-typescript-sdk.
This page documents 0.2.0.
Upgrading from 0.1.0: a phone-controller session phone is keyed slot, not id.
Methods are camelCase. Fields sent to and returned by the API keep their snake_case names, so the types match the API reference one for one. Every duration is in milliseconds.
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 throws PermissionDeniedError.
| Option | Default | Environment |
|---|---|---|
apiToken | required | ZEROBULL_API_TOKEN |
baseURL | https://0bull.net/api | ZEROBULL_BASE_URL |
timeout | 30000 | |
maxRetries | 2 | |
fetch | the global fetch |
First call
Code
phones.list() resolves to 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.
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 promise resolves, 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. The union is
exported as Hotkey, and the list as HOTKEYS. An unknown key throws TypeError before any
request is sent.
Runs
Commands, macros and agent tasks each queue a Run and resolve with it right away:
Code
runCommand 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).
runMacro takes exactly one of workflow (with optional scalar params) or steps, up to
200 steps. runAgent 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
resolves 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 null, so check status rather than the result.
TERMINAL_RUN_STATUSES is exported if you need to test a run you already hold.
wait() throws WaitTimeoutError once timeout passes. Defaults are 300000ms with a 2000ms
interval.
Read the history with runs.list(), which resolves to a Page. for await walks that page
and every page after it:
Code
Or page by hand with nextPage(), which returns null on the last page.
page.currentPage, page.lastPage, page.perPage and page.total describe where you are.
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 throws TypeError. update
changes only the fields you pass, and null clears one.
Submissions and uploads
Code
Pass exactly one of video (a local path, a Uint8Array, or a Blob), 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() resolves once the status is published, drafted, failed or cancelled, and
throws WaitTimeoutError otherwise. cancel(id) stops one that is still running, and
delete(id) removes it and its stored video.
Billing
Code
startRental returns a checkout_url for the user to open. requestPhoneCount applies
straight away when it fits your standing allowance, otherwise it returns an approval_url
and stays pending until someone acts on it. Poll getRequest 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 throws TypeError.
Events over WebSocket
client.socket() gives you the same resource methods over one connection that also pushes
status events, so you can stop polling. The socket starts unconnected, so call connect():
Code
events() yields a union discriminated on type: { type: "run", run },
{ type: "submission", submission } and { type: "billing_request", request }.
The socket reconnects on its own after an unexpected drop; calls that were in flight throw
SocketClosedError rather than being resent. user and session are REST only. A local
video works over the socket too: the SDK uploads it through a signed URL first and sends the
resulting upload_id. Use socket.call(fun, data) for a fun the SDK does not
model yet.
On TypeScript 5.2 or newer with lib: "esnext", or Node 24, await using socket = client.socket() closes the socket when the scope exits and replaces the try block above.
Errors
Everything the SDK throws on purpose subclasses ZeroBullError.
| Condition | Exception |
|---|---|
| 400 | BadRequestError |
| 401 | AuthenticationError |
| 403 | PermissionDeniedError |
| 404 | NotFoundError |
| 409 | ConflictError |
| 422 | ValidationError, with .errors |
| 429 | RateLimitError, with .retryAfter |
| 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 throw TypeError before any request goes out. Only a
request that reached the server can throw ValidationError.
Rate limits
The API allows 250 requests a minute, and 60 a minute for ocr. A 429 is retried
automatically up to maxRetries, honoring Retry-After, before RateLimitError is thrown.
Nothing else is retried.

