For AI agents: a documentation index is available at /llms.txt
Skip to main content

BAP TypeScript SDK

Browser Automation Protocol (BAP) is the TypeScript SDK for BrowserQL. It wraps BrowserQL's GraphQL-over-WebSocket API in a Puppeteer-shaped interface, so you write page.goto() and page.click() instead of hand-writing GraphQL documents. This page covers when to reach for BAP, how it talks to Browserless, and which parts of the Puppeteer API do and don't carry over.

Prerequisites

When to use BAP

Use BAP when you want BrowserQL's automation engine (managed stealth, residential proxies, CAPTCHA solving, live debugging URLs) but prefer writing TypeScript over GraphQL. Every option and response is typed, so you get autocomplete and compile-time checks instead of runtime GraphQL errors.

If you want to send BQL documents directly, from another language or through the BrowserQL IDE, use BrowserQL instead.

If you already have Puppeteer or Playwright code you don't want to rewrite, connect it to Browsers as a Service (BaaS) over CDP. BAP is not a drop-in replacement for Puppeteer. See Differences from Puppeteer.

If you need a single one-off task such as a screenshot or a PDF, use the REST APIs.

How it works

BAP does not connect over Chrome DevTools Protocol. Each Page method builds a BrowserQL mutation, sends it as one JSON frame over a WebSocket, and awaits the response. Operations are queued and run serially, matching BrowserQL's server-side concurrency model.

That architecture is why BAP is fast over the wire: a CDP session sends thousands of small messages for a single interaction, while BAP sends one frame per method call. It's also why the method set is a subset of Puppeteer's. Page exposes what BQL can express, and nothing that needs a live CDP session.

Each call to browser.newPage() opens its own WebSocket connection. Nothing connects at Browserless.connect() time.

Install and run

npm install @browserless.io/bap-ts

See Getting started with BAP for a runnable first script, the endpoint to use per browser, and the full list of connection options.

Common tasks

These examples continue from the quickstart, where browser is a connected Browserless instance.

Capture a screenshot or PDF

const page = await browser.newPage();
await page.goto("https://example.com");

// Returns a Uint8Array. `path` also writes to disk, and is Node.js only.
await page.screenshot({ path: "screenshot.png" });

await page.screenshot({
path: "full.webp",
type: "webp",
fullPage: true,
quality: 80,
});

await page.pdf({ path: "page.pdf", format: "a4", printBackground: true });

Extract content

html(), text(), and mapSelector() have no Puppeteer equivalent. They run server-side, so you get structured results back without a round trip per element.

await page.goto("https://example.com");

// Text of a single selector.
const heading = await page.$eval("h1");

// Structured properties for every match, in one request.
const items = await page.$$eval("ul li");
for (const item of items) {
console.log(item.innerText);
}

// Full HTML, optionally scoped to a selector and cleaned.
const { html } = await page.html({ selector: "main" });

Fill out a form

await page.goto("https://example.com/login");

// A delay range makes typing look human to bot detection.
await page.type("#username", "user@example.com");
await page.type("#password", "secret", { delay: [100, 200] });
await page.click("#submit");

await page.waitForNavigation({ waitUntil: "networkIdle" });

Block requests and route through a proxy

Call these before goto(), since they configure the session rather than act on the current page.

const page = await browser.newPage();

// Skipping images and stylesheets cuts page load time on scrape-only runs.
await page.reject({ type: ["image", "stylesheet"], operator: "or" });

await page.proxy({ country: "US", state: "California", sticky: true });

await page.goto("https://example.com");

Solve a CAPTCHA

await page.goto("https://example.com/protected");

const result = await page.solve({ type: "cloudflare", timeout: 30000 });
console.log(result.solved);

Listen to page events

Attaching the first console, request, or response listener opens a GraphQL subscription lazily; removing the last one closes it. Use on, once, and off as you would in Puppeteer.

page.on("console", (msg) => console.log(`[${msg.type}] ${msg.text}`));
page.on("request", (req) => console.log(`${req.method} ${req.url}`));

// Response bodies are not fetched by default, since streaming them is expensive.
page.on("response", (res) => console.log(`${res.status} ${res.url}`));

// Non-fatal streaming and handshake failures surface here instead of throwing.
page.on("error", (err) => console.error(err.message));

await page.goto("https://example.com");

Handle errors

BAP throws three typed errors, so you can tell a dead socket apart from a slow page.

import Browserless, {
BrowserQLError,
ConnectionError,
TimeoutError,
} from "@browserless.io/bap-ts";

