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

BAP TypeScript SDK Overview

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
  • A Browserless API token from your account dashboard
  • Familiarity with Puppeteer's page API is helpful but not required
  • To run the examples below, install and connect first: BAP Quickstart

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 Migrate to BAP.

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 BAP Quickstart 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" });

// BAP runs on BrowserQL, so the session already pins to one stable exit IP by
// default. Pass `sticky: false` to rotate the IP per request instead. Anti-bot
// systems (Akamai, DataDome) bind their sensor to the egress IP, so a
// mid-session rotation can re-trigger the block.
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);
}
}

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. See Migrate to BAP for what changes, what has no equivalent, and what BAP adds. 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

Was this page helpful?