Observed since 20 Oct, 22:05 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
Typed TypeScript client for the 0bull API
A typed TypeScript client for the 0bull API: manage posting accounts, publish videos, and control phones from Node. See 0bull.net for the product and docs.0bull.net for the full API reference.
npm install @0bull/sdk
The package is published as @0bull/sdk, ESM only, and requires Node 22+. It has zero runtime
dependencies (it uses the global fetch, FormData, Blob, and WebSocket).
import { ZeroBull } from "@0bull/sdk";
Methods are camelCase (runMacro); fields sent to or returned by the API keep the API's
snake_case names (account_id, video_url), so the types mirror the API docs directly. Durations
(timeout, interval, callTimeout, ...) are always milliseconds.
Create a token at 0bull.net/settings/api-tokens, scoped to the abilities your code needs:
| Ability | Grants |
|---|---|
phones:read | List phones, snapshots, OCR |
phones:control | Tap, swipe, hotkey, type, runs |
accounts | List, create, update, delete posting accounts |
submissions | List, create, cancel, delete submissions; uploads |
billing | Billing summary, rentals, phone count requests |
Pass the token explicitly or set ZEROBULL_API_TOKEN:
import { ZeroBull } from "@0bull/sdk";
const client = new ZeroBull({ apiToken: "..." });
// or, with ZEROBULL_API_TOKEN set in the environment:
const client = new ZeroBull();
A request made with a token missing the required ability throws PermissionDeniedError.
The SDK is async-only. ZeroBull is the REST client; client.socket() returns a Socket for the
WebSocket (see The socket and events).
import { ZeroBull } from "@0bull/sdk";
const client = new ZeroBull();
const phones = await client.phones.list();
const phone = phones[0];
if (phone) console.log(`${phone.slot}: ${phone.name}`);
Coordinates for tap, swipe, and runCommand's brightness level are 0-1 fractions of the
screen; snapshot and ocr accept an optional width between 120 and 2000.
const phones = await client.phones.list();
const slot = phones[0]!.slot;
const jpeg: Uint8Array = await client.phones.snapshot(slot, { width: 600 });
const text: string = await client.phones.ocr(slot);
await client.phones.tap(slot, { fx: 0.5, fy: 0.9 });
await client.phones.swipe(slot, { fx1: 0.5, fy1: 0.8, fx2: 0.5, fy2: 0.2, steps: 30 });
await client.phones.type(slot, "hello");
await client.phones.hotkey(slot, "enter");
Commands, macros, and agent tasks all queue a Run:
let run = await client.phones.runCommand(slot, "brightness", { level: 0.5 });
run = await client.phones.runMacro(slot, { workflow: "post-to-story", params: { caption: "hi" } });
run = await client.phones.runAgent(slot, "Open Settings and turn on Wi-Fi");
run = await client.runs.wait(run, { timeout: 300_000 });
console.log(run.status, run.result);
for await (const run of await client.runs.list(slot)) {
console.log(run.id, run.status);
}
wait() polls until the run reaches a terminal status (succeeded, failed, cancelled) and
throws WaitTimeoutError if timeout elapses first.
const account = await client.accounts.create({ handle: "@me", platform: "tiktok", slot });
// google_email is required for, and only valid on, youtube accounts; slot is required for tiktok
await client.accounts.update(account.id, { notes: "test" });
await client.accounts.delete(account.id);
for await (const account of await client.accounts.list()) {
console.log(account.id, account.handle);
}
const submission = await client.submissions.create({
account_id: account.id,
video: "clip.mp4", // a local path, a Uint8Array, or a Blob
caption: "hi",
});
// or video_url: "https://...", or upload_id from client.uploads.upload(...)
const finished = await client.submissions.wait(submission, { timeout: 900_000 });
await client.submissions.cancel(submission.id);
await client.submissions.delete(submission.id);
submissions.create takes exactly one of video (a local file, Uint8Array, or Blob, MP4/MOV),
video_url, or upload_id. submissions.wait() polls until a terminal status (published,
drafted, failed, cancelled).
.list() returns a Page; see Pagination for walking every page.
Upload a video once and reuse the resulting id, for example to submit the same video to several accounts:
const uploadId = await client.uploads.upload("clip.mp4");
const submission = await client.submissions.create({
account_id: account.id,
upload_id: uploadId,
caption: "hi",
});
Over REST, a local video is sent as multipart form data directly. Over the socket, which can't
carry file bytes, the SDK uploads it through a signed URL first and sends the resulting
upload_id instead — no code change needed on your end.
const summary = await client.billing.summary();
const rental = await client.billing.startRental({ accept_terms: true, phones: 5, country: "US" });
const change = await client.billing.requestPhoneCount({ accept_terms: true, add: 2 });
const request = await client.billing.getRequest(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.
client.socket() returns an unconnected Socket exposing the same resource methods as the REST
client (except user and session, which are REST-only), over a connection that reconnects
automatically:
import { TERMINAL_RUN_STATUSES, ZeroBull } from "@0bull/sdk";
const client = new ZeroBull();
await using socket = client.socket(); // closed when the scope exits
await socket.connect();
const run = await socket.phones.runMacro(slot, { workflow: "post-to-story" });
for await (const event of socket.events()) {
if (event.type === "run" && event.run.id === run.id && TERMINAL_RUN_STATUSES.has(event.run.status)) {
break;
}
}
await using needs TypeScript 5.2+ with lib: "esnext", or Node 24+. In plain JavaScript on Node 22, call
socket.close() in a finally block instead.
events() yields a discriminated union on type: { type: "run", run },
{ type: "submission", submission }, { type: "billing_request", request }.
On an unexpected disconnect, the socket reconnects with a fresh session URL and exponential
backoff, up to maxReconnectAttempts; calls in flight at the time of the disconnect throw
SocketClosedError rather than being resent.
socket.call()Use socket.call(fun, data) to invoke a WebSocket function the SDK doesn't model yet. It gets the
same error mapping as every other call:
const result = await socket.call("/app/some/new-fun", { foo: "bar" });
All exceptions the SDK throws on purpose subclass ZeroBullError.
| Condition | Exception |
|---|---|
| 400 | BadRequestError |
| 401 | AuthenticationError |
| 403 | PermissionDeniedError |
| 404 | NotFoundError |
| 409 | ConflictError |
| 422 | ValidationError (with .errors) |
| 429 | RateLimitError (after retries, with .retryAfter) |
| 502, 503 | UnavailableError |
| ≥ 500 otherwise | InternalServerError |
| any other status | APIStatusError (base of all status errors) |
| network failure | APIConnectionError |
| request or call timeout | APITimeoutError |
| socket closed with calls in flight | SocketClosedError |
wait() timeout | WaitTimeoutError |
import { APIStatusError, ValidationError } from "@0bull/sdk";
try {
await client.submissions.create({
account_id: account.id,
video_url: "https://example.com/missing.mp4",
caption: "hi",
});
} catch (error) {
if (error instanceof ValidationError) {
console.log(error.errors);
} else if (error instanceof APIStatusError) {
console.log(error.status, error.message);
} else {
throw error;
}
}
Arguments the SDK can validate locally, such as a missing google_email on a YouTube account,
throw a plain TypeError 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 maxRetries before
RateLimitError is thrown. Nothing else is retried automatically.
list() methods return a Page. for await over a page walks it and every page after it:
for await (const account of await client.accounts.list()) {
console.log(account.id, account.handle);
}
Or page manually with nextPage() (null on the last page):
let page = await client.accounts.list();
while (page) {
for (const account of page.items) console.log(account.id);
page = await page.nextPage();
}
page.currentPage, page.lastPage, page.perPage, and page.total describe the current page.
runs.wait(run, { timeout, interval }) and submissions.wait(submissionOrId, { timeout, interval })
poll until the run or submission reaches a terminal status, throwing WaitTimeoutError once
timeout elapses. Defaults: runs 300000ms timeout / 2000ms interval; submissions 900000ms
timeout / 5000ms interval.
ZeroBull:
| Option | Default | Environment |
|---|---|---|
apiToken | required | ZEROBULL_API_TOKEN |
baseURL | https://0bull.net/api | ZEROBULL_BASE_URL |
timeout | 30000ms | |
maxRetries (REST 429) | 2 | |
fetch | global fetch |
client.socket():
| Option | Default |
|---|---|
callTimeout | 60000ms |
autoReconnect | true |
maxReconnectAttempts | 5 |
This SDK follows semantic versioning. Before 1.0.0, minor versions may
include breaking changes. See CHANGELOG.md for what changed in each release.
npm ci
npm run lint # fix: npm run format
npm run typecheck
npm test
npm run build
MIT
Download counts and popularity will appear here as the collector receives them.
This describes our collected data; it is not a security assessment.
Shared repository data · GitHub
Observed since 20 Oct, 22:05 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:dbc9eac460ca225b2f3ad0469be1328cd7ed76251e320b3602809668d4a2a47a