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.
- 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.
- TypeScript
- Python
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();
}
import bap.sync_api as bap
TOKEN = "YOUR_API_TOKEN_HERE"
with bap.Browserless.connect(
# Same SDK, stealth browser: only the endpoint path changes.
browser_ws_endpoint="wss://production-sfo.browserless.io/stealth/bql",
token=TOKEN,
) as browser:
with browser.page() as page:
page.goto("https://example.com")
print(page.title())
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 thedelayrange, defaulting to[50, 200]milliseconds.- Both methods scroll the element into view first (
scroll: true), the way a person would have to. interactable: trueontype()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.
- TypeScript
- Python
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");
page.goto("https://example.com/login")
# A wider delay range reads as slower, more deliberate typing.
page.type("#email", "user@example.com", delay=[80, 250])
page.type("#password", "hunter2", delay=[80, 250])
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.
- TypeScript
- Python
// 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");
# Proxy only document requests through a US residential IP, and keep
# that IP for the whole session.
page.proxy(country="US", sticky=True, type=["document"])
page.goto("https://example.com")
The options that matter most:
| Option | Default | What it does |
|---|---|---|
country, state, city | — | Geo-target the exit IP. State and city are lowercase with spaces removed, such as "newyorkcity" |
network | residential | Pool to use: residential or datacenter. Residential is harder to detect; datacenter is cheaper |
sticky | — | Reuse the same IP for subsequent matching requests, which you want whenever the site ties session state to IP |
url, type, method | — | Proxy only matching requests. Scoping to ["document"] keeps page loads geo-correct while static assets skip the metered proxy |
server | — | Use 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:
- TypeScript
- Python
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();
}
with bap.Browserless.connect(
browser_ws_endpoint="wss://production-sfo.browserless.io/stealth/bql",
token=TOKEN,
) as browser:
with browser.page() as page:
page.proxy(country="GB", sticky=True, type=["document", "xhr", "fetch"])
page.goto("https://example.com/account")
page.type("#user", "name@example.com", delay=[80, 250])
page.click("#next")
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.