Browsers
solari::browser (target solari_browser) has two classes, Client and CdpConnection. The browser control plane: sessions, profiles, replays, proxy countries. Blocking and synchronous over libcurl. See the C++ SDK hub for install and configuration.
#include <solari/browser/browser.hpp> // umbrella headerlaunch() returns a live Playwright Browser. Playwright and Puppeteer have no C++ client, so this binding stops at the control plane: it creates the session and hands back Session::cdpEndpoint. Drive that with a third-party CDP client, or with the minimal CdpConnection escape hatch. For anything non-trivial, split the process. Drive the browser from a language with a real Playwright client and keep C++ on the session/profile lifecycle. Stealth, proxy, captcha, and recording are all applied server-side, so nothing is lost.Contents
Client: Client(), proxyCountries(), http(), sessions.create(), sessions.get(), sessions.release(), sessions.getReplayUrl(), sessions.downloadReplay(), profiles.list(), profiles.create(), profiles.remove(), profiles.save()CdpConnection: CdpConnection(), connect(), send(), onEvent(), close(), connected()- Pure helpers:
buildCreateBody,parseSessionResponse,parseReplayUrl,deriveCdpFromWs,HttpTransport::prepare - Types:
ClientOptions,CreateSessionOptions,Session,StorageState,ProxyRequest,ProxySpec,ResolvedProxyConfig,Profile,SaveProfileResult,ReplayUrl,ProxyCountries,SolariError
Client
The API client. Creates sessions and manages profiles. Not copyable.
Properties
sessionsSessionsResource: a member object, not a method:client.sessions.create().profilesProfilesResource: likewise,client.profiles.list().
Constructors
Client()
explicit Client(const ClientOptions& options)Creates a client. Opens no connection.
Parameters:
optionsClientOptions: see ClientOptions.
Throws: SolariError if apiKey is empty.
Example:
solari::browser::ClientOptions opts;
opts.apiKey = "slr_live_...";
solari::browser::Client client(opts);Methods
proxyCountries()
ProxyCountries proxyCountries()GET /proxy/countries: the egress countries this gateway supports, and whether proxying is configured at all.
Returns: ProxyCountries (enabled, countries).
Throws: SolariError on a non-2xx response.
Example:
auto pc = client.proxyCountries();
if (!pc.enabled) std::cerr << "managed proxy is unavailable\n";
for (const auto& c : pc.countries) std::cout << c << "\n";http()
HttpTransport& http()The underlying transport, for endpoints the SDK does not wrap. Use http().request(method, path, body). It applies the client’s auth, retry, and timeout policy.
Returns: HttpTransport&, whose request() returns an HttpResponse (status, body, ok()). Non-2xx is data, not an exception.
Example:
auto res = client.http().request("GET", "/profiles");
std::cout << res.status << " " << res.body << "\n";sessions.create()
Session create(const CreateSessionOptions& opts = {})POST /sessions: acquires a browser session. The returned Session carries the endpoints you drive.
Parameters:
optsCreateSessionOptions:profileId,recording,stealth,captcha,webBotAuth,proxy.
Returns: Session
Throws: SolariError carrying status and the server code (e.g. FeatureRequiresPlan, ConcurrencyLimitExceeded).
Example:
solari::browser::CreateSessionOptions o;
o.stealth = true;
o.proxy = solari::browser::ProxyRequest{}; // or std::string("us")
auto session = client.sessions.create(o);
std::cout << session.cdpEndpoint << "\n";buildCreateBody() omits every falsy/unset field, so create({}) yields an empty object and the client sends no body at all. storageState is fetched only when you passed a profileId.captcha = true and any proxy request unless stealth = true is also set.sessions.get()
Json get(const std::string& id)GET /sessions/:id, returned as raw nlohmann::json.
Parameters:
idstd::string: session id.
Returns: Json, whatever the pool returns.
Throws: SolariError on a non-2xx response. Today, always.
Example:
auto raw = client.sessions.get(session.id); // throws SolariError, status 404GET /sessions/:id route. Every call fails with SolariError, status 404. It is kept for completeness and returns raw JSON because a response that is never produced cannot be typed. Do not build on it.sessions.release()
void release(const std::string& id)DELETE /sessions/:id. Blocking, with the TS SDK’s releaseAndWait() semantics, not its fire-and-forget release(). Idempotent: a 404 is success.
Parameters:
idstd::string: session id.
Throws: SolariError on any non-404 error status.
Example:
client.sessions.release(session.id);
// the replay is available ~1-3s latersessions.getReplayUrl()
ReplayUrl getReplayUrl(const std::string& id)GET /sessions/:id/replay-url: a presigned link to the recording. Requires recording = true at create time.
Parameters:
idstd::string: session id.
Returns: ReplayUrl (url, expiresInSeconds, contentEncoding).
Throws: SolariError, a 404 until the replay lands, about 1 to 3s after release.
Example:
auto replay = client.sessions.getReplayUrl(session.id);
std::cout << replay.url << " expires in " << replay.expiresInSeconds << "s\n";sessions.downloadReplay()
std::string downloadReplay(const std::string& id)Resolves the replay URL and downloads the bytes in one call.
Parameters:
idstd::string: session id.
Returns: std::string, gzipped NDJSON bytes.
Throws: SolariError if the download fails.
Example:
auto bytes = client.sessions.downloadReplay(session.id);
std::ofstream("replay.ndjson.gz", std::ios::binary).write(bytes.data(), bytes.size());Authorization header. S3 signs the URL itself and rejects the header. One attempt, then it throws.profiles.list()
std::vector<Profile> list()GET /profiles: every profile owned by the caller’s org. Non-object rows are skipped.
Returns: std::vector<Profile>
Throws: SolariError on a non-2xx response.
Example:
for (const auto& p : client.profiles.list())
std::cout << p.id << " " << p.name << "\n";profiles.create()
Profile create(const std::string& name)POST /profiles: creates an empty profile. Attach it to a session via CreateSessionOptions::profileId to persist cookies and localStorage.
Parameters:
namestd::string: human-facing name.
Returns: Profile (id, name, raw).
Throws: SolariError, e.g. PlanLimitExceeded when the profile quota is spent.
Example:
auto profile = client.profiles.create("logged-in");
solari::browser::CreateSessionOptions o;
o.profileId = profile.id;
auto session = client.sessions.create(o);profiles.remove()
void remove(const std::string& id)DELETE /profiles/:id. Named remove(), not delete, which is a C++ keyword. Idempotent: a 404 is success.
Parameters:
idstd::string: profile id.
Throws: SolariError on any non-404 error status.
Example:
client.profiles.remove(profile.id);profiles.save()
SaveProfileResult save(const std::string& id, const StorageState& storageState)POST /profiles/:id/save: overwrites a profile with the given storage state.
Parameters:
idstd::string: profile id.storageStateStorageState: anlohmann::jsonblob of cookies + origins.
Returns: SaveProfileResult (version, sizeBytes, both 0 when the gateway omits them).
Throws: SolariError on a non-2xx response.
Example:
solari::browser::StorageState state = nlohmann::json::parse(R"({
"cookies": [{"name": "sid", "value": "abc", "domain": "example.com"}],
"origins": []
})");
auto r = client.profiles.save(profile.id, state);
std::cout << "v" << r.version << " " << r.sizeBytes << " bytes\n";CdpConnection
One raw-CDP WebSocket. Compiled only when built with SOLARI_WITH_WS=ON (the default). Not copyable; send() is thread-safe.
CdpConnection does exactly one thing: send a CDP command and block for its correlated reply. No page/frame/element model, no auto-attach, no waiting primitives, no navigation lifecycle. C++’s CDP ecosystem is thin. Evaluate any third-party client yourself rather than taking a recommendation. For non-trivial work, split the process (see the note at the top).Constructors
CdpConnection()
explicit CdpConnection(std::string cdpEndpoint, long callTimeoutMs = 30000)Wraps a session’s raw-CDP URL. Opens nothing until connect().
Parameters:
cdpEndpointstd::string:Session::cdpEndpoint.callTimeoutMslong: per-send()timeout. Default30000.
Example:
solari::browser::CdpConnection cdp(session.cdpEndpoint);/cdp/ upgrade needs no Bearer header. The session id in the path is HMAC-signed, so the URL authenticates itself. Treat it as a secret.Methods
connect()
void connect()Opens the socket and blocks until the handshake completes.
Throws: SolariError if the handshake fails.
Example:
cdp.connect();send()
nlohmann::json send(const std::string& method,
const nlohmann::json& params = nlohmann::json::object(),
const std::optional<std::string>& sessionId = std::nullopt)Sends one CDP command and blocks for its reply. Returns the result object.
Parameters:
methodstd::string: CDP method, e.g."Page.navigate".paramsnlohmann::json: command params.sessionIdstd::optional<std::string>: targets a flattened target session. Required for page-scoped commands.
Returns: nlohmann::json, the result object.
Throws: SolariError on a protocol error, on timeout, or when the socket is not open.
Example:
auto version = cdp.send("Browser.getVersion");
std::cout << version["product"].get<std::string>() << "\n";
// Attach to a page target; flattened sessions carry a CDP sessionId.
auto targets = cdp.send("Target.getTargets");
auto targetId = targets["targetInfos"][0]["targetId"].get<std::string>();
auto att = cdp.send("Target.attachToTarget", {{"targetId", targetId}, {"flatten", true}});
auto sid = att["sessionId"].get<std::string>();
cdp.send("Page.enable", {}, sid);
cdp.send("Page.navigate", {{"url", "https://example.com"}}, sid);onEvent()
void onEvent(std::function<void(const nlohmann::json&)> handler)Subscribes to CDP events (any frame without an id, e.g. Page.loadEventFired).
Parameters:
handlerstd::function<void(const nlohmann::json&)>: invoked on the receive thread.
Example:
cdp.onEvent([](const nlohmann::json& ev) {
if (ev.value("method", "") == "Page.loadEventFired") std::cout << "loaded\n";
});
cdp.connect(); // set the handler BEFORE connectingconnect(), otherwise early events are missed. It runs on the receive thread: do not call send() or close() from inside it.close()
void close()Closes the socket and fails every in-flight send(). Idempotent.
Example:
cdp.close();
client.sessions.release(session.id); // close() does NOT release the sessionconnected()
bool connected() constWhether the socket is currently open.
Returns: bool
Example:
if (!cdp.connected()) throw std::runtime_error("cdp dropped");Pure helpers
The request builders and response parsers are exported so you can assert wire shapes in tests with no network. They are pure functions over nlohmann::json.
| Function | Purpose |
|---|---|
buildCreateBody(opts) | The POST /sessions body. Empty options → empty object → no body sent. |
parseSessionResponse(body) | A 201 body → Session. Derives cdpEndpoint when omitted; defaults expiresAt to one hour out. |
parseReplayUrl(body) | ReplayUrl, defaulting expiresInSeconds → 0 and contentEncoding → "gzip". |
parseProfile / parseProfiles / parseSaveResult / parseProxyCountries | The remaining response shapes. |
deriveCdpFromWs(wsEndpoint) | Swaps the /ws/<id> path prefix for /cdp/<id>. |
HttpTransport::prepare(...) | The concrete request bytes (method, url, headers, body) without sending. |
encodeURIComponent / isRetryableStatus / iso8601FromNow | Path-segment escaping, the 502/503/504 predicate, and ISO-8601 stamping. |
proxyToJson(spec) | A ProxySpec → the JSON sent under proxy. Unset ProxyRequest fields are omitted. |
parseErrorCode(body) / throwHttpError(what, status, body) | Lift code out of a JSON error body (nullopt when absent or not a string), and raise the corresponding SolariError. The client already does this for you. |
fetchUrl(url, timeoutMs) | GET an absolute URL with no auth header, for presigned S3 links, which carry their own signature and reject Authorization. |
using namespace solari::browser;
// No client, no gateway, no network.
CreateSessionOptions o;
o.stealth = true;
CHECK(buildCreateBody(o) == nlohmann::json({{"stealth", true}}));
CHECK(buildCreateBody(CreateSessionOptions{}).empty()); // -> no body
CHECK(deriveCdpFromWs("wss://api.getsolari.com/ws/abc?x=1")
== "wss://api.getsolari.com/cdp/abc?x=1");"ws" (which it does not mangle). Everything but the path prefix is preserved; an input that does not parse, or whose path is not /ws/…, is returned unchanged.Types
ClientOptions
apiKeystd::string: required. Formatslr_live_<id>_<secret>.baseUrlstd::string: defaulthttps://api.getsolari.com.maxAttemptsint: default2(one retry). TOTAL attempts, not retries.backoffMslong: default500, fixed.timeoutMslong: default90000, per attempt.
CreateSessionOptions
profileIdstd::optional<std::string>: attach a stored profile.recordingbool: record the session.falseby default.stealthbool: enable the runtime stealth shim.falseby default.captchabool: managed captcha solving. Requiresstealth.webBotAuthbool: sign outbound requests for Cloudflare Web Bot Auth. Independent ofstealth.proxystd::optional<ProxySpec>: managed egress. Requiresstealth.
ProxySpec
std::variant<std::string, ProxyRequest> mirrors the TypeScript union string | ProxyRequest. The string form takes a country code, or the sentinels "off" / "smart".
CreateSessionOptions o;
o.stealth = true;
o.proxy = std::string("smart"); // sentinel
o.proxy = std::string("us"); // country
ProxyRequest p; // structured
p.country = "gb";
p.tier = "mobile";
o.proxy = p;ProxyRequest
Every field is optional.
country: ISO-3166-1 alpha-2, lowercase. Gateway default"us".tier:"residential"(default, rotating),"static"(fixed ISP IP), or"mobile".asn: pin egress to an ASN, e.g."20057".session: sticky-session id (alnum + dash, ≤32 chars). Pins the egress IP.sessionDurationstd::optional<int>: sticky lifetime in minutes (1 to 30, default 10). Only withsession.state: US-only geo narrowing, e.g."california".city: US-only city pin, e.g."los_angeles".
Session
idstd::string: session id.wsEndpointstd::string: Playwright wire protocol (/ws/<id>). Returned for completeness; only a Playwright client can use it, and there is none for C++.cdpEndpointstd::string: raw CDP (/cdp/<id>). This is the one you drive.expiresAtstd::string: ISO 8601 UTC deadline; the session auto-releases then.storageStatestd::optional<StorageState>: tri-state. Unset = no profile attached; present-but-JSON-null (storageState->is_null()) = profile exists but is empty; an object = the profile’s state.proxystd::optional<ResolvedProxyConfig>: set only when a managed proxy was requested.
Authorization header. Treat them as secrets.StorageState
using StorageState = nlohmann::json: a Playwright storageState blob, {cookies: [...], origins: [...]}. Kept raw: the SDK never interprets it, only moves it between a profile and a browser.
ResolvedProxyConfig
server,username,passwordstd::string: proxy URL and credentials.timezoneIdstd::string: timezone matching the egress IP.countrystd::string: resolved country.tierstd::optional<std::string>: resolved tier.
Profile
idstd::string,namestd::string.rawJson: the full row the platform returned, so a new field never forces an SDK bump.
SaveProfileResult / ReplayUrl / ProxyCountries
SaveProfileResult:version,sizeByteslong long: both0when omitted.ReplayUrl:urlstd::string,expiresInSecondslong long (default0),contentEncodingstd::string (default"gzip").ProxyCountries:enabledbool: false when managed proxy is unavailable;countriesstd::vector<std::string>: lowercase alpha-2, sorted.
SolariError
Extends std::runtime_error. The single exception type the browser SDK throws.
statusstd::optional<int>: HTTP status; unset for transport-level errors.codestd::optional<std::string>: the machine-readable code lifted out of the gateway’s JSON error body.
Branch on code rather than the message. The constants live in solari::browser::error_code:
| Constant | Status | Meaning |
|---|---|---|
FeatureRequiresPlan | 403 | Stealth / proxy / captcha asked for on a plan without it. |
ConcurrencyLimitExceeded | 429 | The org is at its concurrent-session cap. |
PlanLimitExceeded | 403 | A plan quota (e.g. stored profiles) is exhausted. |
BrowserUnhealthy | None | The acquired browser failed its health probe. |
try {
auto session = client.sessions.create(o);
} catch (const solari::browser::SolariError& e) {
if (e.code == solari::browser::error_code::ConcurrencyLimitExceeded) {
// 429 is NOT retried by the transport. Back off yourself.
std::this_thread::sleep_for(std::chrono::seconds(2));
}
std::cerr << e.what() << " status=" << e.status.value_or(0) << "\n";
}ConcurrencyLimitExceeded (429) is not retried. Handle it yourself.