Browser contexts
Save logins, cookies and site storage between browser sessions.
A browser context is a saved browser profile. Sign in once, and every later
/wss session that uses the same contextId starts with
that login, its cookies, and its site storage in place.
Quick start
- Create a profile with
POST /contextsand keep the returnedid. - Connect to
/wss?contextId=…, sign in using the browser's default context, and close the browser. Closing is what saves the profile. - Reconnect with the same
contextId. Addpersist=falseto open as many read-only sessions as you need at once.
import { chromium } from "playwright-core";
const API = "https://request.usestring.ai/v1";
const auth = { Authorization: `Bearer ${process.env.STRING_API_KEY}` };
const connect = (contextId, persist = true) =>
chromium.connectOverCDP(`${API.replace("https", "wss")}/wss?contextId=${contextId}&persist=${persist}`, {
headers: auth,
timeout: 90_000
});
// 1. Create a profile.
const created = await fetch(`${API}/contexts`, {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({ name: "checkout-login" })
});
const { id: contextId } = await created.json();
// 2. Sign in once. Closing the browser saves the profile.
const browser = await connect(contextId);
const [context] = browser.contexts();
const page = context.pages()[0] ?? (await context.newPage());
await page.goto("https://example.com/login");
// ...fill in the login form and submit...
await browser.close();
// 3. Reconnect. The saved login is restored; read-only sessions (persist=false) can run in parallel.
const restored = await connect(contextId);
const [restoredContext] = restored.contexts();
await (restoredContext.pages()[0] ?? (await restoredContext.newPage())).goto("https://example.com/account");
await restored.close();What to know
- One saving session at a time. A second saving connection to the same profile is refused while
the first is running. Read-only sessions (
persist=false) are unlimited and run alongside it, but a profile that has never been saved cannot be opened read-only. - Only the default browser context is saved.
browser.newContext()creates an incognito context whose data is discarded. Usebrowser.contexts()[0]. - Send the same
countryCodeandproxyevery time. Profiles do not store them, and a login restored from a different country may be challenged or signed out by the website. - Allow 90 seconds to connect. Set your client's connection timeout to at least that.
- Profiles are shared across your organization and expire after 30 days without use.
Create a profile
POST https://request.usestring.ai/v1/contexts
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Idempotency-Key: checkout-login-2026-09-07
{ "name": "checkout-login" }name is optional: 1–200 characters after trimming, unique among your live profiles. Unknown
fields return 400; a duplicate name returns 409. Success returns 201 with the
profile object:
{
"id": "d7a89cd7-c9f7-47fd-84df-77320f254cf8",
"name": "checkout-login",
"status": "EMPTY",
"sizeBytes": null,
"failureReason": null,
"latestSaveOperation": null,
"lastSavedAt": null,
"lastUsedAt": null,
"createdAt": "2026-09-07T09:14:22.031Z",
"updatedAt": "2026-09-07T09:14:22.031Z"
}Send an Idempotency-Key to retry a create without making a second profile. Keys are scoped to
your organization for 24 hours and accept 1–255 characters from A-Z a-z 0-9 . _ : -.
| Situation | Response |
|---|---|
| First use | 201 with the new profile. |
| Retry after completion | 201 with the same profile. |
| Retry while creation is in progress | 409; retry with backoff. |
| Retry after that profile was deleted | A new profile is created. |
| Malformed or repeated header | 400. |
Connect
Connect to /wss?contextId=… with your API key; persist defaults to true. Parameters are listed
under GET /wss.
Saving and read-only sessions
A session with contextId saves its changes when it closes. Only one saving session can use a
profile at a time; a second saving connection is refused while the first is still running. Any
connection that arrives while the previous session's save is still finishing waits for it, at most
45 seconds, and then starts with the result.
Add persist=false to restore the saved profile without saving anything back. Read-only sessions
are not the profile's saving session: run as many as you need at once, alongside a saving session.
A profile that has never been saved cannot be used with persist=false. Read-only sessions count as
use for retention.
Country and proxy settings
countryCode and proxy are session parameters, documented under
GET /wss. Profiles do not remember them,
so send the same values on every connection. A login restored from a different country may be
challenged or signed out, and the same settings do not guarantee the same exit IP.
Refused connections
A save that outlives the wait described above answers Context is still being saved; a profile
that another saving session is using answers Context is attached to a running session. Both are a
WebSocket close with code 1008 and that reason. Playwright and Puppeteer report the close only as
a failed connection, so read the profile's status to tell a busy refusal (IN_USE or UPLOADING)
from any other error, then retry with bounded backoff:
for (let attempt = 0; attempt < 10; attempt++) {
try {
return await connect(contextId);
} catch (error) {
const { status } = await (await fetch(`${API}/contexts/${contextId}`, { headers: auth })).json();
if (attempt === 9 || (status !== "IN_USE" && status !== "UPLOADING")) throw error;
await new Promise((resolve) => setTimeout(resolve, 5_000));
}
}Read-only sessions are never refused for either reason.
Confirm the save
Optional. Use it when you need to know that a session's changes were saved, for example before telling a user that their login is stored.
Read GET /contexts/{contextId} before connecting and keep its lastSavedAt (null for a profile
that has never been saved). After closing the browser, poll the same endpoint every few seconds
until status is no longer IN_USE or UPLOADING:
| Result | Meaning |
|---|---|
READY and lastSavedAt is later than the value you kept | This session's changes were saved. |
FAILED | The save failed. Read failureReason. Any previously saved profile is retained. |
READY or EMPTY and lastSavedAt is unchanged | Nothing was saved, for example because the connection was refused or closed before the session started. Check latestSaveOperation. |
Still IN_USE or UPLOADING after your timeout | The outcome is unknown, not failed. Inspect the profile before retrying. |
If more than one client may save to the same profile, lastSavedAt cannot tell you whose save
finished; track the specific save instead.
Track a specific save
latestSaveOperation describes the most recent save. While your session is connected, read
GET /contexts/{contextId} and record latestSaveOperation.id. After closing the browser, poll and
check that the id still matches before reading its status; if another session has saved since, the
status endpoint cannot confirm your earlier save. With persist=false, no save is started.
| Save status | Meaning |
|---|---|
PENDING | The save has not finished. Keep polling within your timeout. |
COMMITTED | The save succeeded. |
FAILED | The save failed. Read its failureReason. Any previously saved profile is retained. |
EXPIRED | The session ended before anything could be saved. Retry. |
SUPERSEDED | Another session saved before this one finished. Do not treat it as success. |
When a save fails
latestSaveOperation.failureReason describes the save you are tracking; the profile's top-level
failureReason keeps the most recent failure until a save succeeds. Reconnecting restores the
previous successful save, and the failed session's changes are lost. If the profile's only save
failed, delete it and create another.
Status
| Profile status | Meaning |
|---|---|
EMPTY | Created with no saved data. Connect with persistence enabled to populate it. |
IN_USE | A saving session is using the profile. Another saving session or a deletion is refused. |
UPLOADING | A save is finishing. |
READY | Saved data is available. |
FAILED | A save failed. Previously saved data, if any, remains available. |
DELETING | Deletion was accepted; stored data is still being removed. Returned only by DELETE. |
Read-only sessions do not change the status, and READY does not reserve the profile. Contact us
if a profile stays busy long after its session ended.
The profile object
Create, inspect, and list return:
| Field | Type | Meaning |
|---|---|---|
id | string | Opaque UUID used to reconnect, inspect, or delete. |
name | string or null | Optional label. |
status | string | Current profile status. |
sizeBytes | integer or null | Compressed size of the saved profile. Null before the first successful save. |
failureReason | string or null | Most recent save failure, cleared by a successful save. |
latestSaveOperation | object or null | Latest save: { id, status, failureReason, completedAt }. |
lastSavedAt | timestamp or null | Time of the last successful save. |
lastUsedAt | timestamp or null | Last connection, saving or read-only. Reading status does not update it. |
createdAt | timestamp | Creation time. |
updatedAt | timestamp | Last profile update. |
List profiles
GET https://request.usestring.ai/v1/contexts?limit=50&offset=0 returns
{ "contexts": [...] }, newest first, without deleted profiles.
limit defaults to 50 and is capped at 200; invalid values use the default. offset defaults to
0. Use a page size from 1–200 and stop when a page comes back short.
Inspect a profile
GET https://request.usestring.ai/v1/contexts/{contextId} returns the profile object. A malformed
ID returns 400; an unknown, foreign-owned, or deleted profile returns
404 { "error": "Context not found" }.
Delete a profile
DELETE https://request.usestring.ai/v1/contexts/{contextId} returns 202:
{ "id": "d7a89cd7-c9f7-47fd-84df-77320f254cf8", "status": "DELETING", "deleted": false }The profile is gone at once: it cannot be used, inspect returns 404, list omits it, and its name
and its slot in the profile limit are free to reuse. Stored data is removed in the background. A
profile with a saving session open returns 409; read-only sessions do not block deletion.
Repeating the request answers 202 until the stored data has been removed, then 404.
Limits and retention
| Limit | Default value |
|---|---|
| Profiles per organization | 25. A deleted profile frees its slot immediately. |
| Concurrent saving sessions | 10 per organization, including saves still in progress. Read-only sessions are not counted. |
| Storage per organization | 1 GiB across all profiles, including saves in progress. |
| Profile size, compressed | 50 MiB per save. |
| Profile size, uncompressed | 256 MiB. |
| Idle expiry | 30 days after last use, saving or read-only. Status polling does not extend it. |
| Never-used empty profile expiry | 7 days after creation. |
Read-only sessions are still subject to the normal
browser session limits. At a limit, creation returns 429
and connecting closes with 1008. A save over either size limit reports archive_failed and keeps
the previous save. Contact us for higher limits.
Billing
Listing, inspecting, and deleting profiles do not require a positive balance. Creating profiles and running browser sessions do, at normal browser pricing. Refused connections are not billed.
What is restored
Cookies, localStorage, IndexedDB, service-worker registrations, and other persistent site data in the default browser context. A restored login can still expire or require verification under the website's own rules. Each connection starts a new browser session: open pages and running scripts do not resume, and cached resources may be downloaded again.
Status codes
| HTTP status | Meaning |
|---|---|
200 | Profile or list returned. |
201 | Profile created or an idempotent create replayed. |
202 | Deletion accepted; stored data is being removed. |
400 | Invalid request. |
401 | Missing or invalid API key. |
402 | Insufficient balance for creation. |
404 | Profile not found for this organization. |
409 | Name conflict, create in progress, or profile busy. |
429 | Profile, storage, or request-rate limit reached. |
500 | Internal error. |
WebSocket refusals use close frames; see GET /wss.