Browsers
solari-browser (library solari_browser) is the control plane: Client, the sessions / profiles / proxy namespaces, and the optional connect() helper. See the Rust SDK hub for install and configuration.
solari-browser = "0.1" # control plane
solari-browser = { version = "0.1", features = ["connect"] } # + connect()solari-browser is published to crates.io (source in the solari-sdk GitHub org).Contents
Client: Client::new(), sessions(), profiles(), proxy(), base_url()Sessions: create(), get(), release(), replay_url(), download_replay()Profiles: list(), create(), save(), delete()Proxy: countries()ConnectedBrowser: connect(), connect_endpoint(), into_parts(), disconnect()- Types:
ClientOptions,CreateSessionOptions,Session,SessionView,StorageState,ProxySpec,ProxyRequest,ProxyTier,ResolvedProxyConfig,ProxyCountries,Profile,ProfileSaveResult,ReplayUrl,SolariRegion,SolariError,SolariErrorCode
Client
The browser client. Creates sessions and manages profiles.
Constructors
Client::new()
fn new(opts: ClientOptions) -> Result<Client, SolariError>Creates a client. Does not open any connection.
Parameters:
optsClientOptions: see ClientOptions.
Returns: Result<Client, SolariError>
Errors: SolariError::Config if api_key or base_url is empty, or the HTTP client fails to build.
Example:
use solari_browser::{Client, ClientOptions};
let client = Client::new(ClientOptions::default_region(&api_key))?;
// Or point at a staging / self-hosted gateway:
let client = Client::new(ClientOptions::new(&api_key, "https://gw.example.com"))?;Debug is hand-written on ClientOptions and on the internal HttpTransport, so the key renders as "<redacted>". Client derives Debug, but its only field is that transport, so logging a client is safe too; the secret never reaches your logs.Methods
base_url()
fn base_url(&self) -> &strThe resolved API base URL, with any trailing slash stripped.
Returns: &str
Example:
assert_eq!(client.base_url(), "https://api.getsolari.com");Sessions
The session namespace. Obtain with client.sessions().
create()
async fn create(&self, options: CreateSessionOptions) -> Result<Session, SolariError>Acquires a browser session (POST /sessions). Unset options are omitted from the body entirely.
Parameters:
optionsCreateSessionOptions: see CreateSessionOptions.
Returns: Result<Session, SolariError>
Errors: SolariError::Api carrying status and the server code (e.g. FeatureRequiresPlan, ConcurrencyLimitExceeded); SolariError::Protocol if the response has no sessionId or wsEndpoint.
Example:
use solari_browser::{CreateSessionOptions, ProxySpec};
let session = client
.sessions()
.create(CreateSessionOptions::new().stealth(true).proxy(ProxySpec::country("us")))
.await?;
println!("{} expires {}", session.id, session.expires_at);.captcha(true) and any .proxy(…) request are rejected by the API unless .stealth(true) is also set.get()
async fn get(&self, id: &str) -> Result<SessionView, SolariError>Fetches a session’s current view (GET /sessions/:id). Every field is optional; unknown keys land in SessionView::extra.
Parameters:
id&str: session id.
Returns: Result<SessionView, SolariError>
Errors: SolariError::Api on a non-2xx status; SolariError::Protocol if the body is not JSON.
Example:
let view = client.sessions().get(&session.id).await?;
println!("{:?} {:?}", view.status, view.expires_at);GET /sessions/:id currently 404s on the production gateway. The pool does not serve it. The method and SessionView exist so the crate is ready if the route returns; that is also why every field is Option and unknown keys are preserved. Track a session’s deadline with Session::expires_at instead.release()
async fn release(&self, id: &str) -> Result<(), SolariError>Releases a session and waits for confirmation (DELETE /sessions/:id). A 404 is treated as success.
Parameters:
id&str: session id.
Returns: Result<(), SolariError>
Errors: SolariError::Api on any non-404 error status.
Example:
client.sessions().release(&session.id).await?;
// the replay is available ~1-3s laterrelease() / releaseAndWait() split. This method is the equivalent of releaseAndWait. tokio::spawn it yourself if you do not want to await the round-trip.replay_url()
async fn replay_url(&self, id: &str) -> Result<ReplayUrl, SolariError>Presigned URL for a recorded session’s replay. Requires .recording(true) at create time; available ~1 to 3s after release().
Parameters:
id&str: session id.
Returns: Result<ReplayUrl, SolariError> with url, expires_in_seconds (0 when omitted), content_encoding ("gzip" when omitted).
Errors: SolariError::Api if the replay is not ready; SolariError::Protocol if the body carries no url.
Example:
let replay = client.sessions().replay_url(&session.id).await?;
println!("{} ({}s)", replay.url, replay.expires_in_seconds);download_replay()
async fn download_replay(&self, id: &str) -> Result<Vec<u8>, SolariError>Resolves the replay URL and downloads the bytes in one call. The bytes are still content_encoding-encoded (gzipped NDJSON by default).
Parameters:
id&str: session id.
Returns: Result<Vec<u8>, SolariError>
Errors: everything replay_url() raises, plus SolariError::Api if the download itself is non-2xx.
Example:
let bytes = client.sessions().download_replay(&session.id).await?;
std::fs::write("replay.ndjson.gz", bytes)?;Profiles
Stored browser profiles (cookies + localStorage). Obtain with client.profiles().
list()
async fn list(&self) -> Result<Vec<Profile>, SolariError>Lists every profile on the account.
Returns: Result<Vec<Profile>, SolariError>
Errors: SolariError::Api on a non-2xx status; SolariError::Protocol on a malformed body.
Example:
for p in client.profiles().list().await? {
println!("{} {}", p.id, p.name);
}create()
async fn create(&self, name: impl Into<String>) -> Result<Profile, SolariError>Creates an empty profile. Attach it to a session with .profile_id(…) to persist cookies and localStorage.
Parameters:
nameimpl Into<String>: human-facing name.
Returns: Result<Profile, SolariError>
Errors: SolariError::Api with the server code on failure.
Example:
let profile = client.profiles().create("logged-in").await?;
let session = client
.sessions()
.create(CreateSessionOptions::new().profile_id(&profile.id))
.await?;save()
async fn save(
&self,
id: &str,
storage_state: &StorageState,
) -> Result<ProfileSaveResult, SolariError>Overwrites a profile with the given storage state (POST /profiles/:id/save).
Parameters:
id&str: profile id.storage_state&StorageState: cookies + origins.
Returns: Result<ProfileSaveResult, SolariError> with version and size_bytes, each 0 when the API omits them.
Errors: SolariError::Api on a non-2xx response.
Example:
use solari_browser::{Cookie, SameSite, StorageState};
let state = StorageState {
cookies: Some(vec![Cookie {
name: "sid".into(),
value: "abc".into(),
domain: Some(".example.com".into()),
same_site: Some(SameSite::Lax),
..Default::default()
}]),
..Default::default()
};
let saved = client.profiles().save(&profile.id, &state).await?;
println!("v{} ({} bytes)", saved.version, saved.size_bytes);delete()
async fn delete(&self, id: &str) -> Result<(), SolariError>Deletes a profile. Idempotent, so a 404 resolves successfully.
Parameters:
id&str: profile id.
Returns: Result<(), SolariError>
Errors: SolariError::Api on any non-404 error status.
Example:
client.profiles().delete(&profile.id).await?;Proxy
Managed proxy metadata. Obtain with client.proxy().
countries()
async fn countries(&self) -> Result<ProxyCountries, SolariError>Supported egress countries, and whether managed proxy egress is configured on this gateway at all.
Returns: Result<ProxyCountries, SolariError> with enabled, countries.
Errors: SolariError::Api on a non-2xx status.
Example:
let res = client.proxy().countries().await?;
if !res.enabled {
// sending a proxy spec would fail on this gateway
}
println!("{:?}", res.countries); // ["br", "de", "gb", "us"]ConnectedBrowser
A chromiumoxide Browser attached to a session over CDP, plus the spawned task pumping its event handler. Behind the connect feature.
Fields
browserchromiumoxide::Browser: the attached browser. Drive it with the usual chromiumoxide API.handlerJoinHandle<()>: the handler pump. It must keep running for as long as the browser is used. Everybrowsercall resolves through it.
ws_endpoint) path only. Attaching over raw CDP (which is what Rust does) bypasses that layer, so clicks are single-shot teleport-and-press. That is exactly the tell interaction-triggered anti-bots score. Expect lower pass rates on behaviourally-gated sites than the TypeScript SDK gets.Functions
connect()
async fn connect(session: &Session) -> Result<ConnectedBrowser, SolariError>Attaches to session.cdp_endpoint and spawns the handler pump. A free function on the crate root, not a method.
Parameters:
session&Session: the acquired session.
Returns: Result<ConnectedBrowser, SolariError>
Errors: SolariError::Connect if the CDP attach fails.
Example:
let session = client
.sessions()
.create(CreateSessionOptions::new().stealth(true))
.await?;
let connected = solari_browser::connect(&session).await?;
let page = connected.browser.new_page("https://example.com").await?;
println!("{:?}", page.url().await?);
connected.disconnect().await;
client.sessions().release(&session.id).await?;wss://, but chromiumoxide 0.7’s tokio-runtime feature only turns on its WebSocket dependency’s tokio-runtime feature — no TLS backend. With just features = ["connect"] and nothing else, connect() fails with "TLS support not compiled in" (confirmed against a live session). Add a direct dependency on the same async-tungstenite version with a TLS feature (e.g. async-tungstenite = { version = "0.27", features = ["tokio-native-tls"] }) so Cargo unifies it into the shared build.connect_endpoint()
async fn connect_endpoint(cdp_endpoint: &str) -> Result<ConnectedBrowser, SolariError>Attaches to an explicit CDP endpoint. Use it when you persisted a session’s cdp_endpoint and no longer hold the Session.
Parameters:
cdp_endpoint&str: the upstream CDP URL.
Returns: Result<ConnectedBrowser, SolariError>
Errors: SolariError::Connect if the attach fails.
Example:
let connected = solari_browser::connect_endpoint(&saved_cdp_url).await?;into_parts()
fn into_parts(self) -> (Browser, JoinHandle<()>)Splits into the browser and its handler task, when you want to own the two separately.
Returns: (chromiumoxide::Browser, JoinHandle<()>)
Example:
let (browser, handler) = connected.into_parts();
// … drive the browser; the handler ends on its own once it is dropped
drop(browser);
let _ = handler.await;disconnect()
async fn disconnect(self)Drops the browser (ending the CDP connection) and awaits the handler. Does not release the Solari session.
Returns: ()
Example:
connected.disconnect().await; // closes the connection
client.sessions().release(&session.id).await?; // frees the slotTypes
ClientOptions
Three constructors, then chainable builders. Reads no environment variables.
ClientOptions::new(api_key, base_url) // explicit gateway
ClientOptions::for_region(api_key, region) // a region's public API
ClientOptions::default_region(api_key) // SolariRegion::UsWest
ClientOptions::default_region(&api_key)
.max_attempts(3)
.backoff_ms(250)
.timeout_ms(30_000)api_keyString: required; empty is rejected.base_urlString: required; trailing slash stripped.max_attemptsu32: default2(one retry). Clamped to a minimum of 1.backoff_msu64: default500, fixed.timeout_msu64: default90_000, per attempt.
ConcurrencyLimitExceeded (429) is not retried. Match on SolariError::Api and back off yourself.CreateSessionOptions
Builder-style. Falsy fields are omitted from the request body; if every field is falsy, no body is sent at all.
CreateSessionOptions::new()
.stealth(true)
.captcha(true)
.recording(true)
.web_bot_auth(true)
.profile_id("p_123")
.proxy(ProxySpec::country("us"))profile_idOption<String>: attach a stored profile. Empty strings are omitted.recordingbool: record the session. Off by default.stealthbool: enable the runtime stealth shim. Off by default.captchabool: managed captcha solving. Requiresstealth.web_bot_authbool: sign outbound requests for Cloudflare Web Bot Auth. Independent ofstealth; inert unless Web Bot Auth is enabled for your account.proxyOption<ProxySpec>: managed egress. Requiresstealth.
Session
idString: session id.ws_endpointString: Playwright wire-protocol endpoint.cdp_endpointString: raw CDP endpoint, the one to use from Rust. Derived fromws_endpoint(/ws/→/cdp/) when the API omits it.expires_atString: ISO 8601 UTC deadline; the session auto-releases then. Falls back to ~1h from now when the API omits it.storage_stateOption<Option<StorageState>>: tri-state; see below.proxyOption<ResolvedProxyConfig>: present only when a managed proxy was requested.
wsEndpoint / cdpEndpoint through a Node-side loopback proxy. This crate has no such indirection: both fields are the upstream gateway endpoints, reachable from anywhere with the session still live. You can persist them and hand them to another machine.storage_state is a serde double-option, mirroring the TypeScript storageState?: StorageState | null. It is only populated when a profile was requested:
match &session.storage_state {
None => {} // no profile attached to this session
Some(None) => {} // profile attached, but it has no saved state yet
Some(Some(state)) => { // the profile's state
println!("{} cookies", state.cookies.as_ref().map_or(0, |c| c.len()));
}
}SessionView
Returned by get(). Every field is optional because the API proxies the pool’s view through verbatim.
session_id,status,expires_atOption<String>.extraMap<String, Value>:#[serde(flatten)]; every key the crate does not model survives here rather than being dropped.
StorageState
cookiesOption<Vec<Cookie>>: eachCookiehasname,value, and optionaldomain,path,expires,http_only,secure,same_site.originsOption<Vec<StorageOrigin>>: each hasoriginand optionallocal_storage(Vec<LocalStorageEntry>ofname/value).extraMap<String, Value>: flattened unknown keys.SameSite:Strict,Lax,None.
ProxySpec
The proxy field of a create request. Mirrors the TypeScript union string | ProxyRequest | "off" | "smart".
ProxySpec::country("us") // proxy: "us"
ProxySpec::off() // proxy: "off"
ProxySpec::smart() // proxy: "smart", gateway sweeps strategies on block
ProxySpec::from(ProxyRequest::country("us").tier(ProxyTier::Static))Shorthand(String): a country code, or the literals"off"/"smart".Request(ProxyRequest): a full egress request.From<ProxyRequest>is implemented, so.proxy(req)takes either directly.
ProxyRequest
Builder-style; unset fields are omitted from the wire.
ProxyRequest::country("us")
.tier(ProxyTier::Mobile)
.asn("21928")
.session("warm-1")
.session_duration(15)countryOption<String>: ISO-3166-1 alpha-2, lowercase. Defaults to"us"server-side.tierOption<ProxyTier>:Residential(default, rotating),Static(fixed ISP IP),Mobile(carrier IPs).asnOption<String>: pin egress to an ASN, e.g."20057"for AT&T Mobility.sessionOption<String>: sticky-session id (alnum + dash, ≤32 chars). Pins the egress IP.session_durationOption<u32>: sticky lifetime in minutes (1 to 30, default 10). Only withsession.state,cityOption<String>: US-only geo narrowing, e.g."california"/"los_angeles".
ResolvedProxyConfig
server,username,passwordString: the resolved proxy and its credentials.timezone_idString: timezone matching the egress IP.countryString,tierOption<ProxyTier>: what the gateway actually resolved.
ProxyCountries
enabledbool: false when managed proxy is unavailable; sending aproxyspec will then fail.countriesVec<String>: supported lowercase alpha-2 codes.
Profile / ProfileSaveResult
Profile:idString,nameString,extraMap<String, Value> (flattened unknown keys).ProfileSaveResult:versionu64,size_bytesu64. Both0when the API omits them.
ReplayUrl
urlString: presigned download URL.expires_in_secondsu64: URL lifetime;0when omitted.content_encodingString:"gzip"when omitted.
SolariRegion
UsWest(default): resolves tohttps://api.getsolari.com. The only region today; read it withregion.base_url().
SolariError
Every failure the crate raises. Derives thiserror::Error.
| Variant | Fields | Meaning |
|---|---|---|
Api | status, code, message | A non-2xx response from the API. |
Transport | message | Network failure, or retries exhausted. |
Protocol | message | A 2xx body missing required fields, or not JSON. |
Config | message | The client was constructed with invalid options. |
Connect | message | CDP attach failed. Only with the connect feature. |
Two accessors read across variants: .status() → Option<u16> and .code() → Option<&SolariErrorCode>, both None for non-Api variants.
SolariErrorCode
Parsed out of the API’s { "code": … } error body. Unknown codes are preserved in Other(String) rather than dropped, so the wire can grow new codes without breaking the crate.
FeatureRequiresPlan: the plan does not include the feature (e.g. captcha).ConcurrencyLimitExceeded: at the concurrent-session cap.PlanLimitExceeded: a plan quota is exhausted.BrowserUnhealthy: the acquired browser failed its health check.Other(String): a code this crate does not know.
use solari_browser::{SolariError, SolariErrorCode};
match client.sessions().create(opts).await {
Ok(session) => { /* … */ }
Err(e) if e.code() == Some(&SolariErrorCode::FeatureRequiresPlan) => {
eprintln!("upgrade required: {:?}", e.status());
}
Err(SolariError::Transport { message }) => eprintln!("retry later: {message}"),
Err(e) => return Err(e),
}