For AI agents: a documentation index is available at /llms.txt
Skip to main content

Authenticated Profiles

Authenticated profiles let you log in to a website once and reuse that signed-in state across many parallel browser sessions. Once the profile is captured, any session that passes parameter ?profile=<profile-name> automatically restores the cookies, localStorage, and IndexedDB entries without requiring you to log in again.

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.
Prerequisites
  • A Browserless API token (available in your account dashboard)
  • A CDP client like Puppeteer or Playwright installed
Plan requirements

Creating and reusing profiles over the API/CDP has no plan requirement. Creating a profile through the manual, remote-browser login flow below opens a live session link, which requires live URL access. This is available on the Prototyping plan or above (or Enterprise/Dedicated).

How it works

A profile captures three pieces of the browser state from a live session and stores them under a name scoped to your API token:

  • Cookies — including HttpOnly, Secure, and SameSite attributes
  • localStorage — for every origin the session touched
  • IndexedDB — databases, object stores, and entries
Why is sessionStorage not stored?

sessionStorage is excluded because its values only exist for the duration of a single browser tab. Restoring them in a new session would inject stale data that breaks OAuth redirects, CSRF tokens, and other short-lived flows.

Sessions are isolated

Each time you pass ?profile=<profile-name>, Browserless restores the saved profile state before your code runs. Any changes during the session stays local to that session and won't affect the original profile.

Creating a profile

  1. Open the profile creator

    Open Profiles in the Browserless dashboard and click New Profile under Authenticated Profiles.

  2. Choose the remote browser flow

    Select Sign in via remote browser. Enter a unique profile Name and, optionally, the URL where you want the browser to open. Under Advanced, you can also configure a proxy for the login session.

    Click Continue to start the profile session.

  3. Complete the login

    Click Open auth flow in new tab, then sign in through the remote browser. Complete any MFA, CAPTCHA, or magic-link steps before saving.

  4. Save the profile

    Click Save profile in the remote browser toolbar. You can also return to the setup dialog and click Done. The saved profile appears under Authenticated Profiles and is ready to use with ?profile=<profile-name>.

Using a profile

Once a profile exists, pass ?profile=<profile-name> on any browser-launching request. The examples below show how to connect to Browserless with a saved profile across different clients and request types:

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
tip

Profile names are scoped to your token. Different tokens can't see or use each other's profiles.

Managing profiles

Profiles are managed through the following REST endpoints. All endpoints that return data use the same profile metadata object:

{
"id": "string",
"name": "string",
"cookieCount": "number",
"originCount": "number",
"lastUsedAt": "string | null",
"createdAt": "string",
"updatedAt": "string"
}

List profiles

curl "https://production-sfo.browserless.io/profiles?token=YOUR_API_TOKEN_HERE"

Returns an array of profile metadata objects. 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"

Returns a single profile metadata object.

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" }'

Returns the updated profile metadata object with the new name.

Delete

curl -X DELETE "https://production-sfo.browserless.io/profile/acme-prod?token=YOUR_API_TOKEN_HERE"

Returns a 204 No Content on success. Deletion is permanent and both the profile and its captured state are removed.

Manage profile state via JSON

If you already have authentication state from another system, you can create or refresh a profile directly from JSON. These endpoints do not launch a browser; Browserless stores the supplied cookies, localStorage, and IndexedDB state directly.

The state payload must use the Browserless profile format:

  • cookies is an array of browser cookies. Each cookie needs name, value, and domain; Browserless fills defaults for optional fields such as path, expires, httpOnly, secure, and session.
  • origins is an array of origin-scoped storage objects. Each origin must be an http or https origin, and localStorage must be an object whose keys and values are strings.
  • indexedDBs is optional. When present, each database needs name, version, and objectStores; each object store needs name, keyPath, autoIncrement, entries, and indexes.
Adapting Playwright storage state

Playwright's storageState() returns origins[].localStorage as an array of { name, value } pairs. Convert it to an object before uploading:

const toBrowserlessState = (storageState) => ({
cookies: storageState.cookies,
origins: storageState.origins.map(({ origin, localStorage }) => ({
origin,
localStorage: Object.fromEntries(
(localStorage ?? []).map(({ name, value }) => [name, value]),
),
})),
});

If you include IndexedDB data from your own capture pipeline, send it as indexedDBs in the Browserless format shown above.

Upload a JSON profile

