Antidetect browser API with Puppeteer and Playwright
An antidetect browser API lets your scripts manage browser profiles and connect to their web pages. The WGBrowser Local API runs on your computer, opens an existing profile, and returns a Chrome DevTools Protocol (CDP) address for Puppeteer or Playwright. You can use it to query profiles, run page operations, and close each profile through its normal sync process.
This guide covers setup, authentication, and complete Node.js and Python examples. If you already use BitBrowser scripts, read the BitBrowser migration guide alongside it. For an AI assistant that chooses browser tools from your instructions, use MCP on the same local service.
Enable Local API
Sign in to the desktop client, open Settings → Local API, and enable Turn on Local API. The default port is 54345. The first time you enable the service, it creates an API key beginning with fgbk_. Copy the key for your script. If another program uses that port, change Port in the settings and update your script’s base address.
Your plan must include Local API. You must be a team owner, an admin, or a member granted Local API access. The service stops when you sign out or when a change to your permissions or plan removes access. It listens only on 127.0.0.1, so run your script on the computer running the client; another computer cannot connect directly.
Authenticate requests and check results
Send POST requests with JSON bodies and the Content-Type: application/json header. Put your key in x-api-key; Bearer authentication is also supported. A missing or incorrect key returns an unauthorized response. Choosing Regenerate in the client immediately revokes the old key, so update every script that uses it. Keep actual keys out of shared code and source control.
Check both the HTTP status and the response body. A request can return HTTP 200 with success: false, an explanation in msg, and an error code. A profile parameter accepts its UUID or profile number. When migrating an existing integration, replace the source application’s profile IDs with identifiers from your current WGBrowser account.
Prepare a profile and install dependencies
Choose a profile with a working proxy and no active session on another device. Record its profile number. Each example opens that existing profile, visits an example website, prints its title, and closes the profile when finished. Use a dedicated profile without unsaved page content: opening a profile that already runs locally attaches to that window.
For Node.js, use a version with built-in fetch and install puppeteer-core. For Python, install requests and playwright. Save the relevant script below as api-puppeteer.mjs or api-playwright.py. Set your profile number and key in the Bash commands, then run either example. Both connect to the browser started by the client.
# Node.js example dependencies
npm install puppeteer-core
# Python example dependencies
python3 -m pip install requests playwright
# Connection settings shared by both examples
export FGB_BASE='http://127.0.0.1:54345'
export FGB_PROFILE_ID='1'
export FGB_KEY='fgbk_xxxxxxxx' # Replace with your key from Settings → Local API
# Save the corresponding script below, then run either command
node api-puppeteer.mjs
# python3 api-playwright.pyConnect with Puppeteer
The Node.js example checks /health, calls /browser/open, and passes the returned ws address to Puppeteer. It leaves the profile’s viewport unchanged and selects an existing web page. The CDP connection exposes website tabs; the profile window’s address bar, tab bar, and developer tools are excluded from its page targets.
Add your own page operations where the example reads the title. Its cleanup runs after a successful visit or a page error: it disconnects Puppeteer, then calls /browser/close. Disconnecting alone leaves the window available for manual work. This example explicitly closes the profile so the normal shutdown and sync process can finish.
import puppeteer from 'puppeteer-core';
const base = process.env.FGB_BASE || 'http://127.0.0.1:54345';
const key = process.env.FGB_KEY;
const id = process.env.FGB_PROFILE_ID;
if (!key || !id) throw new Error('Set FGB_KEY and FGB_PROFILE_ID');
async function call(path, body = {}) {
const res = await fetch(base + path, {
method: 'POST',
headers: { 'x-api-key': key, 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
const result = await res.json();
if (!result.success) {
throw new Error(`${result.code || 'API_ERROR'}: ${result.msg || 'request failed'}`);
}
return result.data;
}
await call('/health');
const info = await call('/browser/open', { id });
let browser;
try {
browser = await puppeteer.connect({
browserWSEndpoint: info.ws,
defaultViewport: null
});
const [page] = await browser.pages();
if (!page) throw new Error('The profile has no open page');
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
console.log(await page.title());
} finally {
try {
if (browser) await browser.disconnect();
} finally {
await call('/browser/close', { id });
}
}Connect with Playwright
The Python example uses the same Local API and connects Playwright to the returned CDP address. Use the existing browser context and page. Each browser profile already supplies its own isolated browsing data; creating another isolated context inside it is unsupported. For separate account data, use another profile.
You can create additional tabs through the existing context. Once /browser/open succeeds, the example requests a normal close even if the later CDP connection or page visit fails. Read cloud sync for the timing of cookie and local storage uploads. Close through the API instead of terminating the browser process directly.
import os
import requests
from playwright.sync_api import sync_playwright
BASE = os.environ.get('FGB_BASE', 'http://127.0.0.1:54345')
KEY = os.environ.get('FGB_KEY')
PROFILE_ID = os.environ.get('FGB_PROFILE_ID')
if not KEY or not PROFILE_ID:
raise RuntimeError('Set FGB_KEY and FGB_PROFILE_ID')
def call(path, body=None):
response = requests.post(
BASE + path,
json={} if body is None else body,
headers={'x-api-key': KEY},
timeout=120
)
response.raise_for_status()
result = response.json()
if not result.get('success'):
raise RuntimeError(
f"{result.get('code', 'API_ERROR')}: {result.get('msg', 'request failed')}"
)
return result['data']
call('/health')
info = call('/browser/open', {'id': PROFILE_ID})
try:
with sync_playwright() as playwright:
browser = playwright.chromium.connect_over_cdp(info['ws'])
page = browser.contexts[0].pages[0]
page.goto('https://example.com', wait_until='domcontentloaded')
print(page.title())
finally:
call('/browser/close', {'id': PROFILE_ID})CDP addresses and supported browser operations
The Local API port and a profile’s CDP port serve different purposes. Local API remains at the configured address; each opened profile gets its own debugging address. That address can change after closing and reopening the profile. Read the latest /browser/open response on every run instead of saving a CDP port in your script.
New web tabs are supported. Headless mode and additional isolated browser contexts are not. Puppeteer’s browser.close() also uses the normal profile shutdown process, including sync. The examples use a separate disconnect and close request to make those operations explicit. BitBrowser-compatible paths do not imply support for every behavior an existing browser script may expect.
Troubleshoot connections and occupied profiles
For a refused connection, check that you are signed in, Local API is enabled, and the port matches your script. For an unauthorized response, check whether the key was regenerated. PROFILE_OCCUPIED means another device is using the profile; ask that user to close it normally before retrying. For a missing profile, check its number and your account’s access.
Start with one profile and verify opening, page operations, and closing before adding loops and error logs for multiple accounts. If colleagues need to select profiles, watch progress, and handle manual steps from the client, package the repeatable workflow as an automation plugin. Check API compatibility and your website-specific script behavior separately.
FAQ
What is the default Local API port?
The default port is 54345, and you can change it in Settings → Local API. This is the management API port. Each opened profile receives a separate CDP address, which can change when the profile restarts. Your script should read that address from /browser/open rather than reuse the management port for browser debugging.
Can I use my BitBrowser scripts with this API?
Common BitBrowser paths and fields are compatible. Configure the WGBrowser address, API key, and profile identifiers, then check the endpoints your script calls. Headless mode is unsupported, and the driver field returns null. Scripts that depend on those behaviors need changes even when their profile management requests use compatible paths.
Should I create a new Playwright browser context?
Use the context already present after connecting over CDP. A profile is already an independent browser with its own browsing data, and creating another isolated context inside it is unsupported. You can add pages to the existing context. Open a different browser profile when you need separate cookies and storage for another account.
Why does Local API return HTTP 200 without opening a profile?
Business errors can use HTTP 200, so a successful HTTP response alone does not confirm that the profile opened. Inspect success, msg, and code in the JSON body. Possible causes include a profile occupied on another device, account permissions, and plan limits. Record the error code before deciding whether the request should be retried.