Use this pattern when a workflow needs to log in once, then hand that authenticated browser state to later BrowserCity sessions. Keep it limited to accounts and applications you are authorized to automate; do not use stored state to bypass a site’s access controls or terms.
The handoff has two explicit phases:
- Capture authenticated storage. Create a session, connect with Playwright, complete login on the target site, then terminate the session with
DELETE /v1/sessions/:id. The delete response can includesummary.storagewith Playwright-style cookies and origin localStorage entries. - Reuse that storage later. Create a new BrowserCity session with the captured
storagepayload so the ready context/page starts authenticated.
The examples below use Playwright for the login flow. If you drive the first phase with REST or Humanized REST actions instead, still capture storage by ending the BrowserCity session with DELETE /v1/sessions/:id and reading summary.storage from that response.
Phase 1: authenticate once and capture storage
Replace the placeholder selectors, credentials, and target URLs with your own application flow. The snippets save the captured storage to storage-state.json; cookies remain arrays, so count them with .length, len(...), or .Count when you inspect them.
import { writeFile } from 'node:fs/promises';import { chromium } from 'playwright';const apiKey = process.env.BROWSERCITY_API_KEY;if (!apiKey) { throw new Error('Set BROWSERCITY_API_KEY before running this script.');}const apiBase = 'https://api.browser.city/v1';type StorageState = { cookies?: unknown[]; origins?: unknown[];};type Session = { id: string; endpoint: string; token: string;};type DeleteSessionResponse = { summary?: { storage?: StorageState; };};const created = await fetch(`${apiBase}/sessions`, { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ browser: 'chromium', labels: ['template:auth-capture'] }),});if (!created.ok) { throw new Error(`Create session failed: ${created.status} ${await created.text()}`);}const { endpoint, token, id } = (await created.json()) as Session;let capturedStorage: StorageState | undefined;try { const browser = await chromium.connect(endpoint, { headers: { Authorization: `Bearer ${token}` }, }); const [context] = browser.contexts(); const [page] = context?.pages() ?? []; if (!page) { throw new Error('BrowserCity session did not return a ready page.'); } await page.goto('https://app.example.com/login'); await page.getByLabel('Email').fill(process.env.APP_LOGIN_EMAIL ?? '<email>'); await page.getByLabel('Password').fill(process.env.APP_LOGIN_PASSWORD ?? '<password>'); await page.getByRole('button', { name: 'Sign in' }).click(); await page.waitForURL('https://app.example.com/dashboard');} finally { const deleted = await fetch(`${apiBase}/sessions/${id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${apiKey}` }, }); if (!deleted.ok) { throw new Error(`Delete session failed: ${deleted.status} ${await deleted.text()}`); } const { summary } = (await deleted.json()) as DeleteSessionResponse; capturedStorage = summary?.storage; if (capturedStorage) { await writeFile('storage-state.json', JSON.stringify(capturedStorage, null, 2)); console.log(`Captured ${capturedStorage.cookies?.length ?? 0} cookies.`); }}if (!capturedStorage) { throw new Error('Session ended without summary.storage. Confirm the login completed before deletion.');}import jsonimport osfrom pathlib import Pathimport requestsfrom playwright.sync_api import sync_playwrightAPI_BASE = "https://api.browser.city/v1"API_KEY = os.environ["BROWSERCITY_API_KEY"]AUTH_HEADERS = {"Authorization": f"Bearer {API_KEY}"}created = requests.post( f"{API_BASE}/sessions", headers=AUTH_HEADERS, json={"browser": "chromium", "labels": ["template:auth-capture"]}, timeout=60,)created.raise_for_status()session = created.json()endpoint, token, session_id = session["endpoint"], session["token"], session["id"]playwright = Nonecaptured_storage = Nonetry: playwright = sync_playwright().start() browser = playwright.chromium.connect( endpoint, headers={"Authorization": f"Bearer {token}"}, ) page = browser.contexts[0].pages[0] page.goto("https://app.example.com/login") page.get_by_label("Email").fill(os.getenv("APP_LOGIN_EMAIL", "<email>")) page.get_by_label("Password").fill(os.getenv("APP_LOGIN_PASSWORD", "<password>")) page.get_by_role("button", name="Sign in").click() page.wait_for_url("https://app.example.com/dashboard")finally: deleted = requests.delete(f"{API_BASE}/sessions/{session_id}", headers=AUTH_HEADERS, timeout=60) deleted.raise_for_status() summary = (deleted.json() if deleted.content else {}).get("summary") or {} captured_storage = summary.get("storage") if captured_storage: Path("storage-state.json").write_text(json.dumps(captured_storage, indent=2), encoding="utf-8") print("Captured cookies:", len(captured_storage.get("cookies") or [])) if playwright: playwright.stop()if not captured_storage: raise RuntimeError("Session ended without summary.storage. Confirm the login completed before deletion.")using Microsoft.Playwright;using System.Net.Http.Headers;using System.Net.Http.Json;using System.Text.Json.Nodes;var apiKey = Environment.GetEnvironmentVariable("BROWSERCITY_API_KEY") ?? throw new InvalidOperationException("Set BROWSERCITY_API_KEY before running this script.");using var http = new HttpClient { BaseAddress = new Uri("https://api.browser.city/v1/") };http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);var created = await http.PostAsJsonAsync("sessions", new { browser = "chromium", labels = new[] { "template:auth-capture" }});created.EnsureSuccessStatusCode();var session = await created.Content.ReadFromJsonAsync<Session>() ?? throw new InvalidOperationException("Create session response did not include an id, endpoint, and token.");var (id, endpoint, token) = session;IPlaywright? playwright = null;JsonNode? capturedStorage = null;try { playwright = await Playwright.CreateAsync(); var browser = await playwright.Chromium.ConnectAsync(endpoint, new() { Headers = new() { ["Authorization"] = $"Bearer {token}" } }); var page = browser.Contexts[0].Pages[0]; await page.GotoAsync("https://app.example.com/login"); await page.GetByLabel("Email").FillAsync(Environment.GetEnvironmentVariable("APP_LOGIN_EMAIL") ?? "<email>"); await page.GetByLabel("Password").FillAsync(Environment.GetEnvironmentVariable("APP_LOGIN_PASSWORD") ?? "<password>"); await page.GetByRole(AriaRole.Button, new() { Name = "Sign in" }).ClickAsync(); await page.WaitForURLAsync("https://app.example.com/dashboard");} finally { var deleted = await http.DeleteAsync($"sessions/{id}"); deleted.EnsureSuccessStatusCode(); var body = await deleted.Content.ReadFromJsonAsync<JsonObject>(); capturedStorage = body?["summary"]?["storage"]; if (capturedStorage is not null) { await File.WriteAllTextAsync("storage-state.json", capturedStorage.ToJsonString(new() { WriteIndented = true })); Console.WriteLine($"Captured cookies: {capturedStorage["cookies"]?.AsArray().Count ?? 0}"); } playwright?.Dispose();}if (capturedStorage is null) { throw new InvalidOperationException("Session ended without summary.storage. Confirm the login completed before deletion.");}record Session(string Id, string Endpoint, string Token);import com.fasterxml.jackson.databind.JsonNode;import com.fasterxml.jackson.databind.ObjectMapper;import com.microsoft.playwright.BrowserType;import com.microsoft.playwright.Playwright;import java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;import java.nio.file.Path;import java.util.List;import java.util.Map;public class CaptureAuthenticatedStorage { static final URI API_BASE = URI.create("https://api.browser.city/v1/"); static final String API_KEY = System.getenv("BROWSERCITY_API_KEY"); static final HttpClient HTTP = HttpClient.newHttpClient(); static final ObjectMapper JSON = new ObjectMapper(); public static void main(String[] args) throws Exception { if (API_KEY == null || API_KEY.isBlank()) { throw new IllegalStateException("Set BROWSERCITY_API_KEY before running this script."); } var session = postSession(Map.of( "browser", "chromium", "labels", List.of("template:auth-capture"))); var id = session.id(); var endpoint = session.endpoint(); var token = session.token(); JsonNode capturedStorage = null; Playwright playwright = null; try { playwright = Playwright.create(); var browser = playwright.chromium().connect(endpoint, new BrowserType.ConnectOptions().setHeaders(Map.of("Authorization", "Bearer %s".formatted(token)))); var page = browser.contexts().get(0).pages().get(0); page.navigate("https://app.example.com/login"); page.getByLabel("Email").fill(System.getenv().getOrDefault("APP_LOGIN_EMAIL", "<email>")); page.getByLabel("Password").fill(System.getenv().getOrDefault("APP_LOGIN_PASSWORD", "<password>")); page.getByRole(com.microsoft.playwright.options.AriaRole.BUTTON, new com.microsoft.playwright.Page.GetByRoleOptions().setName("Sign in")).click(); page.waitForURL("https://app.example.com/dashboard"); } finally { var deleted = deleteSession(id); capturedStorage = deleted.path("summary").path("storage"); if (!capturedStorage.isMissingNode() && !capturedStorage.isNull()) { JSON.writerWithDefaultPrettyPrinter().writeValue(Path.of("storage-state.json").toFile(), capturedStorage); System.out.println("Captured cookies: " + capturedStorage.path("cookies").size()); } if (playwright != null) { playwright.close(); } } if (capturedStorage == null || capturedStorage.isMissingNode() || capturedStorage.isNull()) { throw new IllegalStateException("Session ended without summary.storage. Confirm the login completed before deletion."); } } static Session postSession(Map<String, ?> body) throws Exception { var response = HTTP.send(apiRequest("sessions") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(JSON.writeValueAsString(body))) .build(), HttpResponse.BodyHandlers.ofString()); if (response.statusCode() / 100 != 2) { throw new RuntimeException("Create session failed: %d %s".formatted(response.statusCode(), response.body())); } return JSON.readValue(response.body(), Session.class); } static JsonNode deleteSession(String id) throws Exception { var response = HTTP.send(apiRequest("sessions/%s".formatted(id)) .DELETE() .build(), HttpResponse.BodyHandlers.ofString()); if (response.statusCode() / 100 != 2) { throw new RuntimeException("Delete session failed: %d %s".formatted(response.statusCode(), response.body())); } return response.body().isBlank() ? JSON.createObjectNode() : JSON.readTree(response.body()); } static HttpRequest.Builder apiRequest(String path) { return HttpRequest.newBuilder(API_BASE.resolve(path)) .header("Authorization", "Bearer %s".formatted(API_KEY)); } record Session(String id, String endpoint, String token) {}}
Phase 2: start a later session with captured storage
Send the saved storage object back on session creation. BrowserCity applies it before you connect, so the first existing context/page is already the authenticated handoff target.
import { readFile } from 'node:fs/promises';import { chromium } from 'playwright';const apiKey = process.env.BROWSERCITY_API_KEY;if (!apiKey) { throw new Error('Set BROWSERCITY_API_KEY before running this script.');}const apiBase = 'https://api.browser.city/v1';const storage = JSON.parse(await readFile('storage-state.json', 'utf8')) as { cookies?: unknown[]; origins?: unknown[];};const created = await fetch(`${apiBase}/sessions`, { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ browser: 'chromium', labels: ['template:auth-reuse'], storage, }),});if (!created.ok) { throw new Error(`Create session failed: ${created.status} ${await created.text()}`);}const { endpoint, token, id } = (await created.json()) as { endpoint: string; token: string; id: string };try { const browser = await chromium.connect(endpoint, { headers: { Authorization: `Bearer ${token}` }, }); const [context] = browser.contexts(); const [page] = context?.pages() ?? []; if (!page) { throw new Error('BrowserCity session did not return a ready page.'); } await page.goto('https://app.example.com/dashboard'); await page.getByText('Account overview').waitFor();} finally { await fetch(`${apiBase}/sessions/${id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${apiKey}` }, });}import jsonimport osfrom pathlib import Pathimport requestsfrom playwright.sync_api import sync_playwrightAPI_BASE = "https://api.browser.city/v1"API_KEY = os.environ["BROWSERCITY_API_KEY"]AUTH_HEADERS = {"Authorization": f"Bearer {API_KEY}"}storage = json.loads(Path("storage-state.json").read_text(encoding="utf-8"))created = requests.post( f"{API_BASE}/sessions", headers=AUTH_HEADERS, json={"browser": "chromium", "labels": ["template:auth-reuse"], "storage": storage}, timeout=60,)created.raise_for_status()session = created.json()endpoint, token, session_id = session["endpoint"], session["token"], session["id"]playwright = Nonetry: playwright = sync_playwright().start() browser = playwright.chromium.connect( endpoint, headers={"Authorization": f"Bearer {token}"}, ) page = browser.contexts[0].pages[0] page.goto("https://app.example.com/dashboard") page.get_by_text("Account overview").wait_for()finally: try: requests.delete(f"{API_BASE}/sessions/{session_id}", headers=AUTH_HEADERS, timeout=60).raise_for_status() finally: if playwright: playwright.stop()using Microsoft.Playwright;using System.Net.Http.Headers;using System.Net.Http.Json;using System.Text.Json.Nodes;var apiKey = Environment.GetEnvironmentVariable("BROWSERCITY_API_KEY") ?? throw new InvalidOperationException("Set BROWSERCITY_API_KEY before running this script.");using var http = new HttpClient { BaseAddress = new Uri("https://api.browser.city/v1/") };http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);var storage = JsonNode.Parse(await File.ReadAllTextAsync("storage-state.json")) ?? throw new InvalidOperationException("storage-state.json did not contain valid JSON storage.");var created = await http.PostAsJsonAsync("sessions", new { browser = "chromium", labels = new[] { "template:auth-reuse" }, storage});created.EnsureSuccessStatusCode();var session = await created.Content.ReadFromJsonAsync<Session>() ?? throw new InvalidOperationException("Create session response did not include an id, endpoint, and token.");var (id, endpoint, token) = session;IPlaywright? playwright = null;try { playwright = await Playwright.CreateAsync(); var browser = await playwright.Chromium.ConnectAsync(endpoint, new() { Headers = new() { ["Authorization"] = $"Bearer {token}" } }); var page = browser.Contexts[0].Pages[0]; await page.GotoAsync("https://app.example.com/dashboard"); await page.GetByText("Account overview").WaitForAsync();} finally { await http.DeleteAsync($"sessions/{id}"); playwright?.Dispose();}record Session(string Id, string Endpoint, string Token);import com.fasterxml.jackson.databind.JsonNode;import com.fasterxml.jackson.databind.ObjectMapper;import com.microsoft.playwright.BrowserType;import com.microsoft.playwright.Playwright;import java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;import java.nio.file.Path;import java.util.List;import java.util.Map;public class ReuseAuthenticatedStorage { static final URI API_BASE = URI.create("https://api.browser.city/v1/"); static final String API_KEY = System.getenv("BROWSERCITY_API_KEY"); static final HttpClient HTTP = HttpClient.newHttpClient(); static final ObjectMapper JSON = new ObjectMapper(); public static void main(String[] args) throws Exception { if (API_KEY == null || API_KEY.isBlank()) { throw new IllegalStateException("Set BROWSERCITY_API_KEY before running this script."); } JsonNode storage = JSON.readTree(Path.of("storage-state.json").toFile()); var session = postSession(Map.of( "browser", "chromium", "labels", List.of("template:auth-reuse"), "storage", storage)); var id = session.id(); var endpoint = session.endpoint(); var token = session.token(); Playwright playwright = null; try { playwright = Playwright.create(); var browser = playwright.chromium().connect(endpoint, new BrowserType.ConnectOptions().setHeaders(Map.of("Authorization", "Bearer %s".formatted(token)))); var page = browser.contexts().get(0).pages().get(0); page.navigate("https://app.example.com/dashboard"); page.getByText("Account overview").waitFor(); } finally { deleteSession(id); if (playwright != null) { playwright.close(); } } } static Session postSession(Map<String, ?> body) throws Exception { var response = HTTP.send(apiRequest("sessions") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(JSON.writeValueAsString(body))) .build(), HttpResponse.BodyHandlers.ofString()); if (response.statusCode() / 100 != 2) { throw new RuntimeException("Create session failed: %d %s".formatted(response.statusCode(), response.body())); } return JSON.readValue(response.body(), Session.class); } static void deleteSession(String id) throws Exception { var response = HTTP.send(apiRequest("sessions/%s".formatted(id)) .DELETE() .build(), HttpResponse.BodyHandlers.discarding()); if (response.statusCode() / 100 != 2) { throw new RuntimeException("Delete session failed: %d".formatted(response.statusCode())); } } static HttpRequest.Builder apiRequest(String path) { return HttpRequest.newBuilder(API_BASE.resolve(path)) .header("Authorization", "Bearer %s".formatted(API_KEY)); } record Session(String id, String endpoint, String token) {}}
Operational guardrails
- Only capture and reuse storage that your user or organization is authorized to use.
- Treat
storage-state.jsonlike a password: encrypt it at rest, keep it out of source control, and rotate it when access changes. - Scope cookies to the target domain and keep lifetimes short when possible.
- Delete capture and reuse sessions promptly after each phase finishes.
- Prefer labels that identify the workflow, not the human user’s private data.
Cost and plan notes
Authenticated workflows often spend more time waiting on application pages than public extraction jobs. Estimate browser minutes for both the capture phase and each reuse run, then multiply by expected concurrent jobs in /pricing-calculator.