Skip to main content

Guides

Hold a browser from your app

Bind your app's accounts to profiles, hold a profile's browser while your code works, renew the hold, let go without closing it and follow session events.

View as Markdown

An app that works with many accounts — a poster, an inbox checker, a scraper — needs three things from Browlix: which profile belongs to which of its accounts, a browser it can use without someone else taking it mid-task, and a way to let go that does not throw away a warm browser the next job needs. Bindings and holds give you exactly that, through the same API, MCP tools, SDK and permissions as everything else.

A complete program that does all of this — and recovers from a lost connection and a restart — is the sample app.

Bind your accounts to profiles

A binding says "account acc-42 of app fb-poster uses this profile". It never holds a password, cookie or token — the profile already carries the signed-in state.

const b = await bx.call("bindings.create", {
  body: { app_id: "fb-poster", external_account_id: "acc-42", profile_id: profileId, label: "Shop page" },
});
  • One binding per (app_id, external_account_id). A second one is CONFLICT with the existing binding_id; move it with bindings.update instead.
  • bindings.update and bindings.delete need the revision you last read, so two tools never overwrite each other.
  • metadata is up to 4 KiB of your own data. Keys that look like credentials (password, token, cookie, secret, otp, auth, api_key) are refused.
  • bindings.list shows each binding with the profile's live session and who holds it.

Creating, changing and removing bindings needs profile.edit; reading them needs profile.read. Group scope applies: a key whose member sees only some groups sees only those bindings.

Hold a browser

sessions.acquire opens the profile on a fitting machine — or reuses its browser if it is still open and free — and gives your app a hold on it.

const hold = await bx.acquireSession({
  appId: "fb-poster",
  externalAccountId: "acc-42",
  holderRef: "poster-worker-3",   // your stable name for this worker
  ttlSeconds: 120,                // renew within this time
  keepWarmSeconds: 300,           // stay open this long after you let go
  wait: true,                     // resolve once the browser is ready
  onLost: () => abortCurrentJob(), // someone newer holds it, or it ended
});
const a = await bx.attach(hold.sessionId);
// … drive the page with Playwright (see Attach with Playwright)
await hold.detach();

Send profile_id instead of app_id + external_account_id when you do not use bindings. Add device_id to choose the machine; otherwise Browlix picks the least busy online machine with the profile's OS.

The reply is the session plus lease (generation, holder_ref, expires_at, …) and acquired:

acquiredHTTPMeaning
opened201A new browser was started for you
reused200The browser was still open from an earlier hold; you got it with a new generation
renewed200You already held it (same key and holder_ref); the hold was extended

When someone else has it

A profile has one browser and one holder at a time. If it is in use, acquire answers BUSY with details.holder_kind:

holder_kindWhoretryable
personSomeone using it in the app (remote view with control) or a session opened without a holdno — ask them
automationA running automation flowyes, when the run ends
appAnother app's current holdyes, when it lets go or lapses
stoppingThe browser is closingyes, in a moment
other_machineIt is open on a machine other than the device_id you asked forno

Browlix never tells you who the other holder is. RECOVERY_REQUIRED means the last session ended uncertainly — recover it first.

Fencing

Every hold has a generation. Each time a different holder takes the browser the generation goes up, so a worker that was paused, lost its network or kept running after a timeout cannot act for the browser any more:

  • sessions.renew and sessions.detach must send the generation and holder_ref acquire returned, with the same API key.
  • An older generation, a hold that lapsed, was detached or whose session ended answers FENCED with details.reason (superseded, expired, detached or session_ended).
  • On FENCED, stop touching that browser and acquire again. The SDK's acquireSession stops renewing and calls onLost for you.

Renew before ttl_seconds (30–600, default 120) runs out — the SDK renews every third of it. A hold that is not renewed lapses: the browser stays open for keep_warm_seconds, then stops unless someone acquires it.

Let go without closing

sessions.detach ends your hold but never stops the browser by itself. It stays warm for keep_warm_seconds (0–1800, default 120, or the value you send with detach) so the next job for the same account starts instantly. After that it stops — unless a person took control or an automation run claimed it in the meantime; then it keeps running for them. To close it right away, send keep_warm_seconds: 0 or call sessions.stop.

Browlix checks expired holds once a minute, so a browser may stay open up to about a minute past its keep-warm time.

Follow session events

Webhooks can be missed while your receiver is down. sessions.events is the log behind them: every session state change and every hold acquired, detached, lapsed or ended, in id order.

ts
let after = loadCursor(); // the last id you processed, 0 the first time
const r = await bx.call("sessions.events", { query: { after, limit: 200 } });
for (const e of r.data as { id: number; kind: string; session_id: string }[]) {
  handle(e);           // ids only grow; skip an id you have seen
  after = e.id;
}
saveCursor(after);
  • Events are kept 7 days. gap: true means events after your cursor are already gone — resync with sessions.list and bindings.list.
  • Filter with session_id or profile_id. Reading needs session.read.

What each machine supports

devices.capabilities lists what every machine can do now — CDP attach (always same-machine), headless, profile sync, the storage key, remote view, automation, the synchronizer, the AI agent and the AdsPower-compatible API — with the worker version each needs, its capacity and how many browsers it runs. Check it instead of guessing from version numbers.

Permissions

OperationCapability
bindings.listprofile.read
bindings.create, bindings.update, bindings.deleteprofile.edit
sessions.acquire, sessions.renew, sessions.detachsession.start
sessions.eventssession.read
devices.capabilitiesdevice.read

Every write is audited and takes a command_id, so a retry after a lost reply returns the first result (commands and receipts). Over MCP the same operations are the tools bindings_create, sessions_acquire, sessions_renew, sessions_detach, sessions_events and so on.