Live URLs and session recording in BAP
A BAP session doesn't have to run blind. liveURL() returns a link that streams the browser to anyone you send it to, optionally letting them click and type, and session replay records everything for the dashboard. This guide covers both, plus switching windows when a flow opens more than one.
- A Browserless API token from your account dashboard
- The BAP SDK installed and connecting, from the BAP Quickstart
Stream the session with a live URL
liveURL() returns a shareable link to a live stream of the browser. The two big use cases are watching your own automation while you debug it, and human-in-the-loop flows where a person completes the one step automation can't, such as entering a one-time passcode.
- TypeScript
- Python (Coming soon)
import Browserless from "@browserless.io/bap-ts";
const TOKEN = "YOUR_API_TOKEN_HERE";
const browser = Browserless.connect({
browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
token: TOKEN,
});
try {
const page = await browser.newPage();
await page.goto("https://example.com/login");
// interactable lets the viewer click and type, not just watch.
const { liveURL } = await page.liveURL({ interactable: true });
console.log(`Hand off to a human: ${liveURL}`);
// Block until the human's part is done, then continue the automation.
await page.waitForSelector("#dashboard", { timeout: 120_000 });
} finally {
// Close even when the human never finishes, or the session keeps billing.
await browser.close();
}
import bap.sync_api as bap
TOKEN = "YOUR_API_TOKEN_HERE"
with bap.Browserless.connect(
browser_ws_endpoint="wss://production-sfo.browserless.io/chromium/bql",
token=TOKEN,
) as browser:
with browser.page() as page:
page.goto("https://example.com/login")
# interactable lets the viewer click and type, not just watch.
live = page.live_url(interactable=True)
print(f"Hand off to a human: {live['liveURL']}")
# Block until the human's part is done, then continue the automation.
page.wait_for_selector("#dashboard", timeout=120000)
Response:
Hand off to a human: https://production-sfo.browserless.io/e/53b1.../live/index.html?i=a1b2c3d4...&t=...&showBrowserInterface=false
The URL needs no token or account, so the person you hand it to doesn't need Browserless credentials. It views a tab within one session; tab selection and popup following depend on targetId and mode (see below). Minting it also keeps that browser alive until the link's timeout, even after your script closes its connection or the operation finishes. The session's maximum duration still caps it. That's why the link can keep working after your client reports that the session ended.
Treat a live URL as a bearer secret. With
interactable: true, anyone holding the URL controls the browser session until it ends, including any logged-in state inside it. Send it over a trusted private channel only, and keep it out of logs, tickets, and shared messages.
Tuning the stream
| Option | Default | What it does |
|---|---|---|
interactable | true | Forward the viewer's clicks and keystrokes into the browser. Set false for watch-only streams |
type | jpeg | Stream frame format: jpeg or png. png is lossless and uses more bandwidth than jpeg. With png, also pass quality: 100 (see quality) |
quality | 70 | JPEG quality from 1 to 100. Any value below 100 with type: png fails with "type" must be "jpeg" when setting a quality, and that includes the default 70, so pass quality: 100 with png |
resizable | true | Resize the browser to match the viewer's screen. Set false to keep your viewport and letterbox the stream instead, which you want when a later screenshot depends on the viewport |
showBrowserInterface | false | Show a navigation bar and tabs around the stream |
emulateComponents | true | Emulate <select> dropdowns and the virtual keyboard for touch devices. Set false to suppress the virtual keyboard |
timeout | — | Sets link validity and the post-disconnect keep-alive window, capped to the time the session has left; when it expires the viewer sees a session-closed prompt |
If no session time remains, liveURL() rejects the request with The browser session has expired.
The response also carries liveURLId for correlating handoffs, targetId for the starting tab, and timeout for the lifetime actually applied in milliseconds. Older SDK releases may expose fewer fields. The generated LiveURLOptions reference stays current with the schema, including the compressed bandwidth toggle.
Choose which tab the viewer sees
The SDK's windows() method lists open page targets without changing automation's active window. Each entry contains targetId, url, title, active, and browserContextId. Use a returned targetId to mint a link for that tab:
- TypeScript
- Python (Coming soon)
const windows = await page.windows();
const target = windows.find((window) => window.active);
if (!target) throw new Error("No active tab");
const live = await page.liveURL({
targetId: target.targetId,
mode: "tab",
interactable: true,
instructions: "Complete this step, then click Done.",
timeout: 120_000,
});
// Deliver live.liveURL to the viewer through a trusted private channel.
const completion = await page.waitForLiveComplete({ timeout: 120_000 });
if (completion.reason !== "userDone") {
throw new Error(`Handoff ended: ${completion.reason}`);
}
// Verify the application state before continuing automation.
// Switching automation's tab is a separate, explicit operation:
await page.switchToWindow({ targetId: target.targetId });
# Within an open bap.sync_api Page:
windows = page.windows()
target = next((window for window in windows if window["active"]), None)
if target is None:
raise RuntimeError("No active tab")
link = page.live_url(
target_id=target["targetId"],
mode="tab",
interactable=True,
instructions="Complete this step, then click Done.",
timeout=120_000,
)
# Deliver link["liveURL"] through a trusted private channel.
completion = page.wait_for_live_complete(timeout=120_000)
if completion["reason"] != "userDone":
raise RuntimeError(f"Handoff ended: {completion['reason']}")
# Verify application state before continuing automation.
page.switch_to_window(target_id=target["targetId"])
For the async API (import bap), await each method call. Arguments use target_id; response dictionaries retain the GraphQL key targetId.
| Selection | Default mode | Behavior |
|---|---|---|
targetId supplied | "tab" | Stay on the selected tab. |
targetId omitted | "follow" | Start on the active window and follow new tabs and popups. |
An explicit mode overrides either default. Minting a link for another tab does not switch automation's active window. The mint response's targetId identifies the starting tab. waitForLiveComplete() and liveComplete listeners receive the link's anchor targetId at completion, including any re-anchor after closure; correlate handoffs by liveURLId.
These additions require an SDK revision that exposes windows() and the targetId/mode options (Python: target_id/mode), plus a server with the corresponding BrowserQL fields and completion targetId. Deploy the server before upgrading the SDK: older servers cannot satisfy the new response selections. These examples describe the updated SDK sources; use them only after the corresponding SDK release and server deployment are available.
For the CDP multi-tab recipe, see Multi-Tab Workflows.
Record the session for replay
Session replay captures DOM events, console logs, and network activity for the dashboard's Session Replay viewer. Recording is a connection decision, not a method call: add replay=true to the endpoint you connect to, and it starts as soon as the session does.
- TypeScript
- Python (Coming soon)
const browser = Browserless.connect({
// Recording starts automatically because of replay=true.
browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql?replay=true",
token: TOKEN,
});
try {
const page = await browser.newPage();
await page.goto("https://example.com");
// Optional: stop early and upload now instead of at session end.
const { success, error } = await page.stopSessionRecording();
console.log(success ? "uploaded" : `failed: ${error}`);
} finally {
// Close once the work is done; an open session keeps billing after the stop.
await browser.close();
}
with bap.Browserless.connect(
# Recording starts automatically because of replay=true.
browser_ws_endpoint="wss://production-sfo.browserless.io/chromium/bql?replay=true",
token=TOKEN,
) as browser:
with browser.page() as page:
page.goto("https://example.com")
# Optional: stop early and upload now instead of at session end.
result = page.stop_session_recording()
print("uploaded" if result["success"] else f"failed: {result['error']}")
Recordings upload when the session ends or when stopSessionRecording() succeeds, whichever comes first, and appear in the Session Replay section of your dashboard. Stopping early is worth doing on long sessions where only the first part matters, since everything after the stop isn't captured.
Switch between windows
When a click opens a new tab or window, your page methods keep talking to the original. switchToWindow() retargets the session at another window, matched by url or title pattern, or just the newest one.
- TypeScript
- Python (Coming soon)
await page.click("#open-checkout");
// The click spawned a tab; follow it.
await page.switchToWindow({ newest: true });
console.log(await page.url());
// Or match the window you want explicitly.
await page.switchToWindow({ url: "*checkout.example.com*" });
page.click("#open-checkout")
# The click spawned a tab; follow it.
page.switch_to_window(newest=True)
print(page.url())
# Or match the window you want explicitly.
page.switch_to_window(url="*checkout.example.com*")
Matching waits up to timeout (default 5000 milliseconds) for the window to exist, so calling right after the click that spawns it is safe.
FAQ & Troubleshooting
The live URL shows a closed-session message
The link's timeout expired, or the session hit its maximum duration. Closing your client or finishing the operation doesn't end the link on its own, because minting keeps the browser alive until the link's timeout. Mint the link with a timeout long enough for the person's task. For handoff flows, keep the script blocked on something like waitForSelector() while the human works.
Viewers can see the stream but can't click anything
The stream was created with interactable: false, or the element they're clicking needs the virtual keyboard you disabled with emulateComponents: false. Create a new live URL with interaction on; the option can't be flipped on an existing stream.
My replay never showed up in the dashboard
Recording only happens when the connection URL included replay=true. Confirm the parameter made it onto the endpoint, and if you called stopSessionRecording(), check its success and error fields, since a failed stop means nothing was uploaded.
Is a live URL the same as session replay?
No. liveURL() streams the session while it runs and leaves nothing behind. Replay records the session for viewing afterward in the dashboard. They're independent, and one session can use both.