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.
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 isCONFLICTwith the existingbinding_id; move it withbindings.updateinstead. bindings.updateandbindings.deleteneed therevisionyou last read, so two tools never overwrite each other.metadatais up to 4 KiB of your own data. Keys that look like credentials (password,token,cookie,secret,otp,auth,api_key) are refused.bindings.listshows 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:
acquired | HTTP | Meaning |
|---|---|---|
opened | 201 | A new browser was started for you |
reused | 200 | The browser was still open from an earlier hold; you got it with a new generation |
renewed | 200 | You 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_kind | Who | retryable |
|---|---|---|
person | Someone using it in the app (remote view with control) or a session opened without a hold | no — ask them |
automation | A running automation flow | yes, when the run ends |
app | Another app's current hold | yes, when it lets go or lapses |
stopping | The browser is closing | yes, in a moment |
other_machine | It is open on a machine other than the device_id you asked for | no |
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.renewandsessions.detachmust send thegenerationandholder_refacquire returned, with the same API key.- An older generation, a hold that lapsed, was detached or whose session ended answers
FENCEDwithdetails.reason(superseded,expired,detachedorsession_ended). - On
FENCED, stop touching that browser and acquire again. The SDK'sacquireSessionstops renewing and callsonLostfor 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.
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: truemeans events after your cursor are already gone — resync withsessions.listandbindings.list. - Filter with
session_idorprofile_id. Reading needssession.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
| Operation | Capability |
|---|---|
bindings.list | profile.read |
bindings.create, bindings.update, bindings.delete | profile.edit |
sessions.acquire, sessions.renew, sessions.detach | session.start |
sessions.events | session.read |
devices.capabilities | device.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.
