SDK Reference
Two SDKs, one product: anti-detect-browser on npm and antibrow on PyPI. They share a cache directory, a profile format and an account, so a profile created from one opens in the other and in the desktop app. Either way you launch a profile and receive real Playwright objects over CDP, so the selectors, assertions and traces you already wrote keep working. Windows, macOS and Linux. Local profiles are unlimited on every plan; paid plans add concurrency, cloud sync, managed proxies and Live View.
Node 18 or newer.
npm install anti-detect-browser import { AntiDetectBrowser } from 'anti-detect-browser'
const ab = new AntiDetectBrowser({ key: 'your-api-key' })
const { page, context, browser } = await ab.launch({
profile: 'account-01',
label: 'account-01',
})
// Plain Playwright from here.
await page.goto('https://example.com')
await page.getByRole('button', { name: 'Sign in' }).click()
await browser.close() Python 3.9 to 3.13. You do not need playwright install - the SDK drives its own engine, not Playwright's bundled browsers - but the playwright package is required for its client library.
pip install antibrow
python -m antibrow login # stores your API key
python -m antibrow install # downloads the engine (optional: the first launch does it) from antibrow import launch
browser = launch(profile="account-01")
# Plain Playwright from here.
page = browser.new_page()
page.goto("https://example.com")
page.get_by_role("button", name="Sign in").click()
browser.close() As a context manager, with a proxy and a matching timezone:
from antibrow import launch
with launch(
profile="scraper-eu",
headless=True,
proxy="http://user:pass@gate.example.com:8080",
geoip=True, # timezone and WebRTC follow the proxy exit
) as browser:
page = browser.new_page()
page.goto("https://example.com")
print(browser.timezone, browser.public_ip) Async, for agents and concurrent crawls. The blocking prep work runs in a worker thread, so the event loop is never held up:
import asyncio
from antibrow import launch_async
async def main():
browser = await launch_async(profile="agent-01")
page = await browser.new_page()
await page.goto("https://example.com")
print(await page.title())
await browser.close()
asyncio.run(main()) new AntiDetectBrowser(options)
key | string | required | API key. Exchanged for a short-lived engine license token. |
server | string | optional | License server base URL. Defaults to the hosted one. |
cacheDir | string | optional | Root directory for engines and profiles. Defaults to ~/.anti-detect-browser. |
relayUrl | string | optional | Live View relay endpoint. |
proxyHost | string | optional | Host used when a managed proxy is bound. |
notify | (message) => void | optional | Progress callback for downloads and launches. |
launch(options)
profile | string | required | Profile name, and the directory name on disk. Reuse it to reuse the identity. |
label | string | optional | Label shown in the browser window. |
color | string | optional | Theme colour, hex. |
tags | string[] | optional | Tags stored with the profile. |
proxy | string | optional | Your own proxy: protocol://user:pass@host:port |
proxyId | string | optional | Managed proxy id. Activates it and meters monthly quota. |
headless | boolean | optional | Run without a visible window. See the platform note below. |
userDataDir | string | optional | Override the profile directory. |
liveView | boolean | options | optional | Stream the session. Options: quality, maxWidth, maxHeight, everyNthFrame. |
updateKernelBeforeLaunch | boolean | optional | Check for and install an engine update first. |
deviceType | 'desktop' | 'android' | optional | Simulate an Android phone - runs on this machine, not on a physical device, and needs none. Free on every plan. New profiles only; an existing profile keeps its device type. |
realFingerprint | boolean | optional | Draw the identity from the Captured-machine fingerprint library instead of generating one. Paid plans only - a Free-plan request is rejected. New profiles only. |
headless is platform-dependent. On Windows the window moves off-screen. On Linux it needs Xvfb. On macOS it is currently a no-op and the window is shown. Better said here than discovered later.
What launch() returns
page | Page | A real Playwright Page. | |
context | BrowserContext | A real Playwright BrowserContext, connected over CDP. | |
browser | { close() } | A convenience handle, NOT a Playwright Browser. It has close() and nothing else. | |
profileDir | string | Where this profile lives on disk. | |
sessionKey | string | Identifies the session, used by Live View. | |
viewUrl | string | Dashboard URL for watching the session live. | |
browser is not a Playwright Browser. It is a handle whose only method is close(). A profile runs as a persistent context, so browser.newContext() and browser.contexts() do not exist, and context.browser() returns null. Use the context you were handed.
openProfile(options)
The lower-level entry point that launch() is built on. Use it when you want the session object directly, the exit-IP geo, archive-sync callbacks, or the per-profile engine switches.
import { openProfile } from 'anti-detect-browser'
const session = await openProfile({
key: 'your-api-key',
profileName: 'account-01',
proxyUrl: 'http://user:pass@host:5001',
onProgress: (m) => console.log(m),
})
console.log(session.geo) // { ip, country, countryCode, city, timezone }
const page = session.context.pages()[0] ?? await session.context.newPage()
await page.goto('https://example.com')
await session.close() profileName | string | required | Profile name. Also the cache subdirectory. |
key | string | optional | API key. Required unless you pass licenseToken. |
licenseToken | string | optional | A pre-fetched license token, used instead of requesting one. |
server | string | optional | License server base URL. |
proxyUrl | string | optional | Proxy URL. Timezone, locale and geolocation follow its exit IP. |
label | string | optional | Address-bar label. Defaults to profileName. |
headless | boolean | optional | See the platform note under launch(). |
cacheDir | string | optional | Root cache directory. |
profileDir | string | optional | Explicit profile directory. Takes precedence over cacheDir + profileName. |
kernelVersion | string | optional | New profiles only. Existing ones keep the version in their persona. |
updateKernelBeforeLaunch | boolean | optional | Check for an engine update first. |
deviceType | 'desktop' | 'android' | optional | Simulate an Android phone - runs on this machine, not on a physical device, and needs none. Free on every plan. New profiles only. |
realFingerprint | boolean | optional | Draw the identity from the Captured-machine fingerprint library instead of generating one. Paid plans only. New profiles only. |
canvasNoise | boolean | optional | Canvas and WebGL noise. Off disables it for this profile. |
apiLog | 'off' | 'curated' | 'all' | optional | Write an API call log alongside the profile. |
webauthnCapture | boolean | optional | Capture and replay passkeys. On by default. |
archiveGetUrl | string | optional | Presigned URL to restore profile state from. |
getArchivePutUrl | () => Promise | optional | Resolved after exit. Prefer this over archivePutUrl. |
archivePutUrl | string | optional | Presigned upload URL. A presign rarely outlives a browsing session. |
onProgress | (message) => void | optional | Progress callback. |
onArchiveSync | (event) => void | optional | Sync events: phase download|upload, state start|done|error. |
The session it resolves to
context | BrowserContext | Playwright context connected to the running engine. | |
close | () => Promise | Close the browser. Triggers the archive upload if one is configured. | |
onExit | (cb) => void | Register a callback for when the browser exits on its own. | |
profileDir | string | Where this profile lives on disk. | |
wsEndpoint | string | The CDP WebSocket endpoint. | |
geo | ProxyGeo? | Exit-IP geo when a proxy is bound: ip, country, countryCode, city, timezone. | |
archiveUpload | Promise? | Awaitable handle for the post-exit upload. | |
launch(profile="default", **options)
Starts the engine and returns a handle that is ready to drive. launch_async() takes exactly the same options.
profile | str | "default" | Profile name, and the directory name on disk. Same name, same identity, cookies and storage. |
headless | bool | False | Run without a visible window. See the platform note below. |
proxy | str | dict | None | protocol://user:pass@host:port, or Playwright's {"server", "username", "password"}. |
geoip | bool | True | Resolve the proxy's exit IP through the proxy and make timezone and WebRTC match it. No-op without a proxy. |
timezone | str | None | Force an IANA timezone, overriding the geo lookup. |
api_key | str | env or key file | API key. Falls back to ANTIBROW_API_KEY, then ~/.antibrow/license.key. |
server | str | hosted | License server base URL. |
cache_dir | str | Path | ~/.anti-detect-browser | Root directory for engines and profiles. |
profile_dir | str | Path | None | Exact profile directory. Takes precedence over cache_dir and profile. |
kernel_version | str | newest | New profiles only. Existing ones keep the version frozen in their persona. |
label | str | profile name | Address-bar label, for telling windows apart. |
args | list[str] | None | Extra Chromium switches. |
proxy_auth | "native" | "extension" | "native" | How proxy credentials are answered. Native handles them inside the network stack, with no extension loaded. |
license_token | str | None | Use a pre-fetched token instead of calling the server. |
license_provider | callable | None | Return a token from your own issuer. |
update_kernel | bool | False | Check for an engine update and install it before launching. |
device_type | 'desktop' | 'android' | None | Simulate an Android phone - runs on this machine, not on a physical device, and needs none. Free on every plan. New profiles only; an existing profile keeps its device type. |
real_fingerprint | bool | False | Draw the identity from the Captured-machine fingerprint library instead of generating one. Paid plans only - a Free-plan key raises. New profiles only. |
reuse_initial_page | bool | True | Let the first new_page() return Chromium's initial blank tab instead of opening a second one. |
timeout | float | 120.0 | Seconds to wait for the browser to come up. |
on_progress | callable | None | Receives progress lines for downloads and launches. |
headless is platform-dependent. On Windows the window moves off-screen, because real headless Chromium has a fingerprint of its own. On Linux run headful under Xvfb. On macOS it is currently a no-op and the window is shown.
The Antibrow handle
Attribute lookups fall through to the Playwright BrowserContext, so the handle behaves like one without you having to reach for .context first.
browser = launch(profile="account-01")
# Attribute lookups fall through to the Playwright BrowserContext,
# so anything you would call on a context works on the handle.
browser.add_init_script("...")
browser.add_cookies([...])
browser.pages new_page() | Page | A real Playwright Page. The first call reuses Chromium's initial blank tab; use context.new_page() if you always want a fresh one. | |
context | BrowserContext | The raw Playwright BrowserContext. Every profile is persistent, so context.browser() is null. | |
browser | Browser | The raw Playwright Browser, i.e. the CDP connection. | |
page | Page | The first page, created on demand. | |
cdp_url | str | http://127.0.0.1:PORT - what crawl4ai and puppeteer.connect want. | |
cdp_endpoint | str | The CDP WebSocket endpoint. | |
profile_dir | Path | Where this profile lives on disk. | |
persona | Persona | The frozen identity: UA, GPU, screen, seeds. | |
timezone | str | The browser's timezone, resolved from the proxy when geoip is on. | |
public_ip | str | None | The proxy exit IP, when one was resolved. | |
kernel_version | str | The engine version this profile is pinned to. | |
pid | int | None | The engine process id. | |
plan | LaunchPlan | Everything resolved for this launch. plan.redacted_args() masks the secrets, so it is safe to paste into a bug report. | |
close() | None | Close the browser and reap the process tree. | |
Other entry points
from antibrow import (
launch_async, launch_persistent_context,
launch_persistent_context_async, prepare_launch,
)
browser = await launch_async(profile="p1") # asyncio twin of launch()
context = launch_persistent_context(profile="p1") # a literal Playwright BrowserContext
plan = prepare_launch(profile="p1") # resolve everything, start nothing
print(plan.redacted_args()) # the command line with the token and proxy password masked prepare_launch() resolves the exact executable, arguments, persona and timezone a launch would use without starting a process, which makes it the thing to run for tests, dry runs and bug reports.
Managing profiles
Local profiles need no API call to create: launching a name that does not exist creates it. The server-side functions below are for cloud-synced profiles, which are a paid-plan feature.
import {
listProfiles, getProfileDir, defaultCacheDir,
getOrCreateProfile, deleteProfile,
} from 'anti-detect-browser'
// Local profiles are directories on disk. Unlimited on every plan.
listProfiles() // string[] of profile names
getProfileDir('account-01') // absolute path to that profile
defaultCacheDir() // ~/.anti-detect-browser by default
// Cloud-synced profiles talk to the server and need an API key.
const p = await getOrCreateProfile({ key, name: 'account-01', tags: ['ads'] })
await deleteProfile({ key, id: p.id })
A profile is a directory holding persona.json (the identity, written once and never regenerated), fp-config.json (that persona serialised for the engine, rewritten each launch) and user-data/ (Chromium's own state). The cache root is shared with the Node SDK and the desktop app, so a profile created here shows up there.
from antibrow import list_profiles, profile_dir, default_cache_dir
# Local profiles are directories on disk. Unlimited on every plan, and
# launching a name that does not exist creates it.
list_profiles() # ['account-01', 'account-02']
profile_dir("account-01") # Path to that profile
default_cache_dir() # ~/.anti-detect-browser by default
# Or point at an exact directory, e.g. a volume mounted into CI:
launch(profile_dir="/data/profiles/account-01") Cloud sync and Live View are Node SDK and desktop-app features today; the Python package is local-only. Moving an identity between machines works by copying the profile directory, or by exporting a portable archive from the Node SDK or the desktop app.
Device profiles
Two independent options, both resolved only the first time a profile is created - an existing profile keeps whatever they resolved to, frozen in its persona.
deviceType: 'android'simulates a full Android phone environment - user agent, Client Hints, touch input, screen - that runs on this same Windows, macOS or Linux machine. It does not run on a phone, and no physical device is required. Free on every plan, including Free.realFingerprint: truedraws the identity from the Captured-machine fingerprint library, a pool of fingerprints captured from real physical machines, instead of generating one locally. Paid plans only - a Free-plan request gets a 403 from the server rather than a silent fallback to a generated identity. Works with either device type.
// A simulated Android phone - runs on this same Windows/macOS/Linux
// machine, not on a physical device, and needs none. Free on every
// plan. Only takes effect the first time this profile is created.
await ab.launch({ profile: 'mobile-01', deviceType: 'android' })
// Draw the identity from a real captured device instead of generating
// one. Paid plans only - a Free-plan key gets a 403. Works for either
// device type, and also only applies at first creation.
await ab.launch({ profile: 'captured-01', realFingerprint: true })
// Combine both: an Android identity pulled from the captured library.
await ab.launch({
profile: 'captured-mobile-01',
deviceType: 'android',
realFingerprint: true,
})# A simulated Android phone - runs on this same Windows/macOS/Linux
# machine, not on a physical device, and needs none. Free on every
# plan. Only takes effect the first time this profile is created.
browser = launch(profile="mobile-01", device_type="android")
# Draw the identity from a real captured device instead of generating
# one. Paid plans only - a Free-plan key raises. Works for either
# device type, and also only applies at first creation.
browser = launch(profile="captured-01", real_fingerprint=True)Android needs engine 151 or newer. The SDK downloads it automatically the first time it's needed. An already-installed older engine is not silently downgraded to - the launch is refused instead, because an Android configuration on a kernel without mobile support would produce a profile whose user agent claims Android while its Client Hints still claim desktop.
Export and import
Profiles export to a portable .fpprofile archive on every plan, including Free, because the ability to leave should not be a paid feature. It carries the fingerprint, cookies, storage, bookmarks and passkeys. Treat the file as a credential: whatever can open it is signed in as that identity.
import { exportProfileArchive, importProfileArchive, getProfileDir } from 'anti-detect-browser'
import { writeFileSync, readFileSync } from 'node:fs'
// Carries the fingerprint, cookies, storage, bookmarks and passkeys.
const buf = exportProfileArchive(getProfileDir('account-01'), meta)
writeFileSync('account-01.fpprofile', buf)
// Restore it into another machine's profile directory.
importProfileArchive(readFileSync('account-01.fpprofile'), getProfileDir('account-01')) Proxies
Pass proxy for your own or proxyId for a managed one. Either way the engine derives timezone, locale and geolocation from the exit IP, so the browser does not report your own clock through someone else's address, which is the most common way a correct proxy setup gives itself away. When a proxy is bound, openProfile() resolves with geo describing that exit. Claiming a managed proxy is free; quota is metered when you launch with it.
Authenticated proxies work as-is - put the credentials in the URL (http, https or socks5) and the engine answers the challenge itself: HTTP and HTTPS 407 in the network stack, SOCKS5 by RFC 1929 negotiation. Nothing is loaded into chrome://extensions, which is exactly the kind of tell an antidetect browser must not have, and the credentials never reach a renderer process. See Managed Proxies.
With geoip=True (the default) the exit IP is looked up through the proxy before launch, and its timezone is written into the fingerprint - so the browser does not report your own clock through someone else's address, which is the most common way a correct proxy setup gives itself away.
launch(proxy="http://user:pass@gate.example.com:8080")
launch(proxy="socks5://user:pass@127.0.0.1:1080")
launch(proxy={"server": "http://gate.example.com:8080", "username": "u", "password": "p"})
browser = launch(profile="p1", proxy="socks5://user:pass@127.0.0.1:1080")
print(browser.public_ip, browser.timezone) # 203.0.113.7 America/Los_Angeles
Credentials are answered inside the engine: HTTP and HTTPS 407 challenges in the network stack, SOCKS5 by RFC 1929 negotiation. Nothing is loaded into chrome://extensions, which is exactly the kind of tell an antidetect browser must not have. See Managed Proxies.
Framework integrations
Every integration works the same way: the SDK starts the browser, and you hand its CDP endpoint to whatever wants to drive it. Playwright needs no glue at all - the handle already is a context.
browser = launch(profile="agent-01")
browser.cdp_url # http://127.0.0.1:54321 - crawl4ai, puppeteer.connect
browser.cdp_endpoint # ws://127.0.0.1:54321/devtools/browser/...
# browser-use
Agent(task="...", llm=llm, browser=Browser(cdp_url=browser.cdp_url))
# crawl4ai
AsyncWebCrawler(config=BrowserConfig(cdp_url=browser.cdp_url, headless=False))
# Scrapling
DynamicFetcher.fetch("https://example.com", cdp_url=browser.cdp_endpoint) Selenium cannot attach to a CDP-only endpoint without a matching chromedriver, so there is no Selenium binding today. If you are migrating from it, Playwright is the shortest path. Driving the browser from an AI agent instead? See the MCP server.
Live View
Stream a running session to the dashboard. Paid plans only.
const { page, viewUrl } = await ab.launch({
profile: 'account-01',
liveView: { quality: 60, maxWidth: 1280, everyNthFrame: 2 },
})
console.log(viewUrl) // open in the dashboard to watch the session Engine updates
The engine downloads on first use and is cached under the cache directory. New profiles use the current default version; an existing profile keeps the version recorded in its persona, so its fingerprint does not shift underneath it after an update.
await ab.checkKernelUpdates()
if (await ab.hasKernelUpdate()) {
await ab.updateKernel(undefined, (m) => console.log(m))
}
// Or fold it into a launch:
await ab.launch({ profile: 'account-01', updateKernelBeforeLaunch: true })# Fold the check into a launch:
launch(profile="account-01", update_kernel=True)
# Or from the CLI:
# python -m antibrow install --forceCLI and environment
python -m antibrow install [--version 151] [--force] # get the engine
python -m antibrow info # engines, profiles, license
python -m antibrow login [--key ab_live_...] # store an API key
python -m antibrow version # SDK and default engine version antibrow ... works too, as a console script. info is the first thing to run when something is wrong: it prints the cache directory, every engine version with its install status, all profiles with the version they are pinned to, and where your API key was found.
ANTIBROW_API_KEY | API key. ANTI_DETECT_BROWSER_KEY is accepted too, so one variable covers both SDKs. | ||
ANTIBROW_LICENSE_TOKEN | A pre-fetched license token. Skips the server call entirely. | ||
ANTIBROW_CACHE_DIR | Root for engines and profiles. Defaults to ~/.anti-detect-browser. | ||
ANTIBROW_SERVER | License server base URL. | ||
Docker
The Linux engine runs headful under Xvfb, because real headless Chromium has its own fingerprint. This image works on both linux/amd64 and linux/arm64; the matching build is chosen from the container's CPU.
FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
xvfb xauth libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \
libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \
libgbm1 libasound2 libpango-1.0-0 libcairo2 fonts-liberation ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir antibrow
COPY script.py .
# Two details this line earns the hard way: xvfb-run needs xauth (without it you get
# "xauth command not found"), and it must not be PID 1 - as PID 1 it never reaps the
# browser's children, so the container hangs after a successful launch with no output.
CMD ["sh", "-c", "xvfb-run -a python script.py"]
Mount the cache directory as a volume (-v antibrow-cache:/root/.anti-detect-browser) so the engine and your profiles survive between runs, otherwise every container downloads the engine again.
Errors
from antibrow import AntibrowError, ConcurrencyLimitError, LicenseError
try:
browser = launch()
except ConcurrencyLimitError:
... # the plan's simultaneous-browser cap is in use
except LicenseError:
... # no API key, or the server rejected it
except AntibrowError:
... # engine download, unsupported platform, proxy, launch failure AntibrowError | Base class. Every intentional failure derives from it. | ||
LicenseError | No API key, or the server rejected it. | ||
ConcurrencyLimitError | The plan's simultaneous-browser cap is already in use. | ||
KernelDownloadError | The engine could not be downloaded or extracted. | ||
LaunchError | The engine started but never reported a usable CDP endpoint. | ||
ProxyError | The proxy string could not be parsed, or the exit lookup failed. | ||
UnsupportedPlatformError | No engine build exists for this OS and CPU. | ||
Server API
Both SDKs call these for you, so you rarely need them directly. They are here for anyone integrating without an SDK. Authenticate with Authorization: Bearer <api-key>.
POST /api/v1/engine/session | Issue an engine session for a launch. | ||
GET /api/v1/profiles | List cloud-synced profiles. | ||
POST /api/v1/profiles | Create a cloud-synced profile. | ||
GET /api/v1/profiles/:name | Fetch one profile. Rate limited to 1 request per minute per profile. | ||
DELETE /api/v1/profiles/:name | Delete a cloud-synced profile and its stored archive. | ||
GET/PUT /api/v1/profiles/:name/archive | Presigned URLs for profile-state sync. The PUT presign is short-lived, so request it at the moment you upload rather than at launch. | ||
GET /api/v1/devices/pick?os=android|windows | Draw one fingerprint from the Captured-machine fingerprint library. Paid plans only (403 on Free). Returns { device }. | ||
GET /api/v1/proxies | Managed proxies assigned to you, plus monthly quota. | ||
GET /api/v1/account | Plan, concurrency cap and usage. | ||
Driving a browser from an AI agent instead of writing code? The same profiles are reachable over the MCP server.