Authenticated Profiles
Authenticated profiles let you log in to a website once and reuse that signed-in state across many parallel browser sessions. After capturing a profile, every session that requests ?profile=<name> starts with the cookies, localStorage, and IndexedDB already in place — your code skips the login step entirely.
This is useful when:
- Your scraper or agent needs to act as a logged-in user, but you don't want to re-authenticate on every run.
- You want to share an authenticated state across a fleet of workers without distributing credentials.
- You're running multi-step flows (paginated dashboards, account-only endpoints) where logging in each time is the slowest part.
How it works
A profile is a snapshot of three pieces of browser state, captured from a live session and stored under a name unique to your token:
- Cookies — including HttpOnly, Secure, and SameSite attributes
localStorage— for every origin the session touchedIndexedDB— databases, object stores, and entries
sessionStorage is intentionally not captured — it's tab-scoped and reusing stale values would break OAuth redirects, CSRF tokens, and other short-lived flows.
When a new session passes ?profile=<name>, Browserless loads the captured state into the browser before your code runs. Changes that happen during the session don't write back to the source profile — every session gets its own working copy.
Creating a profile
Profile creation is a two-step flow: launch a temporary browser via REST, perform the login (manually or programmatically), then call the Browserless.saveProfile CDP method to persist the state.
Creation needs a CDP-capable client (Puppeteer, Playwright, or raw CDP) because Browserless.saveProfile is a CDP method, not a BrowserQL mutation. Once a profile exists, BrowserQL can use it freely via ?profile=<name>.
Launch a creation session
Call
POST /profilewith a name for the profile. You'll get back a WebSocket URL to connect to and astopURL you can use to terminate the session early.curl -X POST "https://production-sfo.browserless.io/profile?token=YOUR_API_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{ "name": "acme-prod" }'{
"id": "<temporary-session-id>",
"name": "acme-prod",
"connect": "wss://production-sfo.browserless.io/session/connect/<id>?token=...",
"stop": "https://production-sfo.browserless.io/session/<id>?token=..."
}The creation session expires after 10 minutes if you don't capture before then.
Connect and authenticate
Connect to the returned WebSocket URL with Puppeteer (or any CDP client) and perform the login. You can drive it programmatically:
import puppeteer from 'puppeteer-core';
const browser = await puppeteer.connect({ browserWSEndpoint: session.connect });
const page = await browser.newPage();
await page.goto('https://app.example.com/login');
await page.type('#email', 'me@example.com');
await page.type('#password', process.env.PASSWORD);
await page.click('button[type="submit"]');
await page.waitForNavigation();…or hand off to a human via a live URL and complete the login by hand if the site has CAPTCHAs, MFA, or a flow that's hard to script.
Save the captured state
Once the browser holds the authenticated state, send the
Browserless.saveProfileCDP command. Browserless captures the cookies,localStorage, andIndexedDBfrom the running browser and persists them under the profile name.const cdp = await page.target().createCDPSession();
const result = await cdp.send('Browserless.saveProfile', { name: 'acme-prod' });
console.log(result);
// { ok: true, profileId: '<id>', name: 'acme-prod', cookieCount: 12, originCount: 1 }
await browser.close();Final nameThe
nameyou pass toBrowserless.saveProfileis the authoritative one — it's what gets stored and what you'll later use as?profile=<name>. Thenamein step 1'sPOST /profilerequest is advisory and doesn't have to match; you can choose a different name when you save.If the captured state exceeds the size limit, the call returns
{ ok: false, error: ... }and nothing is persisted.
Using a profile
Once a profile exists, pass ?profile=<name> on any browser-launching request. Browserless loads the captured state before your code runs.
- Puppeteer
- Playwright
- Python
- BrowserQL
- REST APIs
- Persisted Session
import puppeteer from 'puppeteer-core';
const TOKEN = 'YOUR_API_TOKEN_HERE';
const browserWSEndpoint = `wss://production-sfo.browserless.io?token=${TOKEN}&profile=acme-prod`;
const browser = await puppeteer.connect({ browserWSEndpoint });
const page = await browser.newPage();
await page.goto('https://app.example.com/dashboard'); // already logged in
import { chromium } from 'playwright';
const TOKEN = 'YOUR_API_TOKEN_HERE';
const browserWSEndpoint = `wss://production-sfo.browserless.io/chromium/playwright?token=${TOKEN}&profile=acme-prod`;
const browser = await chromium.connect(browserWSEndpoint);
const page = await browser.newPage();
await page.goto('https://app.example.com/dashboard');
from playwright.sync_api import sync_playwright
TOKEN = "YOUR_API_TOKEN_HERE"
WS_ENDPOINT = (
f"wss://production-sfo.browserless.io/chromium/playwright?token={TOKEN}&profile=acme-prod"
)
with sync_playwright() as playwright:
browser = playwright.chromium.connect_over_cdp(WS_ENDPOINT)
context = browser.contexts[0]
page = context.pages[0]
page.goto("https://app.example.com/dashboard")
browser.close()
curl -X POST "https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE&profile=acme-prod" \
-H "Content-Type: application/json" \
-d '{
"query": "mutation { goto(url: \"https://app.example.com/dashboard\") { status } screenshot { base64 } }"
}'
?profile= works on the REST endpoints that launch a fresh browser per request — /screenshot, /pdf, /unblock, and /chromium/export (plus their /chrome/* variants). The captured state is loaded into the browser before the page is rendered, so you can take a screenshot or PDF of an authenticated page without scripting the login.
curl -X POST "https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE&profile=acme-prod" \
-H "Content-Type: application/json" \
-d '{ "url": "https://app.example.com/dashboard" }' \
--output dashboard.png
See Endpoint coverage for the full list of supported endpoints and the handful of exceptions.
A profile can also be attached to a persisted session. Pass profile in the POST /session body, and the captured state is loaded the first time the session's browser starts.
import puppeteer from 'puppeteer-core';
const TOKEN = 'YOUR_API_TOKEN_HERE';
const ORIGIN = 'https://production-sfo.browserless.io';
// 1. Create a persisted session pre-loaded with a saved profile.
const session = await fetch(`${ORIGIN}/session?token=${TOKEN}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ttl: 1_800_000, // session lifetime in ms (30 minutes here)
profile: 'acme-prod', // saved auth state, loaded on first browser launch
}),
}).then((r) => r.json());
// 2. Connect to the session — the browser is already authenticated.
const browser = await puppeteer.connect({ browserWSEndpoint: session.connect });
const page = await browser.newPage();
await page.goto('https://app.example.com/dashboard');
// 3. Disconnect when done — the browser stays alive for the remaining TTL.
await browser.disconnect();
// 4. Later, reconnect with the same `session.connect` URL — same browser,
// same cookies/storage you accumulated since the profile was first loaded.
The profile is loaded once when the session's browser first starts. After that, any state changes (new cookies set, localStorage writes) accumulate on the live browser and are not written back to the saved profile.
The profile name is scoped to your token — different tokens can't see or use each other's profiles.
Endpoint coverage
As a rule, ?profile= takes effect on any request that launches a fresh Chromium-family browser. The exceptions are listed below.
Supported — auth state is applied automatically:
- All WebSocket connect routes:
/,/chromium,/chrome,/edge,/chromium/stealth,/chrome/stealth,/stealth, plus the BrowserQL endpoints/chromium/bql,/chrome/bql,/stealth/bql(POST and WS) and the BrowserQL Agent/chromium/agent - REST endpoints that launch a browser per request:
/screenshot,/pdf,/unblock,/chromium/export,/content,/scrape,/function,/download,/performance(and their/chrome/*,/chromium/*,/edge/*variants where applicable) POST /sessionwithprofilein the body — see the Persisted Session tab
Caveats:
/chromium/playwright,/chrome/playwright,/edge/playwright: the auth state is applied, but Playwright clients must re-use the existing browser context (browser.contexts()[0]) instead of creating a new one. New contexts start without the loaded cookies and storage.
Not supported:
/firefox/playwright,/webkit/playwright: only Chromium-family browsers are supported./smart-scrape:?profile=is currently ignored on this endpoint./crawl:?profile=is currently ignored on this endpoint./reconnect/*:?profile=is rejected with a400here. The browser was already authenticated at first launch and changing its identity mid-session would invalidate the live state.
For the unsupported flows, capture and apply the auth state inside a BrowserQL or Puppeteer session instead.
Managing profiles
Profiles are managed through a small set of REST endpoints.
List profiles
curl "https://production-sfo.browserless.io/profiles?token=YOUR_API_TOKEN_HERE"
Returns an array of profile metadata objects (name, ID, cookie/origin counts, timestamps). Paginate via limit (default 100, capped at 1000) and offset (default 0).
Get one profile
curl "https://production-sfo.browserless.io/profile/acme-prod?token=YOUR_API_TOKEN_HERE"
Rename
curl -X PUT "https://production-sfo.browserless.io/profile/acme-prod?token=YOUR_API_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{ "name": "acme-staging" }'
Delete
curl -X DELETE "https://production-sfo.browserless.io/profile/acme-prod?token=YOUR_API_TOKEN_HERE"
Deletion is permanent — both the profile and its captured state are removed.
Limits
| Limit | Value |
|---|---|
| Captured state size | 2 MB |
| Distinct origins per profile | 50 |
| IndexedDB databases per origin | 5 |
| IndexedDB entries per object store | 1,000 |
| Profile name length | 255 characters |
| Profile name uniqueness | Per token |
| Creation session lifetime | 10 minutes |
| Unused-profile retention | 30 days from last use |
Profiles unused for 30 days are removed automatically.
Profiles and Persisted Sessions
Authenticated Profiles and Persisted Sessions solve different problems and can be combined:
- A profile is the what: the saved authentication state to load before your code runs.
- A persisted session is the how-long: a long-lived browser process you can disconnect from and reconnect to.
When to use each
- Use a profile when you want to reuse an authenticated state across many short-lived sessions, including in parallel. Each session gets its own working copy — workers don't fight over a single shared browser, and changes during one session don't leak into the others.
- Use a persisted session when you need the same browser instance to survive across reconnects — for example, to keep tabs open or preserve in-flight UI state across a disconnect. Profiles snapshot auth and replay it into a fresh browser; persisted sessions keep the whole browser alive.
- Use both together when you want a long-lived browser that starts with a saved auth state. Create the session with
POST /sessionandprofile: "<name>"in the body. The profile is loaded once when the browser starts; the session then persists normally and any subsequent state changes accumulate on the live browser. - Use neither for one-off automation where re-authenticating is cheap and you don't want to manage stored state.
Further reading
- Persisting State — keep a single browser instance alive across reconnects
- Connection URL Patterns — full reference for connect-time query parameters
- BrowserQL — drive the creation session through the GraphQL layer