Driving JustBrowser Profiles from Playwright and Puppeteer via the REST API
Automation frameworks are good at clicking, typing and reading pages. They are less good at remembering who they were yesterday. A browser launched fresh from a script has no cookies, no stored sessions and no fixed proxy, so every run starts from nothing.
A profile in a multi-account browser fixes that. Each profile is its own browser with its own cookies, storage, proxy and identity parameters. If you can drive that profile from Playwright, Puppeteer or Selenium, you get the best of both: a scripted workflow running inside a browser that stays consistent from one session to the next.
This guide covers how to do that with the local REST API and CDP that JustBrowser exposes.
What and Why
Playwright, Puppeteer and Selenium can all attach to an already-running Chromium browser over the Chrome DevTools Protocol (CDP). That makes the split of responsibilities straightforward:
- The multi-account browser owns the profile: its cookies, local storage, proxy, timezone, screen and the rest of its identity parameters.
- Your script owns the workflow: navigate here, fill that form, export this report.
This suits teams automating their own work. Think of an agency exporting reports from client dashboards it has access to (the guide to managing many client ad accounts covers the platform side of that), a storefront operator syncing listings across stores it runs, or a QA engineer checking a flow in several locales. In each case the account belongs to you or your client, and the profile keeps those accounts separate and shareable with teammates.
It also helps to be plain about what this is not. Driving a profile does not exempt a script from a site's terms of service or robots rules. If a site restricts automated access, follow its rules. A profile is a tidy place to run an allowed workflow, not a way around a restriction.
Core Concepts
One profile, one browser
JustBrowser is a custom Chromium engine with native C++ integration, not a browser extension, and it is kept current with upstream Chromium. Every profile carries 40+ identity parameters, including canvas, WebGL, audio, fonts, screen, timezone, navigator and hardware settings. The profile applies them consistently at the engine level, so the browser reports one coherent identity for the whole session.
Because every profile is its own browser process, running many at once means more orchestration and more RAM than opening many tabs in one browser. In exchange, each account's state stays in its own container and cannot bleed into another's.
The API surface
The REST API and CDP cover the whole lifecycle:
- Start a profile and receive a
cdp_url. - Attach Playwright, Puppeteer or Selenium to that URL.
- Stop the profile when the job is done.
Errors come back as {"error": {"code", "message"}} with a non-2xx status, which makes failures easy to handle in a loop.
The Complete Process
Step 1: Set up profiles
JustBrowser is one plan at $9.99/month (or $99.99/year) with unlimited profiles, and the local REST API and CDP access are included. The trial is 7 days, card required, and you are not charged if you cancel inside the week. After installing, the API is available at http://127.0.0.1:36542/api/v1. The port is configurable in the app's API/AI tab, which is also where you copy the Bearer token every request needs.
Create profiles in the app first. Each one gets a set of identity parameters, an optional proxy (HTTP, HTTPS or SOCKS5) and a unique profile ID. Keep each profile's timezone and proxy location consistent with each other. We cover how rendering parameters fit together in the WebGL fingerprinting guide.
Step 2: Start a profile via the REST API
// Start a profile with a visible window
const response = await fetch('http://127.0.0.1:36542/api/v1/profiles/PROFILE_ID/start', {
method: 'POST',
headers: { 'Authorization': `Bearer ${JB_API_TOKEN}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ headless: false })
});
const { data: { cdp_url: wsEndpoint } } = await response.json();
// data also carries profile_id, status ("running") and launched_at
Two details matter here. The API starts profiles headless by default, so send {"headless": false} whenever you want a visible window. And the wsEndpoint is a standard CDP address, so any framework that speaks CDP can connect to it.
Step 3: Connect Playwright
import { chromium } from 'playwright';
const browser = await chromium.connectOverCDP(wsEndpoint);
const context = browser.contexts()[0]; // the profile's own context
const page = context.pages()[0] ?? await context.newPage();
await page.goto('https://your-own-dashboard.example.com');
// Your workflow here
Use contexts()[0], never browser.newContext(). A new context is an incognito context outside the profile, so it would not have the profile's cookies or storage.
Step 4: Puppeteer and Selenium
Puppeteer is similar:
import puppeteer from 'puppeteer-core';
const browser = await puppeteer.connect({
browserWSEndpoint: wsEndpoint
});
const pages = await browser.pages();
const page = pages[0] || await browser.newPage();
Selenium attaches through the debugger address instead:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
options = Options()
# host:port parsed out of the cdp_url the start endpoint returned
options.debugger_address = "127.0.0.1:PORT_FROM_CDP_URL"
driver = webdriver.Chrome(options=options)
Pin ChromeDriver to the major version of the profile's Chromium core, not your machine's own Chrome, or Selenium Manager fetches a driver for the wrong version and the attach fails. For a running profile, GET /profiles/{id}/selenium returns the debugger address and capabilities ready to pass to Selenium.
Step 5: Stop the profile
browser.close() only disconnects your framework. The profile keeps running until you stop it:
await fetch(`http://127.0.0.1:36542/api/v1/profiles/${profileId}/stop`, {
method: 'POST',
headers: { Authorization: `Bearer ${JB_API_TOKEN}` }
});
Step 6: Manage a pool of profiles
When several workers share a set of profiles, hand them out through a small pool so two workers never grab the same one:
class ProfilePool {
constructor(profileIds, apiBase = 'http://127.0.0.1:36542/api/v1', token = process.env.JB_API_TOKEN) {
this.available = [...profileIds];
this.inUse = new Map();
this.apiBase = apiBase;
this.token = token; // from the app's API/AI tab
}
async acquire() {
const profileId = this.available.pop();
if (!profileId) throw new Error('No profiles available');
const res = await fetch(`${this.apiBase}/profiles/${profileId}/start`, {
method: 'POST',
headers: { Authorization: `Bearer ${this.token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ headless: false })
});
const { data: { cdp_url: wsEndpoint } } = await res.json();
this.inUse.set(profileId, wsEndpoint);
return { profileId, wsEndpoint };
}
async release(profileId) {
await fetch(`${this.apiBase}/profiles/${profileId}/stop`, {
method: 'POST',
headers: { Authorization: `Bearer ${this.token}` }
});
this.inUse.delete(profileId);
this.available.push(profileId);
}
}
Run your workflow against an acquired profile and release it when done. The profile keeps its cookies and storage across sessions, so a returning job finds the account state it left.
Advanced Tips
Headless is not headed
A headless session has no real window, so its screen and viewport follow headless defaults. If a workflow depends on a realistic window size, such as a layout check or a screenshot you will compare later, start the profile with {"headless": false}.
Plan your hosts around that. JustBrowser ships for Windows x64 and macOS on Apple Silicon, so a profile host is a Windows or Mac machine with a desktop session. There is no Linux build.
Keep proxy and timezone consistent
A profile set to a Chicago timezone and screen should route through a proxy in a matching region. JustBrowser routes each profile through its own proxy, either your own or one from a proxy plan bought in the app. If you are checking how an ad renders in a given country, pick a proxy in that country and set the profile's timezone to match.
Pace your scripts and respect the site
Automation that fills forms instantly and clicks the millisecond an element appears puts load on a site in a way a person never would. Add sensible delays, keep request rates modest and respect robots rules and terms. If a site rate-limits or challenges your script, slow down and check whether the workflow is one the site permits. If it blocks automated access outright, the fix is to ask for access or use an official API, not to push harder.
The same pacing logic applies outside the browser: VeloCalls handles pacing for outbound call workflows, and JustEmails covers email sending.
Common Mistakes
Mistake 1: Calling browser.newContext() (new_context() in Python).
That creates an incognito context outside the profile. Use contexts()[0] and its first page so you work inside the profile's own cookies and storage.
Mistake 2: Assuming browser.close() ends the session.
It only disconnects. The profile keeps running, and so does its memory use. End every session with a POST to /profiles/{id}/stop.
Mistake 3: Forgetting the JSON header on start.
To get a visible window you must send {"headless": false} as JSON with Content-Type: application/json. A start call with no body gives you a headless browser.
Mistake 4: Sharing one profile between workers. Two scripts driving the same profile will fight over its tabs and state. Use a pool, or give each worker its own profile.
Mistake 5: Running too many profiles on a small machine. Each profile is a separate browser process. Size your host for the number of profiles you run at the same time, and stop profiles you are no longer using.
Frequently Asked Questions
How do I connect Playwright or Puppeteer to a JustBrowser profile?
Start the profile with a POST to the local REST API, which returns a cdp_url WebSocket address. In Playwright call chromium.connectOverCDP(cdp_url) and use the first context. In Puppeteer call puppeteer.connect({ browserWSEndpoint: cdp_url }). Your script then drives the profile's own browser, with its own cookies, storage and proxy.
Why use a profile instead of launching a fresh browser from the framework?
A framework-launched browser starts empty every time. A profile keeps its cookies, local storage and proxy between sessions, so a workflow that signs in to an account you are allowed to use can pick up where it left off, and each account stays in its own separate browser.
Does JustBrowser start profiles headless or headed?
The API starts profiles headless by default. Send a JSON body of {"headless": false} with Content-Type: application/json when you want a visible window. A headless session has no real window, so its screen and viewport follow headless defaults.
How do I end an automated session?
browser.close() only disconnects your framework from the profile; the profile keeps running. End the session with a POST to /profiles/{id}/stop.
Resources and Next Steps
If a pipeline misbehaves, the cause is usually one of these:
- Wrong context. Confirm you attached to
contexts()[0], not a new one. - Headless by default. Confirm you sent
{"headless": false}where you need a window. - Proxy and timezone mismatch. Check that the profile's proxy region matches its timezone.
- Leftover sessions. Confirm every run ends with a stop call.
For ad verification workflows, teams often pair a multi-account browser with click fraud protection such as ClickzProtect, so the traffic they analyse is genuine. If you track conversions across the funnels you check, JustAnalytics provides privacy-first analytics.
The JustBrowser REST API documentation covers the full profile management and CDP surface. The 7-day trial includes the REST API and CDP in full, with a card required, so you can validate the whole pipeline before the $9.99/month starts.
Try JustBrowser
A multi-account browser for teams, built on a custom Chromium engine with native C++ integration, not an extension. 40+ identity parameters, a REST API and CDP for Playwright, Puppeteer and Selenium, and free team seats. $9.99/month or $99.99/year. 7-day free trial, card required — cancel any time in the seven days and you are not charged. Unlimited profiles.
Get started → · How it differs from Multilogin / GoLogin / AdsPower
Ready to manage multiple accounts?
Seven days free, then $9.99/month — one plan, everything included.