Use POST /profile/upload to create a new profile from a pre-captured state payload. The name must be unique for your API token. Uploading the same name twice returns an error; use POST /profile/refresh when you want to replace an existing profile's state.

curl -X POST "https://production-sfo.browserless.io/profile/upload?token=YOUR_API_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{
"name": "acme-prod",
"state": {
"cookies": [
{
"name": "sid",
"value": "abc123",
"domain": ".example.com",
"path": "/",
"expires": -1,
"httpOnly": true,
"secure": true,
"session": true,
"sameSite": "Lax"
}
],
"origins": [
{
"origin": "https://app.example.com",
"localStorage": {
"authToken": "..."
},
"indexedDBs": [
{
"name": "auth-db",
"version": 1,
"objectStores": [
{
"name": "tokens",
"keyPath": null,
"autoIncrement": false,
"entries": [
{ "key": "refresh", "value": { "token": "..." } }
],
"indexes": []
}
]
}
]
}
]
}
}'

A successful upload returns the stored profile metadata plus a diagnostics object that reports anything Browserless dropped or truncated before saving:

{
"id": "profile_abc123",
"name": "acme-prod",
"cookieCount": 1,
"originCount": 1,
"lastUsedAt": null,
"createdAt": "2026-05-22T12:00:00.000Z",
"updatedAt": "2026-05-22T12:00:00.000Z",
"diagnostics": {
"skippedMalformedCookies": 0,
"skippedPrivateCookies": 0,
"skippedMalformedOrigins": 0,
"skippedPrivateOrigins": 0,
"truncatedOrigins": 0,
"skippedMalformedIdbDatabases": 0,
"truncatedIdbDatabases": 0,
"skippedMalformedIdbStores": 0,
"truncatedIdbEntries": 0
}
}

Refresh a JSON profile

Use POST /profile/refresh when an existing profile's cookies or storage have expired and you have a replacement state payload. The body is the same shape as /profile/upload, but the profile must already exist for the requesting token.

curl -X POST "https://production-sfo.browserless.io/profile/refresh?token=YOUR_API_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{
"name": "acme-prod",
"state": {
"cookies": [
{
"name": "sid",
"value": "new-value",
"domain": ".example.com",
"path": "/",
"expires": -1,
"httpOnly": true,
"secure": true,
"session": true
}
],
"origins": [
{
"origin": "https://app.example.com",
"localStorage": {
"authToken": "new-token"
}
}
]
}
}'

Refresh overwrites the stored state in place and returns the same response shape as upload, including diagnostics. The profile keeps its id and name, while cookieCount, originCount, and updatedAt reflect the new state.

If the profile does not exist for the token, Browserless returns 404. Existing browser sessions that already loaded the old profile are not changed; new sessions that pass ?profile=acme-prod use the refreshed state.

Import from a local browser via the CLI

The @browserless.io/cli tool captures cookies, localStorage, and IndexedDB directly from a local Chromium-based browser and uploads them as a cloud profile. No scripting or JSON assembly required.

  1. Install the CLI

    npm install -g @browserless.io/cli

    Requires Node.js ≥ 24.

  2. Authenticate

    browserless auth login <your-token>

    The token is stored in your OS keychain when available and falls back to ~/.browserless/config.json.

  3. Upload a profile

    browserless profile upload \
    --browser chrome --profile Default --name my-chrome

    The CLI reads the local browser's user-data directory, extracts the authentication state, and uploads it. Any Browserless session can now start already authenticated by passing ?profile=my-chrome.

The CLI supports Chrome, Edge, Brave, Chromium, and other Chromium-based browsers. Use browserless profile sources list to discover available local profiles.

See the CLI documentation for the full command reference, including domain filtering, artifact-size management, custom browser registration, and configuration.

Endpoint coverage

?profile=<profile-name> works on any request that launches a new Chromium-family browser. A few endpoints behave differently or don't support it at all which are listed below:

