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)

keystringrequiredAPI key. Exchanged for a short-lived engine license token.
serverstringoptionalLicense server base URL. Defaults to the hosted one.
cacheDirstringoptionalRoot directory for engines and profiles. Defaults to ~/.anti-detect-browser.
relayUrlstringoptionalLive View relay endpoint.
proxyHoststringoptionalHost used when a managed proxy is bound.
notify(message) => voidoptionalProgress callback for downloads and launches.

launch(options)

profilestringrequiredProfile name, and the directory name on disk. Reuse it to reuse the identity.
labelstringoptionalLabel shown in the browser window.
colorstringoptionalTheme colour, hex.
tagsstring[]optionalTags stored with the profile.
proxystringoptionalYour own proxy: protocol://user:pass@host:port
proxyIdstringoptionalManaged proxy id. Activates it and meters monthly quota.
headlessbooleanoptionalRun without a visible window. See the platform note below.
userDataDirstringoptionalOverride the profile directory.
liveViewboolean | optionsoptionalStream the session. Options: quality, maxWidth, maxHeight, everyNthFrame.
updateKernelBeforeLaunchbooleanoptionalCheck for and install an engine update first.
deviceType'desktop' | 'android'optionalSimulate 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.
realFingerprintbooleanoptionalDraw 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

pagePageA real Playwright Page.
contextBrowserContextA real Playwright BrowserContext, connected over CDP.
browser{ close() }A convenience handle, NOT a Playwright Browser. It has close() and nothing else.
profileDirstringWhere this profile lives on disk.
sessionKeystringIdentifies the session, used by Live View.
viewUrlstringDashboard 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()
profileNamestringrequiredProfile name. Also the cache subdirectory.
keystringoptionalAPI key. Required unless you pass licenseToken.
licenseTokenstringoptionalA pre-fetched license token, used instead of requesting one.
serverstringoptionalLicense server base URL.
proxyUrlstringoptionalProxy URL. Timezone, locale and geolocation follow its exit IP.
labelstringoptionalAddress-bar label. Defaults to profileName.
headlessbooleanoptionalSee the platform note under launch().
cacheDirstringoptionalRoot cache directory.
profileDirstringoptionalExplicit profile directory. Takes precedence over cacheDir + profileName.
kernelVersionstringoptionalNew profiles only. Existing ones keep the version in their persona.
updateKernelBeforeLaunchbooleanoptionalCheck for an engine update first.
deviceType'desktop' | 'android'optionalSimulate an Android phone - runs on this machine, not on a physical device, and needs none. Free on every plan. New profiles only.
realFingerprintbooleanoptionalDraw the identity from the Captured-machine fingerprint library instead of generating one. Paid plans only. New profiles only.
canvasNoisebooleanoptionalCanvas and WebGL noise. Off disables it for this profile.
apiLog'off' | 'curated' | 'all'optionalWrite an API call log alongside the profile.
webauthnCapturebooleanoptionalCapture and replay passkeys. On by default.
archiveGetUrlstringoptionalPresigned URL to restore profile state from.
getArchivePutUrl() => PromiseoptionalResolved after exit. Prefer this over archivePutUrl.
archivePutUrlstringoptionalPresigned upload URL. A presign rarely outlives a browsing session.
onProgress(message) => voidoptionalProgress callback.
onArchiveSync(event) => voidoptionalSync events: phase download|upload, state start|done|error.

The session it resolves to

contextBrowserContextPlaywright context connected to the running engine.
close() => PromiseClose the browser. Triggers the archive upload if one is configured.
onExit(cb) => voidRegister a callback for when the browser exits on its own.
profileDirstringWhere this profile lives on disk.
wsEndpointstringThe CDP WebSocket endpoint.
geoProxyGeo?Exit-IP geo when a proxy is bound: ip, country, countryCode, city, timezone.
archiveUploadPromise?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.