try {
const page = await browser.newPage();
await page.goto("https://example.com", { timeout: 5000 });
} catch (error) {
if (error instanceof TimeoutError) {
console.error("operation timed out");
} else if (error instanceof ConnectionError) {
console.error("websocket connection failed");
} else if (error instanceof BrowserQLError) {
console.error("browserql returned errors:", error.errors);
}
}

Differences from Puppeteer

Page borrows Puppeteer's method names, but some shared methods behave differently:

MethodPuppeteerBAP
$eval(selector, fn)Runs your function against the matched element and returns its resultReturns the text content of the matched selector. No function argument
$$eval(selector, fn)Runs your function against all matched elementsDelegates to mapSelector and returns structured MapSelectorResponse[]
evaluate(fn, ...args)Passes serialized arguments and returns deserialized resultsAccepts a string or function, always returns string | null, and passes no arguments
waitForRequest / waitForResponseAccept a URL string or a predicate functionAccept a URL string or an options object. No predicate functions
scroll()Not on Page. You use mouse.wheel() or evaluate()First-class method with selector and coordinate targeting

These Puppeteer APIs have no BAP equivalent, because they need a live CDP session:

  • Input devices: no page.keyboard, page.mouse, or page.touchscreen
  • Frames: no page.frames(), page.mainFrame(), or frame targeting
  • Workers: no page.workers()
  • Function exposure: no exposeFunction()
  • Emulation: no emulate() or emulateCPUThrottling(), though emulateMediaType() is supported
  • Security and cache: no setBypassCSP(), setCacheEnabled(), or setOfflineMode()
  • Coverage, tracing, and accessibility: no page.coverage, page.tracing, or page.accessibility

BAP adds methods Puppeteer has no equivalent for: html(), text(), markdown(), mapSelector(), check()/uncheck(), reject(), proxy(), solve(), solveImageCaptcha(), liveURL(), reconnect(), switchToWindow(), stopSessionRecording(), preferences(), loadSecret(), fulfill(), and request()/response().

Node.js and browser support

The package is isomorphic. Node.js uses the ws package, and bundlers targeting the browser (Vite, webpack, esbuild, Rollup) pick the native WebSocket build through the browser export condition, so no Node built-ins end up in your bundle.

screenshot() and pdf() resolve to a Uint8Array on both platforms. In Node.js the value is a Buffer, which is itself a Uint8Array. The path option writes to disk in Node.js only; in the browser it rejects, so use the returned bytes.

// Browser: turn the bytes into something renderable.
const bytes = await page.screenshot({ type: "png" });
const url = URL.createObjectURL(new Blob([bytes], { type: "image/png" }));

// querySelector returns null when the element is absent, so guard before assigning.
const img = document.querySelector("img");
if (img) {
img.src = url;
}

Node.js sends a User-Agent of @browserless.io/bap-ts/<version> on the WebSocket handshake, so those sessions are identifiable as SDK traffic server-side. Browsers forbid custom handshake headers, so the browser build can't send it. Don't use that header to measure browser-build usage, because it never arrives.

Running BAP in the browser exposes your bundled token to the client and requires the endpoint to accept the origin you connect from. Only do this when your token-delivery policy allows that exposure.

API reference

The reference is generated from the public exports of @browserless.io/bap-ts, covering the browser API, transport contracts, GraphQL builder, and every generated option and response type.

FAQ & Troubleshooting

Why does addScriptTag fail with SyntaxError: Invalid or unexpected token?

Inline content must be a single line. The BrowserQL server rejects multi-line content. Host the script and pass url instead, or collapse the source to one line first, which only works when it has no newline-sensitive syntax such as // comments or multi-line template literals.

Can I reuse my existing Puppeteer script?

Not without changes. BAP covers the subset of Puppeteer that BrowserQL can express, so anything using page.mouse, page.keyboard, frames, or argument-passing evaluate() needs rewriting. See Differences from Puppeteer. If you want to run existing scripts unchanged, use BaaS over CDP instead.

Why does evaluate() return a string when my function returns an object?

evaluate() always resolves to string | null, and it passes no arguments to your function. Serialize inside the page with JSON.stringify() and parse the result yourself.

Why is $$eval() returning objects instead of my mapped values?

BAP's $$eval() delegates to mapSelector() rather than running your function in the page, so it returns MapSelectorResponse[] with element properties such as innerText and innerHTML. Read the property you need off each result.

Next steps