Hybrid Automation
Hybrid Automation is only available on paid plans.
Hybrid automation lets you pause an automated script, hand control to a human via a secure live URL, and resume automation once they finish. Use it for login flows, 2FA, CAPTCHAs, or any step that requires human judgment.

How It Works
Create a liveURL through CDP and Browserless returns a one-time, shareable link. No API tokens are exposed and no extra software is needed. For advanced patterns like multi-stage workflows, read-only monitoring, and bandwidth optimization, see Advanced Hybrid Automation Configurations.
| Behavior | Default | Override |
|---|---|---|
| Viewport | Resizes to match the end user's screen | resizable: false to keep the current viewport |
| Interaction | Click, type, scroll, touch, and tap enabled | interactable: false to block all input (view-only mode) |
| Stream quality | Full quality compressed video | quality: 1-100 to reduce bandwidth (useful for mobile) |
| Tab scope | Only the page used to create the URL | showBrowserInterface: true to stream all tabs (may increase CAPTCHA challenges) |
| Events | None | Listen for Browserless.liveComplete and Browserless.captchaFound |
Basic Implementation
- Puppeteer
- Playwright
import puppeteer from 'puppeteer-core';
const browser = await puppeteer.connect({
browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
});
const page = await browser.newPage();
await page.goto('https://practicetestautomation.com/practice-test-login/');
const cdp = await page.createCDPSession();
const { liveURL } = await cdp.send('Browserless.liveURL');
console.log('Share this URL:', liveURL);
// Wait for the user to complete their tasks
await new Promise((r) => cdp.on('Browserless.liveComplete', r));
// Continue automation...
await browser.close();
import { chromium } from 'playwright-core';
const browser = await chromium.connectOverCDP(
'wss://production-sfo.browserless.io/chromium/stealth?token=YOUR_API_TOKEN_HERE'
);
const [context] = await browser.contexts();
const page = await context.newPage();
await page.goto('https://practicetestautomation.com/practice-test-login/');
const cdpSession = await context.newCDPSession(page);
const { liveURL } = await cdpSession.send('Browserless.liveURL');
console.log('Share this URL:', liveURL);
// Wait for the user to complete their tasks
await new Promise((r) => cdpSession.on('Browserless.liveComplete', r));
// Continue automation...
await browser.close();
Close LiveURL Programatically
This script detects whether the user closed the LiveURL tab manually or if a specific selector appeared on the page, and then programmatically closes the LiveURL session.
- Puppeteer
- Playwright
import puppeteer from "puppeteer-core";
const browser = await puppeteer.connect({
browserWSEndpoint: `wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE`,
});
const page = await browser.newPage();
await page.goto("https://practicetestautomation.com/practice-test-login/");
const cdp = await page.createCDPSession();
const { liveURL, liveURLId } = await cdp.send("Browserless.liveURL", {
timeout: 300000,
});
console.log("Share this URL:", liveURL);
// Close when user finishes manually OR login succeeds
const result = await Promise.race([
new Promise((r) => cdp.on("Browserless.liveComplete", () => r("user_closed"))),
page.waitForSelector("h1.post-title", { timeout: 0 }).then(async () => {
await cdp.send("Browserless.closeLiveURL", { liveURLId });
return "login_detected";
}),
]);
console.log(`LiveURL closed via: ${result}`);
await browser.close();
import { chromium } from "playwright-core";
const browser = await chromium.connectOverCDP(
`wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE`
);
const [context] = browser.contexts();
const page = await context.newPage();
await page.goto("https://practicetestautomation.com/practice-test-login/");
const cdpSession = await context.newCDPSession(page);
const { liveURL, liveURLId } = await cdpSession.send("Browserless.liveURL", {
timeout: 300000,
});
console.log("Share this URL:", liveURL);
// Close when user finishes manually OR login succeeds
const result = await Promise.race([
new Promise((r) => cdpSession.on("Browserless.liveComplete", () => r("user_closed"))),
page.waitForSelector("h1.post-title", { timeout: 0 }).then(async () => {
await cdpSession.send("Browserless.closeLiveURL", { liveURLId });
return "login_detected";
}),
]);
console.log(`LiveURL closed via: ${result}`);
await browser.close();
End-User Authentication
LiveURL links authenticate using a short-lived ?i=<id> parameter that is unique to the session. This ID expires automatically after the LiveURL timeout elapses or when Browserless.closeLiveURL is called. LiveURL paths do not require your API token. Do not embed your token in LiveURL links shared with end users.