All articles
TutorialFebruary 25, 2025 · 5 min read

Log In Once: Reusing Browser Profiles Across Sessions

Save a browser's cookies and storage to a profile once, then start every later session already signed in, without handling anyone's password.

Most automations that work behind a login start the same way: open the sign-in page, type the credentials, wait for the redirect, and only then do the real work. Repeating that on every run is slow, adds a step that can fail, and is a good way to trip rate limits, security alerts, or a fresh MFA prompt. If the account uses MFA, someone also has to be there to finish each login.

The browser on your laptop doesn't have this problem, because it remembers that you signed in. Profiles give Hyperbrowser sessions the same memory: sign in once, save the browser's state, and start every later session already signed in.

What a profile saves

A profile is a saved copy of a browser's user data: its cookies, local storage, and session storage. Every Hyperbrowser session starts with a fresh, empty browser unless you attach a profile. With one attached, the browser starts with everything the profile holds.

Working with a profile has three parts:

  • Create it. A new profile is empty.
  • Save to it. Attach the profile to a session with persistChanges: true, do whatever creates the state you want (usually signing in), and stop the session. The profile finishes saving a few seconds after the session stops.
  • Reuse it. Attach the profile to later sessions. They start with the saved state and, by default, leave the profile as it is.

See it work

This script needs nothing but an API key. It saves a cookie and a local storage value in one session, then opens a brand-new session with the same profile and reads them back. Install @hyperbrowser/sdk and playwright-core, set HYPERBROWSER_API_KEY, and save it as profiles.mjs:

import { Hyperbrowser } from "@hyperbrowser/sdk";
import { chromium } from "playwright-core";

const client = new Hyperbrowser(); // reads HYPERBROWSER_API_KEY
const profile = await client.profiles.create({ name: "profile-demo" });

// First session: save what the browser stores.
const first = await client.sessions.create({
  profile: { id: profile.id, persistChanges: true },
});
try {
  const browser = await chromium.connectOverCDP(first.wsEndpoint);
  const page = browser.contexts()[0].pages()[0];
  await page.goto("https://example.com");
  await page.evaluate(() => {
    document.cookie = "signed_in=yes; path=/";
    localStorage.setItem("last_visit", "2026-10-13");
  });
} finally {
  await client.sessions.stop(first.id);
}

// The profile finishes saving a few seconds after the session stops.
await new Promise((resolve) => setTimeout(resolve, 5000));

// Second session: a new browser that starts with the saved state.
const second = await client.sessions.create({ profile: { id: profile.id } });
try {
  const browser = await chromium.connectOverCDP(second.wsEndpoint);
  const page = browser.contexts()[0].pages()[0];
  await page.goto("https://example.com");
  const saved = await page.evaluate(() => ({
    cookie: document.cookie,
    lastVisit: localStorage.getItem("last_visit"),
  }));
  console.log(saved);
} finally {
  await client.sessions.stop(second.id);
}

Run it with node profiles.mjs. The second browser has never been to example.com before, yet it prints:

{ cookie: 'signed_in=yes', lastVisit: '2026-10-13' }

A real login works the same way. The site's session cookie is just another cookie in the profile, so the next session sends it and the site treats the browser as signed in.

Sign in once, without the password

For a real account, the cleanest way to fill a profile is to let the account owner sign in themselves. They type their own password and answer any MFA prompt inside the cloud browser through Live View, so their credentials never pass through your code. Your script opens the login page, shares the Live View link, and waits until the sign-in lands on the next page:

const profile = await client.profiles.create({ name: "acme-dashboard" });
const session = await client.sessions.create({
  profile: { id: profile.id, persistChanges: true },
  timeoutMinutes: 15, // time for a person to sign in
});

try {
  const browser = await chromium.connectOverCDP(session.wsEndpoint);
  const page = browser.contexts()[0].pages()[0];
  await page.goto("https://acme.test/login");

  // Send this link to the account owner. They sign in, MFA included.
  console.log("Sign in here:", session.liveUrl);

  // Wait for the redirect after a successful sign-in.
  await page.waitForURL("**/dashboard", { timeout: 15 * 60_000 });
} finally {
  await client.sessions.stop(session.id);
}
console.log("Saved profile:", profile.id);

Swap in your site's login page and the URL it redirects to. If you'd rather sign in from code, fill the form with Playwright in the same session; the rest stays the same.

Reuse it on every run

Keep the profile ID with the rest of your configuration, and attach it whenever an automation needs the account:

const session = await client.sessions.create({
  profile: { id: process.env.ACME_PROFILE_ID },
});
try {
  const browser = await chromium.connectOverCDP(session.wsEndpoint);
  const page = browser.contexts()[0].pages()[0];
  await page.goto("https://acme.test/dashboard"); // already signed in
  // ...the real work
} finally {
  await client.sessions.stop(session.id);
}

The Python SDK takes the same options: client.sessions.create(params={"profile": {"id": profile_id, "persist_changes": True}}).

Things to know

  • Reuse is read-only by default. A session only writes to the profile when you pass persistChanges: true. That's what makes it safe to run many sessions on one profile at the same time; just save from one session at a time.
  • Use the browser's default context. With Playwright or Puppeteer, work in the context the browser already has (browser.contexts()[0]). A context you create yourself starts empty and doesn't see the profile.
  • Logins still expire. Sites end sessions on their own schedule. When a run finds itself signed out, repeat the sign-in session with persistChanges: true to refresh the profile.
  • Keep one profile per account. To branch from an existing profile, for example to add a second login, client.profiles.fork(id, { name }) makes an independent copy and leaves the original untouched.
  • Treat the Live View link as a secret. Anyone with it can see and control the browser while the session runs. Send it only to the person signing in, and create the session with viewOnlyLiveView: true when someone only needs to watch.
  • Treat profiles like credentials. A signed-in profile holds live session cookies, so anyone with your API key can use the account through it. Delete profiles you no longer need with client.profiles.delete(id).

Get started

Profiles work with the Node.js SDK, the Python SDK, and the REST API. Start with the script above, then swap example.com for the site you automate.

Read the profiles guide →

TutorialProfilesAuthentication