profilestr"default"Profile name, and the directory name on disk. Same name, same identity, cookies and storage.
headlessboolFalseRun without a visible window. See the platform note below.
proxystr | dictNoneprotocol://user:pass@host:port, or Playwright's {"server", "username", "password"}.
geoipboolTrueResolve the proxy's exit IP through the proxy and make timezone and WebRTC match it. No-op without a proxy.
timezonestrNoneForce an IANA timezone, overriding the geo lookup.
api_keystrenv or key fileAPI key. Falls back to ANTIBROW_API_KEY, then ~/.antibrow/license.key.
serverstrhostedLicense server base URL.
cache_dirstr | Path~/.anti-detect-browserRoot directory for engines and profiles.
profile_dirstr | PathNoneExact profile directory. Takes precedence over cache_dir and profile.
kernel_versionstrnewestNew profiles only. Existing ones keep the version frozen in their persona.
labelstrprofile nameAddress-bar label, for telling windows apart.
argslist[str]NoneExtra Chromium switches.
proxy_auth"native" | "extension""native"How proxy credentials are answered. Native handles them inside the network stack, with no extension loaded.
license_tokenstrNoneUse a pre-fetched token instead of calling the server.
license_providercallableNoneReturn a token from your own issuer.
update_kernelboolFalseCheck for an engine update and install it before launching.
device_type'desktop' | 'android'NoneSimulate 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_fingerprintboolFalseDraw 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_pageboolTrueLet the first new_page() return Chromium's initial blank tab instead of opening a second one.
timeoutfloat120.0Seconds to wait for the browser to come up.
on_progresscallableNoneReceives 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()PageA real Playwright Page. The first call reuses Chromium's initial blank tab; use context.new_page() if you always want a fresh one.
contextBrowserContextThe raw Playwright BrowserContext. Every profile is persistent, so context.browser() is null.
browserBrowserThe raw Playwright Browser, i.e. the CDP connection.
pagePageThe first page, created on demand.
cdp_urlstrhttp://127.0.0.1:PORT - what crawl4ai and puppeteer.connect want.
cdp_endpointstrThe CDP WebSocket endpoint.
profile_dirPathWhere this profile lives on disk.
personaPersonaThe frozen identity: UA, GPU, screen, seeds.
timezonestrThe browser's timezone, resolved from the proxy when geoip is on.
public_ipstr | NoneThe proxy exit IP, when one was resolved.
kernel_versionstrThe engine version this profile is pinned to.
pidint | NoneThe engine process id.
planLaunchPlanEverything resolved for this launch. plan.redacted_args() masks the secrets, so it is safe to paste into a bug report.
close()NoneClose 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: true draws 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 --force

CLI 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_KEYAPI key. ANTI_DETECT_BROWSER_KEY is accepted too, so one variable covers both SDKs.
ANTIBROW_LICENSE_TOKENA pre-fetched license token. Skips the server call entirely.
ANTIBROW_CACHE_DIRRoot for engines and profiles. Defaults to ~/.anti-detect-browser.
ANTIBROW_SERVERLicense 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
AntibrowErrorBase class. Every intentional failure derives from it.
LicenseErrorNo API key, or the server rejected it.
ConcurrencyLimitErrorThe plan's simultaneous-browser cap is already in use.
KernelDownloadErrorThe engine could not be downloaded or extracted.
LaunchErrorThe engine started but never reported a usable CDP endpoint.
ProxyErrorThe proxy string could not be parsed, or the exit lookup failed.
UnsupportedPlatformErrorNo 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/sessionIssue an engine session for a launch.
GET /api/v1/profilesList cloud-synced profiles.
POST /api/v1/profilesCreate a cloud-synced profile.
GET /api/v1/profiles/:nameFetch one profile. Rate limited to 1 request per minute per profile.
DELETE /api/v1/profiles/:nameDelete a cloud-synced profile and its stored archive.
GET/PUT /api/v1/profiles/:name/archivePresigned 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|windowsDraw one fingerprint from the Captured-machine fingerprint library. Paid plans only (403 on Free). Returns { device }.
GET /api/v1/proxiesManaged proxies assigned to you, plus monthly quota.
GET /api/v1/accountPlan, concurrency cap and usage.

Driving a browser from an AI agent instead of writing code? The same profiles are reachable over the MCP server.