CDP Extensions
Browserless-specific Chrome DevTools Protocol methods you call via cdp.send() on a page's CDP session in Puppeteer or Playwright. They extend standard CDP with live URLs, captcha solving, session reconnect, recording, file transfer, and page identification. Several also emit CDP events (listen with cdp.on()).
- A Browserless API token from your account dashboard
- An existing WebSocket connection to Browserless via Puppeteer or Playwright
- Familiarity with creating a CDP session (
page.createCDPSession()in Puppeteer,page.context().newCDPSession(page)in Playwright)
Captcha solving, reconnect, and recording are each gated on their own feature flag / query param and return an error in the result when unavailable.
Usage
const browser = await puppeteer.connect({ browserWSEndpoint: '...' });
const [page] = await browser.pages();
const cdp = await page.createCDPSession();
// Command: get a live URL for debugging
const { liveURL } = await cdp.send('Browserless.liveURL', { quality: 70 });
// Event: react to an auto-detected captcha
cdp.on('Browserless.captchaFound', ({ type, status }) => {});
Commands
Browserless.liveURL
Mints a temporary URL that opens a live, interactive view of the current page in any browser. For debugging, QA, or human-in-the-loop workflows. The Live URL timeout sets the URL's validity and post-disconnect keep-alive window. The browser and viewer still end at the existing session deadline; minting a Live URL never resets or extends that deadline. If timeout is longer than the time the session has left, the URL is minted for the remaining time instead, and a session with no time left returns error: "The browser session has expired.". Each Live URL accepts up to five concurrent viewers (a sixth connection receives HTTP 429). Without targetId, it starts on the page whose CDP session sends it and follows new tabs and popups. With targetId, it starts on that page target and defaults to staying on that tab. Selecting a target does not switch automation's active tab. For one link per tab, see Multi-Tab Workflows.
Call via CDP: cdp.send('Browserless.liveURL', { quality: 70 })
Returns: { error, liveURLId, liveURL, timeout, targetId } (error is null on success; timeout is the lifetime actually applied, in ms).
| Parameter | Type | Default | Description |
|---|---|---|---|
targetId | string | sending page | Page target to stream, including a tab in another browser context in the same browser. Required when sending on a browser-level CDP session. The result returns the selected targetId. |
mode | "follow" | "tab" | "tab" with targetId; "follow" otherwise | tab stays on the selected tab; follow follows new tabs and popups. An explicit mode overrides the default. |
quality | number | 70 | Stream image quality (1-100). Applies to jpeg frames. |
type | string | "jpeg" | Stream frame format: jpeg or png. png is lossless, so each frame is larger and the stream uses more bandwidth. |
timeout | number | 30000 | How long (ms) the minted URL stays valid. A value above your plan's maximum session duration returns an error. A value above the session's remaining time is capped to the remaining time, and the applied value comes back as timeout. |
interactable | boolean | true | Allow the viewer to click/type into the page. With non-empty instructions or steps, also shows the completion buttons. |
instructions | string or object | omitted | Legacy text or a structured card with layout, format, title, summary, steps, and currentStep as described below. String text is trimmed and silently capped at 2,048 characters; line breaks are preserved. No non-empty text or steps means no card or completion buttons, without changing interactivity. |
instructionsLayout | "modal" | "panel" | "modal" | Where plain or rich cards appear. modal overlays the page. panel reserves a 340px column beside the stream and requires non-empty text or steps and interactable: true. Viewers 900px wide or narrower get a collapsible version. Any other value returns an error. |
instructions.title | string | omitted | Plain-text card title, at most 80 characters after trimming. |
instructions.summary | string | omitted | Card text, trimmed and capped at 2,048 characters. |
instructions.steps | string[] | omitted | At most 10 strings, each non-empty after trimming and at most 300 characters. An empty array is treated as absent. |
instructions.currentStep | integer | omitted | From 1 through steps.length; requires non-empty steps. Prior steps are checked and disabled; the current and later steps remain editable. |
instructions.layout | "modal" or "panel" | "modal" | Overrides legacy instructionsLayout, which remains supported for string callers. |
doneLabel | string | "Done" | Plain-text success button label, at most 40 characters after trimming. |
failLabel | string | layout default | Plain-text failure button label, at most 40 characters after trimming. Defaults to "Couldn't do it" in modal and "Couldn't finish" in panel, for both plain and rich cards. |
requesterName | string | omitted | Plain-text requester name, at most 60 characters after trimming. |
instructions.format | "text" or "markdown" | "text" | Formatting for summary and steps only. Supports bold (** or __), italic (* or _), inline backticks, and [label](http(s)://url) links, including combined emphasis such as **_bold italic_**. Code spans and HTML remain literal; underscores within words stay unchanged. Title, labels, and requester are always plain text. |
captureScale | number | 1 | Capture resolution multiplier: 1 or 2. 2 captures at twice the pixel density for high-DPI screens, which means four times the pixels per frame. Any other value returns an error. |
resizable | boolean | true | Allow the viewer to resize the viewport. |
showBrowserInterface | boolean | false | Show browser chrome around the page. |
compressed | boolean | true | Compress the stream. |
emulateComponents | boolean | true | Inject a touch-friendly <select> dropdown and, for touch-capable viewers, an on-screen keyboard into the remote page so they work through the stream. Set false if your page doesn't need them. To restyle them, see Style the Live URL keyboard and dropdowns. |
When instructions is an object, put title, steps, current step, and format inside it. Separate instructionsTitle, steps, or currentStep arguments return an error, as does a separate instructionsFormat other than text. The default text value is allowed for BQL compatibility and does not override the object's format. This also applies to Browserless.setLiveURLInstructions.
Browser-level CDP sessions can mint a link when targetId is supplied. Invalid requests return an error:
| Condition | Error |
|---|---|
Browser-level session without targetId | targetId is required when Browserless.liveURL is sent on the browser session |
| Unknown page target | No open page with targetId "<id>" in this browser. |
| Invalid mode | The "mode" parameter must be "follow" or "tab". |
To show instructions in a side panel, set instructions.layout when you mint the link. The button labels and requester remain outer arguments, not object fields:
const { liveURL, liveURLId, error } = await cdp.send('Browserless.liveURL', {
instructions: {
layout: 'panel',
summary: 'Sign in, enter your verification code, then choose Done.',
},
interactable: true,
timeout: 120000,
});
if (error) throw new Error(error);
On read-only links (interactable: false), modal still shows instructions without completion buttons, but panel shows nothing. See Keep instructions beside the page for panel sizing, mobile behavior, and a BQL example.
Without instructions, your automation ends the handoff by detecting the expected page state and calling cdp.send('Browserless.closeLiveURL', { liveURLId }), or letting the link time out. Browserless.liveComplete still reports closed / timeout; last-viewer disconnect can also report viewerDisconnected.
Browserless.setLiveURLInstructions
Replaces the entire card in an open live URL's Instructions panel without minting a new link. Connected viewers see the change right away. This is not a patch: omitted content fields are cleared, so resend all content you want to retain when advancing currentStep. Viewer checkbox ticks are local only, not server-side progress or completion proof. The same normalized steps and unchanged server currentStep preserve local ticks; changing server currentStep resets ticks to the supplied progress, and changing steps resets them. Steps before the supplied currentStep are checked and disabled; the current and later steps remain editable. The first unchecked step is highlighted; when all are checked, none is highlighted and the handoff does not auto-complete.
Call via CDP: cdp.send('Browserless.setLiveURLInstructions', { liveURLId, instructions: 'Now enter the code we texted you.' })
Returns: { error, liveURLId } (error is null on success).
| Parameter | Type | Description |
|---|---|---|
liveURLId | string | Required. The liveURLId returned by liveURL on this session. Another session's ID returns error: "Unknown liveURLId for this session.". |
| Card options | same as above | Optional. Accepts string or object instructions, plus outer doneLabel, failLabel, and requesterName, with the same normalization and validation as liveURL. Use instructions.layout to change layout; legacy outer instructionsLayout only applies when minting. Only liveURLId is required. No non-empty text or steps hides the modal, side panel, collapsed pill, and completion buttons without changing interactivity. |
Existing { liveURLId, instructions } string calls remain supported. Previous flat rich-card fields remain compatibility options; prefer the instructions object for new calls. Empty optional strings are treated as absent. Title, labels, or requester without text or steps cannot create a card. Card options are available when minting through CDP or BQL; updates use this CDP command. REST accepts string instructions only and rejects objects.
Replacement preserves the current layout when layout is omitted and switches layout when supplied. In panel mode, a non-empty replacement keeps the desktop panel open; clearing the card releases its reserved space. A later non-empty replacement restores the panel. At 900px or narrower, each replacement resets the panel to its collapsed summary unless the person has the failure-feedback form open. An open feedback form and its draft survive card replacements and clearing; Go back dismisses and clears the draft. Local checkbox ticks follow the same steps and server-progress rules in both layouts.
Browserless.closeLiveURL
Closes a live URL opened with liveURL, freeing server resources. Called automatically when the session ends.
Call via CDP: cdp.send('Browserless.closeLiveURL', { liveURLId })
Returns: { error, liveURLId }.
| Parameter | Type | Description |
|---|---|---|
liveURLId | string | Required. The liveURLId returned by liveURL. |
Browserless.solveCaptcha
Detects a CAPTCHA on the current page and attempts to solve it via a third-party solving service (reCAPTCHA v2/v3, Cloudflare Turnstile, and more). Requires captcha solving to be available for the session.
Call via CDP: cdp.send('Browserless.solveCaptcha')
Returns: { ok, captchaFound, solveAttempted, solved, token?, message?, error? }.
Browserless.reconnect
Returns a WebSocket URL for reconnecting to this browser session after disconnecting. The browser stays alive server-side for timeout ms. Requires reconnect to be enabled; on hosted Cloud plans a timeout above the plan's maximum session duration returns an error instead of being silently clamped. The browser still terminates at its original session deadline even if the TTL has time left.
Call via CDP: cdp.send('Browserless.reconnect', { timeout: 60000 })
Returns: { auth, error, browserWSEndpoint } (browserWSEndpoint is null on error).
| Parameter | Type | Description |
|---|---|---|
timeout | number | How long (ms) to keep the browser alive after disconnect. Defaults to the deployment's configured connection timeout. |
auth | string | Optional auth token echoed back and required on the reconnect handshake. |
Browserless.pageId
Returns Browserless's stable identifier for the current page target, useful for routing and correlating a page across reconnects.
Call via CDP: cdp.send('Browserless.pageId')
Returns: { pageId: '...' }.
Browserless.startRecording
Starts server-side video recording of the page. Requires connecting with ?record=true. Refused after a credential has been filled on the session (the recording could capture the secret).
Call via CDP: cdp.send('Browserless.startRecording', { width, height })
Returns: { recording, error? }.
| Parameter | Type | Description |
|---|---|---|
width | number | Optional recording width. |
height | number | Optional recording height. |
Browserless.stopRecording
Stops the recording started with startRecording and returns the video. Requires ?record=true.
Call via CDP: cdp.send('Browserless.stopRecording', { encoding: 'base64' })
Returns the encoded recording, or { error }.
| Parameter | Type | Description |
|---|---|---|
encoding | string | "binary" or "base64". |
Browserless.stopSessionRecording
Stops rrweb session-replay capture, then processes and uploads the events. Requires connecting with ?replay=true.
Call via CDP: cdp.send('Browserless.stopSessionRecording')
Returns: { error } (null on success).
Browserless.uploadFile
Uploads one or more files into a file <input> on the page, matched by selector. Total decoded size is capped per request.
Call via CDP: cdp.send('Browserless.uploadFile', { selector, files })
Returns: { ok, error? }.
| Parameter | Type | Description |
|---|---|---|
selector | string | Required. CSS selector for the file input. |
files | array | Required. Objects of { name, content, mimeType? } where content is base64 and mimeType is inferred from name if omitted. |
Browserless.setDownloadEnabled
Toggles streaming of completed downloads back to the client as Browserless.fileDownloaded events. Off by default, so download bytes never flow unless you opt in.
Call via CDP: cdp.send('Browserless.setDownloadEnabled', { enabled: true })
Returns: { ok, enabled }.
| Parameter | Type | Description |
|---|---|---|
enabled | boolean | Whether to emit fileDownloaded events. |
Events
Subscribe with cdp.on('<EventName>', handler).
Browserless.heartbeat
Periodic keep-alive emitted on each attached session at the server's heartbeat interval. No params.
Browserless.captchaFound
Emitted when a CAPTCHA is auto-detected on a page.
Params: { type, status } — status is "solving" when auto-solve is on, otherwise "found".
Browserless.captchaAutoSolved
Emitted after an auto-solve attempt completes (only when auto-solving is enabled).
Params: the solve result { found, solved, token, time, error? }.
Browserless.fileDownloaded
Emitted when a download completes, if enabled via setDownloadEnabled. Mirrored to every attached session.
Params: { filename, mimeType, size, data } (data base64), or { filename, mimeType, size, error: 'FileTooLarge', maxBytes } when the file exceeds the transfer limit.
Browserless.liveComplete
Emitted at most once per mint created with Browserless.liveURL, including links nobody opens. Params: { liveURLId: string, reason: LiveURLCompleteReason, targetId: string, feedback?: { category?: string, message?: string } }.
| Parameter | Type | Meaning |
|---|---|---|
liveURLId | string | Link whose handoff ended. Use this to correlate the event with its mint. |
reason | LiveURLCompleteReason | Why the handoff ended; values below. |
targetId | string | Link's anchor tab when it ended, including any re-anchor after closure. It can differ from the mint response's targetId. |
| Reason | Meaning |
|---|---|
timeout | The link's lifetime expired. |
closed | The link was explicitly closed, replaced by a new mint on the same page, or lost its anchor page without a replacement. |
viewerDisconnected | The last viewer disconnected and stayed gone for two seconds, including credential-protection teardown. |
userDone | The viewer chose Done in the Instructions panel. The link closes; the browser session keeps running. |
userFailed | The viewer chose Couldn't do it, then Send and end session in the feedback form. The link closes; the browser session keeps running. |
Only userFailed can include optional feedback. Couldn't do it opens a feedback form before Send and end session completes the handoff. The viewer can select a category and optionally enter a message:
| Category | Viewer label |
|---|---|
verification | Verification or captcha I couldn't complete |
missingInfo | Missing login details or information |
pageBroken | Page didn't load or looks broken |
unexpected | Something unexpected appeared |
other | Other |
The message is trimmed and limited to a maximum of 500 characters. Feedback is untrusted bearer-holder input: anyone holding the Live URL can submit it. Never render it as HTML or feed it to an LLM as instructions. Avoid logging the message, which may contain sensitive information.
The same feedback is available on BQL liveComplete and REST GET /browser/{id}/live/{liveURLId}. These surfaces return feedback: null when feedback is absent.
Every reason consumes that mint's one completion. After an ordinary viewerDisconnected, the URL remains reopenable until expiry while the browser session is available, but later visits or expiry produce no further completion. Reconnecting within the two-second grace period, such as during a viewer reload, cancels the pending disconnect completion. Credential-protection teardown is an exception: after a stored credential is filled, new viewers are blocked for that session, so reopening cannot cancel or re-arm completion. userDone and userFailed are the viewer's own verdict, not a check Browserless runs. Anyone holding the link can choose Done, so completion never proves the task succeeded. Verify the page state before continuing.
Delivery requires a CDP session attached to the page; earlier events are not replayed. Browser shutdown records closure internally but does not send this event. Browserless.reconnect IDs and links minted through BQL or HTTP do not produce this CDP event. Existing zero-argument listeners continue to work unchanged.
When re-minting on the same page, register the new listener after the mint response, then share the URL. Re-minting reuses the ID and emits closed for the superseded mint; a listener armed before the response can catch that old completion. See handling completion reasons.
Browserless.liveViewerConnected
Emitted each time a viewer WebSocket attaches to a live URL, including additional viewers and reconnects. Both /live/* and /chromium/live/* viewer routes emit this event.
Params: { liveURLId: string }, identifying the live URL returned by Browserless.liveURL. The event is delivered to attached CDP sessions for that URL's page, not other pages. It signals attachment, not that the viewer has finished interacting or received a frame.
Register the listener before sharing the URL; earlier attachments are not replayed:
const cdp = await page.createCDPSession();
cdp.on('Browserless.liveViewerConnected', ({ liveURLId }) => {
console.log('Viewer connected:', liveURLId);
});
const { liveURL } = await cdp.send('Browserless.liveURL');
// Share liveURL with the viewer.
FAQ & Troubleshooting
Why does liveURL or reconnect return an error instead of a URL?
The commands have different requirements. For liveURL, an error means invalid authentication, a runtime failure, an invalid parameter, or an expired session. liveURL rejects a timeout that isn't a whole number of milliseconds above 0 or that exceeds your plan's maximum session duration. It also rejects instructions that is neither a string nor a valid card object, a layout other than "modal" or "panel", and a captureScale other than 1 or 2. liveURL reports these in error and doesn't throw, so check error before using liveURL. reconnect has its own reconnect ceiling, and recording requires ?record=true and a plan that includes recording. When a command is unavailable, it returns error instead of the expected field (liveURL, browserWSEndpoint, and so on).
Both liveURL and setLiveURLInstructions validate card fields and return an error for invalid input:
| Invalid input | Error |
|---|---|
| Invalid instructions | Use a string or a card object; object summary must be a string. |
| Invalid format | format must be text or markdown. |
| Invalid steps | The 'steps' value must be an array of at most 10 non-empty strings of at most 300 characters each. |
| Invalid active step or missing steps | The 'currentStep' value must be a whole number between 1 and the number of steps. |
| Non-string or over-limit optional string | Title: 80 characters; outer doneLabel / failLabel: 40; outer requesterName: 60. |
| Metadata without text or steps | Provide non-empty 'instructions' or 'steps' to use '<field>'. |
| Unsafe Markdown link | Links in summary or steps must start with http:// or https://. |
Why isn't my cdp.on() handler firing?
CDP extension events are scoped to the session that triggered them, so cdp.on() has to be attached to the same CDPSession you called cdp.send() on, before the triggering action happens. A handler attached after the event already fired, or attached to a different page's CDP session, never receives it.
Why does Browserless.startRecording fail after I've already filled in a login form?
Recording is refused once a credential has been filled on the session, since the recording could capture the secret. Start recording before any authentication step, or use a separate session for the recorded portion of the flow.