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

OS emulation in BAP

Browserless sessions run on Linux, so they report a Linux fingerprint by default. The emulationOs query parameter switches the session to a coherent Windows, macOS, or Android identity, and BAP takes it on the connection URL. This guide covers setting it at connect time, emulating a phone, and confirming what the session actually reports.

Prerequisites

Set the OS on the connection URL

emulationOs is a query parameter on the endpoint, not a connect option or a method, so it goes straight into browserWSEndpoint. The SDK appends &token= when the endpoint already carries a query string, so the two coexist without you building the URL by hand.

There's no route to get wrong here. BAP only speaks to /bql endpoints, and every one of them runs stealth-hardened, which is where emulationOs takes effect. On plain CDP routes like /chromium the parameter does nothing, but BAP can't connect to those in the first place.

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

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = Browserless.connect({
// emulationOs is a URL parameter, so it belongs on the endpoint itself.
browserWSEndpoint:
"wss://production-sfo.browserless.io/chromium/bql?emulationOs=windows",
token: TOKEN,
});

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

console.log(await page.evaluate("navigator.platform"));
} finally {
// Close even when an operation rejects, or the session keeps billing.
await browser.close();
}

Response:

Win32

connect() opens no socket. newPage() is what provisions the session, so the emulated identity is already applied to the page it hands back and you never have to wait for it to settle.

Accepted values

ValueEmulated identity
windowsWindows / Chrome on Win64
macosmacOS / Chrome on Intel Mac
linuxNative Linux identity (the default when omitted)
androidMobile Chrome on Android, covered below

Values are exact-match and lowercase, so macOS is not macos. iPhone, iOS, and Safari aren't supported. Setting emulationOs swaps a whole stack of signals at once, including the User-Agent, UA Client Hints, navigator.platform, GPU renderer, CPU core count, fonts, voices, and audio latency, and they stay consistent for the life of the session. The BQL OS emulation guide has the full list.

Emulate an Android phone

emulationOs=android is the one value that changes more than OS-derived signals: it also applies a phone viewport, a high device pixel ratio, touch input, and portrait orientation. The same script becomes a mobile session with no other edits, which is what makes responsive pages reflow to their mobile layout. Swap the one value on the endpoint:

wss://production-sfo.browserless.io/chromium/bql?emulationOs=android

Reading the device metrics back confirms the session is really a phone:

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

// evaluate() returns a string, so serialize the signals inside the page.
console.log(await page.evaluate(`JSON.stringify({
platform: navigator.platform,
width: window.innerWidth,
dpr: window.devicePixelRatio,
touch: navigator.maxTouchPoints
})`));

Response:

{"platform":"Linux armv81","width":412,"dpr":3.5,"touch":5}

The exact numbers depend on which phone you get. Omit emulatedDevice and Browserless picks a real device and keeps it stable for the session, or name one to pin it:

wss://production-sfo.browserless.io/chromium/bql?emulationOs=android&emulatedDevice=pixel-8-pro

Device slugs are case-sensitive and matched exactly. See supported mobile devices for the list. emulatedDevice requires emulationOs=android, and sending it alone is rejected. Because connect() opens no socket, that rejection surfaces on the call that does: newPage() in TypeScript, entering browser.page() in Python.

What it doesn't change

emulationOs only sets OS-derived browser signals. Timezone, locale, and Accept-Language follow your proxy's location instead, and WebRTC handling is applied uniformly to every stealth session. A Windows identity served from a mismatched timezone is itself a detection signal, so match the proxy region to the OS you're emulating. On a session connected with ?emulationOs=macos, call proxy() before the navigation you want proxied:

// Pair the emulated OS with a region-matched IP for a coherent profile.
await page.proxy({ country: "US", sticky: true, type: ["document"] });
await page.goto("https://example.com");

FAQ & Troubleshooting

I set emulationOs but navigator.platform still says Linux

Check the casing and spelling first, since values are exact-match lowercase and macOS isn't macos. Then confirm the parameter survived into the URL you passed as browserWSEndpoint, because it's easy to lose when the endpoint is built from string concatenation. Read the identity back with evaluate("navigator.platform") to confirm.

Can I change the emulated OS mid-session?

No. It's fixed when the socket opens, so switching means closing the browser and calling connect() again with a different endpoint. Run the two sessions separately when you need to compare a desktop and a mobile identity.

Which /bql route should I use it on?

Any of them: /chromium/bql, /chrome/bql, and /stealth/bql. Start on /chromium/bql and move to /stealth/bql when a site still challenges you, the same escalation described in stealth and humanization.

Does emulating Android change how I interact with the page?

No. click() and type() work the same, and both still scroll the target into view first. What changes is the layout you're targeting: a responsive site serves its mobile DOM, so selectors written against the desktop layout may not exist.

Next steps

Was this page helpful?