Solari

Browsers

solari-browser: classes Solari and BrowserSession. See the Python SDK hub for install and configuration.

pip install solari-browser

Contents

Solari

The browser client. Creates sessions and manages profiles.

Properties

  • sessions _Sessions: session create/release/replay calls.
  • profiles _Profiles: stored browser profile CRUD.
  • base_url str: the resolved gateway base URL, read-only.

Constructors

Solari()

Solari(
    api_key: str,
    *,
    region: str = "us-west",
    base_url: str | None = None,
    max_attempts: int = 2,
    backoff_ms: int = 500,
    timeout_ms: int = 90_000,
) -> Solari

Creates a client. Does not open any connection.

Parameters:

  • api_key str: required; the only positional argument.
  • region str: "us-west" only. Ignored when base_url is set.
  • base_url str | None: override the resolved region URL.
  • max_attempts, backoff_ms, timeout_ms int: retry policy; see request().

Returns: Solari

Raises: SolariError if api_key is empty or region is unknown.

Example:

from solari_browser import Solari

solari = Solari(api_key="slr_live_...")

# or as an async context manager, which closes the client on exit
async with Solari(api_key="slr_live_...") as solari:
    ...

Methods

launch()

async launch(
    *,
    profile_id: str | None = None,
    recording: bool = False,
    stealth: bool = False,
    captcha: bool = False,
    web_bot_auth: bool = False,
    proxy: str | ProxyRequest | None = None,
    retries: int = 0,
    probe: bool | None = None,
    probe_timeout_ms: int = 2_000,
) -> BrowserSession

Creates a session and connects to it over the Playwright wire protocol, returning a BrowserSession that wraps a real patchright Browser.

Parameters:

  • profile_id, recording, stealth, captcha, web_bot_auth, proxy: identical to sessions.create().
  • retries int: extra re-launch attempts. Default 0.
  • probe bool | None: probe the browser before returning. Defaults to True when retries > 0.
  • probe_timeout_ms int: probe cap. Default 2_000.

Returns: BrowserSession

Raises: the last connect error once attempts are spent; SolariError with code == BROWSER_UNHEALTHY if the probe fails; SolariError if patchright is not installed.

Example:

browser = await solari.launch(stealth=True, retries=2)
page = await browser.new_page()
await page.goto("https://example.com")
await browser.close()
retries only re-runs launch
Each retry calls sessions.create() again and sleeps 0.1s × attempt, and only for a health failure or a transient connect error. It does not change sessions.create()’s own HTTP retry policy. Python is the only non-TypeScript SDK with launch(), because patchright ships on PyPI while Go, Rust, and C++ have no Playwright client.

close()

async close() -> None

Closes the HTTP client and, if launch() was used, stops the patchright driver. It does not release live sessions.

Returns: None

Example:

await browser.close()  # release the session first
await solari.close()   # then shut the client down

request()

async request(method: str, path: str, body: Any | None = None) -> httpx.Response

Low-level authenticated call against the API, with the client’s retry and timeout policy applied. An escape hatch for endpoints the SDK does not wrap.

Parameters:

  • method str: HTTP verb.
  • path str: path appended to base_url, e.g. /sessions.
  • body Any | None: JSON-serialized when present.

Returns: httpx.Response, the raw response. A non-2xx is returned, not raised.

Raises: SolariError only when all attempts are exhausted.

Example:

res = await solari.request("GET", "/profiles")
print(res.status_code, res.json())
Retry policy
Only 502, 503, and 504 (and transport errors) are retried, with a fixed backoff_ms sleep, not exponential. max_attempts=2 means one retry. timeout_ms is per attempt. 429 is not retried.

sessions.create()

async sessions.create(
    *,
    profile_id: str | None = None,
    recording: bool = False,
    stealth: bool = False,
    captcha: bool = False,
    web_bot_auth: bool = False,
    proxy: str | ProxyRequest | None = None,
) -> Session

Acquires a browser session without connecting. Use it when you want to drive the endpoints yourself; otherwise use launch().

Parameters:

  • profile_id str | None: attach a stored profile.
  • recording bool: record the session.
  • stealth bool: enable the runtime stealth shim.
  • captcha bool: managed captcha solving. Requires stealth.
  • web_bot_auth bool: sign outbound requests for Cloudflare Web Bot Auth. Independent of stealth.
  • proxy str | ProxyRequest | None: a country code ("us"), a ProxyRequest, or the "off" / "smart" sentinels. Requires stealth.

Returns: Session

Raises: SolariError carrying status and the server code (e.g. FeatureRequiresPlan, ConcurrencyLimitExceeded).

Example:

