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

Stealth and humanization in BAP

BAP inherits BrowserQL's bot-detection tooling: a managed stealth browser, interaction methods that behave like a person by default, and a proxy method with geo-targeting. This guide covers how to combine the three when a target site blocks plain automation.

Prerequisites
  • A Browserless API token from your account dashboard
  • The BAP SDK installed and connecting, from the BAP Quickstart
  • Residential proxy access on your plan for the proxy examples

Connect to the stealth browser

Switching to stealth is an endpoint change, not a code change. The /stealth/bql route runs a browser whose fingerprint Browserless manages, which is why there's no stealth flag in the SDK: patched automation libraries leave detectable traces, so the fingerprint work happens in the browser build instead of your script.

import Browserless from "@browserless.io/bap-ts";

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = Browserless.connect({
// Same SDK, stealth browser: only the endpoint path changes.
browserWSEndpoint: "wss://production-sfo.browserless.io/stealth/bql",
token: TOKEN,
});

try {
const page = await browser.newPage();
await page.goto("https://example.com");
console.log(await page.title());
} finally {
// Close even when an operation rejects, or the session keeps billing.
await browser.close();
}

Response:

Example Domain

Start with /chromium/bql and move to /stealth/bql when a site challenges or blocks you. Stealth sessions cost more units, so there's no reason to pay for them against sites that don't check.

Interactions are humanized by default

BAP's type() and click() run inside the browser with human-shaped behavior already on:

  • type() delays each keystroke by a random value from the delay range, defaulting to [50, 200] milliseconds.
  • Both methods scroll the element into view first (scroll: true), the way a person would have to.
  • interactable: true on type() hovers the element and confirms it's actually reachable at that position before typing.

You tune this rather than build it. Widen the delay range on sensitive forms, or set delay: [0, 0] in trusted environments where speed matters more than looking human.

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

// A wider delay range reads as slower, more deliberate typing.
await page.type("#email", "user@example.com", { delay: [80, 250] });
await page.type("#password", "hunter2", { delay: [80, 250] });
await page.click("#submit");

Route traffic through a proxy

proxy() routes matching requests through the Browserless residential proxy network with geo-targeting, or through your own proxy server. Like reject(), call it before the navigation whose traffic you want proxied.

// Proxy only document requests through a US residential IP, and keep
// that IP for the whole session.
await page.proxy({
country: "US",
sticky: true,
type: ["document"],
});

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

The options that matter most:

OptionDefaultWhat it does
country, state, cityGeo-target the exit IP. State and city are lowercase with spaces removed, such as "newyorkcity"
networkresidentialPool to use: residential or datacenter. Residential is harder to detect; datacenter is cheaper
stickyReuse the same IP for subsequent matching requests, which you want whenever the site ties session state to IP
url, type, methodProxy only matching requests. Scoping to ["document"] keeps page loads geo-correct while static assets skip the metered proxy
serverUse your own proxy instead. Geo-targeting and sticky throw when combined with this, since they only apply to the Browserless network

Selective proxying is the cost lever: residential bandwidth is metered at a higher unit rate than datacenter, and a page's images and scripts usually don't need to come from a residential IP. Scope with type or url unless the site fingerprints asset requests too. The BQL proxies guide owns the per-network unit pricing, and the generated ProxyOptions reference covers the remaining options, including website-specific preset codes.

Layer the three together

Sites with serious detection get the full stack. Order matters only in that proxy rules should exist before navigation:

const browser = Browserless.connect({
browserWSEndpoint: "wss://production-sfo.browserless.io/stealth/bql",
token: TOKEN,
});

try {
const page = await browser.newPage();
await page.proxy({ country: "GB", sticky: true, type: ["document", "xhr", "fetch"] });

await page.goto("https://example.com/account");
await page.type("#user", "name@example.com", { delay: [80, 250] });
await page.click("#next");
} finally {
await browser.close();
}

If a CAPTCHA still appears after this, solve it in the session with BAP's CAPTCHA methods rather than starting over.

FAQ & Troubleshooting

I'm still being detected on /chromium/bql

That's the expected escalation path. Move to /stealth/bql, add a residential proxy with sticky: true, and keep the default humanization on type() and click(). If a CAPTCHA appears, solve it instead of retrying the navigation.

Why does proxy() throw when I pass country with server?

country, state, city, and sticky only apply to the Browserless proxy network. With server you're using your own proxy, so geo-targeting belongs to that proxy's configuration instead.

My session's IP changed between requests

Pass sticky: true. Without it, each matching request can exit through a different IP in the pool, which sites that bind cookies or carts to an IP will treat as suspicious.

Does typing faster risk detection?

On protected sites, yes. The default [50, 200] millisecond range exists because instant keystrokes are a classic automation signal. Narrow it only where you've confirmed the target doesn't score input timing.

Next steps

Was this page helpful?