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

BAP Python SDK Overview

Browser Automation Protocol (BAP) is the Python SDK for BrowserQL, distributed as bap-py. Every class, method, option, and response type is generated from the BrowserQL schema, 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 Playwright's API do and don't carry over.

Prerequisites
  • A Browserless API token from your account dashboard
  • Familiarity with Playwright's Python 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 Python over GraphQL. Options and response fields are generated straight from the schema, so a mismatched keyword argument fails at call time instead of silently sending the wrong GraphQL variable.

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

If you already have Playwright or Selenium 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 Playwright. 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 Playwright's. Page exposes what BQL can express, and nothing that needs a live CDP session.

Each call to browser.new_page() or the browser.page() context manager opens its own WebSocket connection. Nothing connects at Browserless.connect() time.

The package ships two API surfaces with matching class and method names: bap for asyncio applications, and bap.sync_api for synchronous code. The sync surface isn't a separate transport, it runs the same async implementation on a background event-loop thread and blocks the calling thread until each call resolves. Importing bap.sync_api is fine even inside an asyncio application. The restriction is calling its methods from the thread that already owns a running event loop, since blocking that thread would deadlock the loop it's supposed to drive.

Install and run

python -m pip install bap-py

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 page is open inside a connected Browserless session. They use the sync API. For async, import bap instead of bap.sync_api and add await. Method names and options are identical.

Capture a screenshot or PDF

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

# Returns bytes. `path` also writes to disk.
page.screenshot(path="screenshot.png")

page.screenshot(
path="full.webp",
type="webp",
full_page=True,
quality=80,
)

page.pdf(path="page.pdf", format="a4", print_background=True)

Extract content

html(), eval_on_selector(), and eval_on_selector_all() have no direct Playwright equivalent since they don't run your code in the page. They run server-side, so you get structured results back without a round trip per element.

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

# Text content of a single selector.
heading = page.eval_on_selector("h1")

# Structured properties for every match, in one request.
items = page.eval_on_selector_all("ul li")
for item in items:
print(item["innerText"])

# Full HTML, optionally scoped to a selector.
html_response = page.html(selector="main")
print(html_response["html"])

Fill out a form

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

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

page.wait_for_navigation(wait_until="networkIdle")

Block requests and route through a proxy

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

page = browser.new_page()

# Skipping images and stylesheets cuts page load time on scrape-only runs.
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.
page.proxy(country="US", state="California", sticky=True)

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

Solve a CAPTCHA

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

result = page.solve(type="cloudflare", timeout=30000)
print(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 Playwright.

page.on("console", lambda msg: print(f"[{msg['type']}] {msg['text']}"))
page.on("request", lambda req: print(f"{req['method']} {req['url']}"))

# Response bodies are not fetched by default, since streaming them is expensive.
page.on("response", lambda res: print(f"{res['status']} {res['url']}"))

# Non-fatal streaming and handshake failures surface here instead of raising.
page.on("error", lambda err: print(err))

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

Handle errors

BAP raises typed errors, so you can tell a dead socket apart from a slow page. ConnectionError and TimeoutError share names with Python's builtin exceptions, so import them explicitly rather than relying on except clauses written for the builtins.

from bap import BrowserQLError, ConnectionError, TimeoutError

try:
with browser.page() as page:
page.goto("https://example.com", timeout=5000)
except TimeoutError:
print("operation timed out")
except ConnectionError:
print("websocket connection failed")
except BrowserQLError as error:
print("browserql returned errors:", error)

API reference

The reference is generated from the public exports of bap-py, covering the async and sync browser APIs, every generated option and response type, and public errors.

FAQ & Troubleshooting

Why does add_script_tag fail with a syntax error?

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 Playwright 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 str | None, and it takes no arg. Serialize inside the page with JSON.stringify() and parse the result yourself.

Why is eval_on_selector_all() returning dicts instead of my mapped values?

BAP's eval_on_selector_all() delegates to map_selector() rather than running an expression in the page, so it returns a list of MapSelectorResponse dicts with element properties such as innerText and innerHTML. Read the key you need off each result.

Next steps

Was this page helpful?