TypeEndpointStatusNotes
WebSocket/, /chromium, /chrome, /edge, /chromium/stealth, /chrome/stealth, /stealth✅ SupportedAuth state applied automatically
BrowserQL/chromium/bql, /chrome/bql, /stealth/bql (POST and WS), /chromium/agent✅ SupportedAuth state applied automatically
REST/screenshot, /pdf, /unblock, /chromium/export, /content, /scrape, /function, /download, /performance, /smart-scrape, /crawl (and /chrome/*, /chromium/*, /edge/* variants)✅ SupportedAuth state applied automatically
SessionPOST /session with profile in the body✅ SupportedSee the Persisted Session tab
Playwright/chromium/playwright, /chrome/playwright, /edge/playwright⚠️ CaveatAuth state is applied, but clients must re-use browser.contexts()[0]. New contexts start without cookies and storage.
Playwright/firefox/playwright, /webkit/playwright❌ Not supportedOnly Chromium-family browsers are supported
Reconnect/reconnect/*❌ Not supported?profile= is rejected with a 400. The browser was already authenticated at first launch; changing its identity mid-session would invalidate the live state.

For unsupported flows, capture and apply the auth state inside a BrowserQL or Puppeteer session instead.

FAQ & Troubleshooting

What are the limits for profiles?

These are the per-profile and per-session constraints. Hitting any of these will prevent a profile from being saved or cause a request to be rejected.

LimitValue
Captured state size2 MB
Distinct origins per profile50
IndexedDB databases per origin5
IndexedDB entries per object store1,000
Profile name length255 characters
Profile name uniquenessPer token
Creation session lifetime10 minutes
Unused-profile retention30 days from last use

Profiles unused for 30 days are removed automatically.

When should I use a profile vs a persisted session?

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.

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 /session and profile: "<name>" in the body. The profile is loaded once when the browser starts; the session then persists normally.

Use neither for one-off automation where re-authenticating is cheap and you don't want to manage stored state.

What does the error "No cookies found in this profile" mean when I create a profile?

This message appears when you save a profile but the browser session had no cookies at the time of capture. It typically means one of three things:

  • You saved too early. The login flow hadn't completed yet, so the site hadn't set any authentication cookies. Make sure the page has fully loaded and the login redirect has finished before calling Browserless.saveProfile.
  • The site doesn't use cookies for auth. Some applications store authentication state exclusively in localStorage or IndexedDB (e.g. JWT tokens). The profile still captures that storage, so the session may work even with zero cookies. Test the profile by connecting with ?profile=<name> and navigating to a protected page.
  • The login happened on a different origin. If your login form posts to a third-party identity provider and the redirect back hasn't completed, cookies may be scoped to the IdP origin rather than your app. Wait for the final redirect before saving.

If the profile has zero cookies but does contain localStorage or IndexedDB data, it can still restore a valid authenticated session. The warning is informational and doesn't mean the profile is broken.

What does enabling proxy sticky session do?

When you enable proxy sticky session (the proxySticky parameter), all network requests within a single browser session are routed through the same proxy exit node. Your session keeps the same IP address for its entire lifetime.

Without sticky sessions, each request may be routed through a different node in the proxy pool. This causes problems:

  • Session invalidation. Many sites tie session cookies to the originating IP. If the IP changes mid-session, the server invalidates the cookie and forces a re-login.
  • Bot detection. Rapid IP changes within a single browsing session are a strong signal for anti-bot systems.
  • Geo-gated content. If requests alternate between regions, the site may serve inconsistent content or block the session entirely.

Enable sticky sessions when creating a profile that requires a proxy, especially if the target site validates session continuity by IP. This ensures the login flow and all subsequent page loads during profile capture use the same exit IP.

See Proxies for the full proxy configuration reference.

Session times out before login completes

When using a live session link for manual login, there are two independent timeouts — both default to ~10 minutes, which may not be enough for SSO, 2FA, or magic-link flows.

TimeoutWhere to set itWhat it controls
Session timeout?timeout= query param on POST /profileHow long the creation browser session stays alive
LiveURL timeout{ timeout: ms } param in Browserless.liveURL CDP callHow long the live session link stays open

Set both explicitly when you need more time:

// Session timeout — set on the POST URL
const session = await fetch(`${ORIGIN}/profile?token=${TOKEN}&timeout=600000`, { ... });

// LiveURL timeout — set when creating the live link
const { liveURL } = await cdp.send('Browserless.liveURL', { timeout: 600000 });
Captured state is too large

The captured state (cookies, localStorage, and IndexedDB) is capped at 2 MB per profile. If your site stores large amounts of data in localStorage or IndexedDB, the save will fail with { ok: false, error: ... } and nothing is persisted.

To fix this, try:

  • Clearing unused localStorage keys before saving the profile
  • Reducing the number of origins the session touches
  • Checking the Limits FAQ entry for the full set of per-profile constraints

Further reading

Next steps