session = await solari.sessions.create(stealth=True, proxy="us")
# connect with any Playwright-compatible client
browser = await chromium.connect(session.ws_endpoint)
captcha and proxy require stealth
captcha=True and any proxy request are rejected unless stealth=True is also set. web_bot_auth is independent.

sessions.release()

async sessions.release(session_id: str) -> None

Best-effort release. Failures are logged rather than raised. Use release_and_wait() when you need confirmation.

Parameters:

  • session_id str: session id.

Returns: None

Example:

await solari.sessions.release(session.id)

sessions.release_and_wait()

async sessions.release_and_wait(session_id: str) -> None

Releases a session and waits for the server to confirm. A 404 is treated as success.

Parameters:

  • session_id str: session id.

Returns: None

Raises: SolariError on any non-404 error status.

Example:

await solari.sessions.release_and_wait(session.id)
# the replay is available ~1-3s later

sessions.get()

async sessions.get(session_id: str) -> dict

Fetches the raw session view from GET /sessions/:id.

Parameters:

  • session_id str: session id.

Returns: dict, the decoded JSON body.

Raises: SolariError on any error status.

Dead endpoint: this always 404s
The pool implements no GET /sessions/:id route, so this method has no working server behind it. Track a session with the Session object create() returned instead.

sessions.get_replay_url()

async sessions.get_replay_url(session_id: str) -> ReplayUrl

Returns a presigned URL for a recorded session’s replay. Requires recording=True at create time.

Parameters:

  • session_id str: session id.

Returns: ReplayUrl: url, expires_in_seconds, content_encoding.

Raises: SolariError if the replay is not ready or does not exist.

Example:

replay = await solari.sessions.get_replay_url(session.id)
print(replay.url, replay.expires_in_seconds)

sessions.download_replay()

async sessions.download_replay(session_id: str) -> bytes

Resolves the replay URL and downloads the NDJSON bytes in one call.

Parameters:

  • session_id str: session id.

Returns: bytes, gzipped NDJSON.

Raises: SolariError if the download fails.

Example:

data = await solari.sessions.download_replay(session.id)
with open("replay.ndjson.gz", "wb") as f:
    f.write(data)

profiles.create()

async profiles.create(name: str) -> Profile

Creates an empty profile. Attach it to a session with profile_id to persist cookies and localStorage.

Parameters:

  • name str: human-facing name.

Returns: Profile

Raises: SolariError with the server code on failure.

Example:

profile = await solari.profiles.create("logged-in")
browser = await solari.launch(profile_id=profile.id)

profiles.list()

async profiles.list() -> list[Profile]

Lists every profile on the account.

Returns: list[Profile]

Example:

for p in await solari.profiles.list():
    print(p.id, p.name)

profiles.save()

async profiles.save(profile_id: str, storage_state: StorageState) -> SaveResult

Overwrites a profile with the given storage state. Pair it with Playwright’s context.storage_state() to snapshot a logged-in session.

Parameters:

  • profile_id str: profile id.
  • storage_state StorageState: a plain dict of cookies + origins.

Returns: SaveResult: version, size_bytes.

Raises: SolariError on a non-2xx response.

Example:

state = await browser.contexts()[0].storage_state()
result = await solari.profiles.save(profile.id, state)
print(result.version, result.size_bytes)

profiles.delete()

async profiles.delete(profile_id: str) -> None

Deletes a profile. Idempotent, so a 404 is success.

Parameters:

  • profile_id str: profile id.

Returns: None

Example:

await solari.profiles.delete(profile.id)

BrowserSession

A live browser and its session. Returned by launch(); not constructed directly. Works as an async context manager, which calls close() on exit.

Properties

  • session Session: the raw session record.
  • id str: session id.
  • expires_at str: ISO 8601 UTC deadline; the session auto-releases then.
  • proxy ResolvedProxyConfig | None: resolved proxy credentials, when one was requested.
  • ws_endpoint str: upstream Playwright wire-protocol endpoint.
  • cdp_endpoint str: upstream raw-CDP endpoint.
  • raw Browser: the underlying patchright Browser, for APIs this wrapper does not surface.
Endpoints are upstream URLs, unlike TypeScript
ws_endpoint and cdp_endpoint point straight at the gateway. The TypeScript SDK rewrites them to a loopback proxy it runs in-process; Python deliberately does not, so these URLs are real, remote, and reusable from another process.
Raw CDP skips input humanization
Driving the browser over cdp_endpoint bypasses the pool’s Playwright-path mouse and keyboard humanization. Relevant only if you care about stealth.

Methods

is_connected()

is_connected() -> bool

