Browsers
solari-browser: classes Solari and BrowserSession. See the Python SDK hub for install and configuration.
pip install solari-browserContents
Solari: Solari(), launch(), close(), request(), sessions.create(), sessions.release(), sessions.release_and_wait(), sessions.get(), sessions.get_replay_url(), sessions.download_replay(), profiles.create(), profiles.list(), profiles.save(), profiles.delete()BrowserSession: is_connected(), version, contexts(), new_context(), new_page(), close()- Types:
Session,StorageState,ProxyRequest,ResolvedProxyConfig,Profile,ReplayUrl,SaveResult,UNSET,SolariError,derive_cdp_from_ws()
Solari
The browser client. Creates sessions and manages profiles.
Properties
sessions_Sessions: session create/release/replay calls.profiles_Profiles: stored browser profile CRUD.base_urlstr: 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,
) -> SolariCreates a client. Does not open any connection.
Parameters:
api_keystr: required; the only positional argument.regionstr:"us-west"only. Ignored whenbase_urlis set.base_urlstr | None: override the resolved region URL.max_attempts,backoff_ms,timeout_msint: 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,
) -> BrowserSessionCreates 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().retriesint: extra re-launch attempts. Default0.probebool | None: probe the browser before returning. Defaults toTruewhenretries > 0.probe_timeout_msint: probe cap. Default2_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()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() -> NoneCloses 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 downrequest()
async request(method: str, path: str, body: Any | None = None) -> httpx.ResponseLow-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:
methodstr: HTTP verb.pathstr: path appended tobase_url, e.g./sessions.bodyAny | 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())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,
) -> SessionAcquires a browser session without connecting. Use it when you want to drive the endpoints yourself; otherwise use launch().
Parameters:
profile_idstr | None: attach a stored profile.recordingbool: record the session.stealthbool: enable the runtime stealth shim.captchabool: managed captcha solving. Requiresstealth.web_bot_authbool: sign outbound requests for Cloudflare Web Bot Auth. Independent ofstealth.proxystr | ProxyRequest | None: a country code ("us"), aProxyRequest, or the"off"/"smart"sentinels. Requiresstealth.
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=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) -> NoneBest-effort release. Failures are logged rather than raised. Use release_and_wait() when you need confirmation.
Parameters:
session_idstr: session id.
Returns: None
Example:
await solari.sessions.release(session.id)sessions.release_and_wait()
async sessions.release_and_wait(session_id: str) -> NoneReleases a session and waits for the server to confirm. A 404 is treated as success.
Parameters:
session_idstr: 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 latersessions.get()
async sessions.get(session_id: str) -> dictFetches the raw session view from GET /sessions/:id.
Parameters:
session_idstr: session id.
Returns: dict, the decoded JSON body.
Raises: SolariError on any error status.
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) -> ReplayUrlReturns a presigned URL for a recorded session’s replay. Requires recording=True at create time.
Parameters:
session_idstr: 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) -> bytesResolves the replay URL and downloads the NDJSON bytes in one call.
Parameters:
session_idstr: 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) -> ProfileCreates an empty profile. Attach it to a session with profile_id to persist cookies and localStorage.
Parameters:
namestr: 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) -> SaveResultOverwrites a profile with the given storage state. Pair it with Playwright’s context.storage_state() to snapshot a logged-in session.
Parameters:
profile_idstr: profile id.storage_stateStorageState: 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) -> NoneDeletes a profile. Idempotent, so a 404 is success.
Parameters:
profile_idstr: 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
sessionSession: the raw session record.idstr: session id.expires_atstr: ISO 8601 UTC deadline; the session auto-releases then.proxyResolvedProxyConfig | None: resolved proxy credentials, when one was requested.ws_endpointstr: upstream Playwright wire-protocol endpoint.cdp_endpointstr: upstream raw-CDP endpoint.rawBrowser: the underlying patchrightBrowser, for APIs this wrapper does not surface.
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.cdp_endpoint bypasses the pool’s Playwright-path mouse and keyboard humanization. Relevant only if you care about stealth.Methods
is_connected()
is_connected() -> boolTrue while the browser connection is alive. Synchronous.
Returns: bool
Example:
if not browser.is_connected():
raise RuntimeError("session dropped")version
version: strThe 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) -> BrowserContextOpens 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’sbrowser.new_context().
Returns: BrowserContext
Example:
ctx = await browser.new_context(locale="en-GB")new_page()
async new_page() -> PageOpens a fresh page in a new context.
Returns: Page
Example:
page = await browser.new_page()
await page.goto("https://example.com")close()
async close() -> NoneCloses 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 exitTypes
Session
A dataclass. Returned by sessions.create().
idstr: session id.ws_endpointstr: upstream Playwright endpoint, forchromium.connect().cdp_endpointstr: upstream CDP endpoint, forconnect_over_cdp().expires_atstr: ISO 8601 UTC deadline.storage_stateStorageState | None | _Unset: tri-state; see below.proxyResolvedProxyConfig | None: present only when a managed proxy was requested.
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.
countrystr | None: ISO-3166-1 alpha-2, lowercase. Server default"us".tier"residential" | "static" | "mobile" | None: server default"residential"(rotating).asnstr | None: pin egress to an ASN.sessionstr | None: sticky-session id (alnum + dash, ≤32 chars).session_durationint | None: sticky lifetime in minutes (1 to 30, default 10). Only withsession.state,citystr | 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
serverstr: proxy server URL.username,passwordstr: credentials.timezone_idstr: timezone matching the egress IP.countrystr: resolved country.tierstr | None: resolved tier.
Profile
idstr: profile id.namestr: human-facing name.rawdict: the full wire object, for forward compatibility.
ReplayUrl
urlstr: presigned download URL.expires_in_secondsint: URL lifetime.content_encodingstr: default"gzip".
SaveResult
versionint: the profile’s new version.size_bytesint: stored size.
SolariError
Extends Exception. Every failure the browser SDK raises.
statusint | None: HTTP status, when the error came from a response.causeBaseException | None: the underlying error.codestr | 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) -> strModule-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.