True while the browser connection is alive. Synchronous.

Returns: bool

Example:

if not browser.is_connected():
    raise RuntimeError("session dropped")

version

version: str

The browser version string. A property, not a method. Do not call it.

Returns: str, e.g. "130.0.6723.31".

Example:

print(browser.version)

contexts()

contexts() -> list[BrowserContext]

All open contexts. Sessions ship with a default context at contexts()[0]. Use it rather than opening a new one. Synchronous.

Returns: list[BrowserContext]

Example:

ctx = browser.contexts()[0]
page = await ctx.new_page()

new_context()

async new_context(**kwargs) -> BrowserContext

Opens a fresh context. Most callers want contexts()[0] instead, because a new context does not inherit the session’s profile or stealth setup.

Parameters:

  • **kwargs: passed through to patchright’s browser.new_context().

Returns: BrowserContext

Example:

ctx = await browser.new_context(locale="en-GB")

new_page()

async new_page() -> Page

Opens a fresh page in a new context.

Returns: Page

Example:

page = await browser.new_page()
await page.goto("https://example.com")

close()

async close() -> None

Closes the browser and releases the session, waiting for confirmation. Idempotent. A browser-close failure is swallowed, because releasing the slot matters more.

Returns: None

Raises: SolariError if the release itself fails.

Example:

async with await solari.launch(stealth=True) as browser:
    page = await browser.new_page()
    await page.goto("https://example.com")
# released automatically at scope exit

Types

Session

A dataclass. Returned by sessions.create().

  • id str: session id.
  • ws_endpoint str: upstream Playwright endpoint, for chromium.connect().
  • cdp_endpoint str: upstream CDP endpoint, for connect_over_cdp().
  • expires_at str: ISO 8601 UTC deadline.
  • storage_state StorageState | None | _Unset: tri-state; see below.
  • proxy ResolvedProxyConfig | None: present only when a managed proxy was requested.
storage_state is tri-state
UNSET (the sentinel, and the default) means no profile was attached. None means a profile exists but is empty. A dict is the profile’s state. TypeScript encodes this as undefined vs null; Python has only None, hence the sentinel. Test with is UNSET / is None, never truthiness. UNSET is falsy.

StorageState

An alias for dict[str, Any], a Playwright storage state (cookies + origins/localStorage) passed straight through to patchright. No schema is imposed on it.

ProxyRequest

A dataclass. Only non-None fields are sent, because the gateway distinguishes absent from null.

  • country str | None: ISO-3166-1 alpha-2, lowercase. Server default "us".
  • tier "residential" | "static" | "mobile" | None: server default "residential" (rotating).
  • asn str | None: pin egress to an ASN.
  • session str | None: sticky-session id (alnum + dash, ≤32 chars).
  • session_duration int | None: sticky lifetime in minutes (1 to 30, default 10). Only with session.
  • state, city str | None: US-only geo narrowing.
from solari_browser import ProxyRequest

browser = await solari.launch(
    stealth=True,
    proxy=ProxyRequest(country="us", tier="mobile", asn="21928"),
)

ResolvedProxyConfig

  • server str: proxy server URL.
  • username, password str: credentials.
  • timezone_id str: timezone matching the egress IP.
  • country str: resolved country.
  • tier str | None: resolved tier.

Profile

  • id str: profile id.
  • name str: human-facing name.
  • raw dict: the full wire object, for forward compatibility.

ReplayUrl

  • url str: presigned download URL.
  • expires_in_seconds int: URL lifetime.
  • content_encoding str: default "gzip".

SaveResult

  • version int: the profile’s new version.
  • size_bytes int: stored size.

SolariError

Extends Exception. Every failure the browser SDK raises.

  • status int | None: HTTP status, when the error came from a response.
  • cause BaseException | None: the underlying error.
  • code str | None: the gateway code. The known values are exported as constants: FEATURE_REQUIRES_PLAN, CONCURRENCY_LIMIT_EXCEEDED, PLAN_LIMIT_EXCEEDED, BROWSER_UNHEALTHY. Unknown codes pass through as plain strings.
from solari_browser import FEATURE_REQUIRES_PLAN, SolariError

try:
    await solari.launch(stealth=True, captcha=True)
except SolariError as e:
    if e.code == FEATURE_REQUIRES_PLAN:
        print("upgrade required:", e.status)

derive_cdp_from_ws()

derive_cdp_from_ws(ws_endpoint: str) -> str

Module-level helper mapping /ws/<id> to /cdp/<id>. The gateway normally returns cdp_endpoint explicitly; this is the fallback for older gateways. Returns its input unchanged if it is not a parseable /ws/ URL.