Go language binding for the Solari Browser platform: create, inspect, and
release managed remote-browser sessions, and manage the stored profiles they
attach. It behaves identically on the wire to the reference TypeScript SDK
(@solarisdk/browser, sdk/src/index.ts).
go get github.com/solari-sdk/solari-browser-go
Module path: github.com/solari-sdk/solari-browser-go, package solari.
Requires Go 1.23+ and github.com/chromedp/chromedp.
Not to be confused with
github.com/solari-sdk/solari-sandbox-go— that is the desktop/sandbox SDK (microVMs, commands, files, code). This one is the browser platform.
This SDK is the REST control plane plus two levels of browser attachment. The
TypeScript SDK's launch() returns a live Playwright Browser — Playwright
has no Go client, so there is no port of that object model, and there won't be
one.
Instead, Sessions.Create hands back the session's raw CDP endpoint, and you
drive the browser with chromedp, either
directly via Connect, or in one call via Sessions.Launch (create, connect,
seed an attached profile's cookies, health-probe, retry, release on Close —
added 2026-10-06, previously missing here while TS/Python had it). Both
endpoints are returned exactly as the gateway issued them — unlike the Node SDK,
nothing is rewritten to a loopback proxy.
| Field | Use |
|---|---|
Session.CDPEndpoint |
What you want. Raw CDP — Connect, or chromedp / any CDP client. |
Session.WSEndpoint |
Playwright wire protocol. No Go client exists; returned for completeness. |
- Context-first. Every network method takes
ctx context.Contextfirst. - Errors as typed values. Every failure is a
*SolariErrorcarryingStatus(HTTP status, or 0 for transport errors) andCode(the gateway's error code). Match witherrors.As, or theIsCodeshorthand. - Nothing is read from the environment. Pass the API key and any overrides
explicitly to
NewClient. - Retries are conservative. Each request is tried up to
MaxAttemptstimes (default 2) with a fixedBackoffMsdelay (default 500ms). Only transport errors and HTTP 502/503/504 are retried — notably not 429, where retrying cannot help.
package main
import (
"context"
"log"
"github.com/chromedp/chromedp"
solari "github.com/solari-sdk/solari-browser-go"
)
func main() {
ctx := context.Background()
client, err := solari.NewClient(solari.ClientOptions{
APIKey: "slr_live_<id>_<secret>",
})
if err != nil {
log.Fatal(err)
}
session, err := client.Sessions.Create(ctx, solari.CreateSessionOptions{
Stealth: true,
Proxy: solari.ProxyCountry("us"),
})
if err != nil {
log.Fatal(err)
}
defer client.Sessions.Release(context.Background(), session.ID)
browserCtx, cancel, err := solari.Connect(ctx, session)
if err != nil {
log.Fatal(err)
}
defer cancel()
var title string
if err := chromedp.Run(browserCtx,
chromedp.Navigate("https://example.com"),
chromedp.Title(&title),
); err != nil {
log.Fatal(err)
}
log.Println("title:", title)
}Connect dials nothing itself — the connection is established on the first
chromedp.Run, and failures surface there. cancel() detaches the client; it
does not release the session, which keeps running until Sessions.Release
or its ExpiresAt deadline.
Create + connect + seed + probe + retry + release, in one call:
launched, err := client.Sessions.Launch(ctx, solari.LaunchOptions{
Create: solari.CreateSessionOptions{Stealth: true},
Retries: 2,
})
if err != nil {
log.Fatal(err)
}
defer launched.Close(context.Background())
var title string
if err := chromedp.Run(launched.Browser, chromedp.Navigate("https://example.com"), chromedp.Title(&title)); err != nil {
log.Fatal(err)
}
log.Println("title:", title)An attached profile's cookies are seeded into the live browser automatically;
localStorage is not (Playwright restores it for free on the TS/Python path,
chromedp has no equivalent) — seed it yourself against launched.Browser if you
need it. Retries controls the whole create+connect+probe sequence, not
individual HTTP requests (that's MaxAttempts on ClientOptions above) — on a
transient failure the dead session is released and a fresh one created. Unlike
the TS/Rust handles, there is no implicit release on GC — Close (or a defer)
is required for a prompt release, or the session lingers until its plan-tier
expiry.
session, err := client.Sessions.Create(ctx, solari.CreateSessionOptions{
ProfileID: "prof_1", // attach a stored profile
Recording: true, // record the session for replay
Stealth: true, // runtime stealth shim
Captcha: true, // managed captcha solving (requires Stealth)
Proxy: solari.ProxySmart,
})
view, err := client.Sessions.Get(ctx, session.ID) // current view
err = client.Sessions.Release(ctx, session.ID) // idempotent
link, err := client.Sessions.ReplayURL(ctx, session.ID) // presigned replay link
raw, err := client.Sessions.DownloadReplay(ctx, session.ID) // replay bytes (NDJSON)The zero CreateSessionOptions is valid and requests a plain session. Release
and Profiles.Delete tolerate a 404, so they are safe to call twice.
Replays are available ~1-3s after Release, and only for sessions created with
Recording: true. DownloadReplay returns the bytes exactly as stored — gzipped
by default, per ReplayURL.ContentEncoding.
CreateSessionOptions.Proxy takes either a preset or a full request. Managed
egress requires Stealth: true.
Proxy: solari.ProxyCountry("gb") // country shorthand
Proxy: solari.ProxySmart // gateway picks + escalates per host
Proxy: solari.ProxyOff // no managed egress
Proxy: solari.ProxyRequest{
Country: "us",
Tier: solari.TierMobile, // or TierResidential (default) / TierStatic
ASN: "21928", // pin the carrier (T-Mobile)
Session: "warm-1", // sticky egress IP
SessionDuration: 10, // minutes (1-30)
State: "california", // US-only
City: "los_angeles", // US-only
}The resolved egress comes back on Session.Proxy (*ResolvedProxyConfig), and
is nil when no managed proxy was requested. To check what a gateway supports
before asking:
countries, err := client.Proxy.Countries(ctx)
// countries.Enabled, countries.Countries → ["us", "gb", …]A profile is a persisted cookie jar + localStorage — the same shape as
Playwright's storageState.
prof, err := client.Profiles.Create(ctx, "shopper")
profiles, err := client.Profiles.List(ctx)
res, err := client.Profiles.Save(ctx, prof.ID, state) // → res.Version, res.SizeBytes
err = client.Profiles.Delete(ctx, prof.ID) // idempotentAttaching a profile populates the session's storage state. Because Go cannot
express TypeScript's undefined vs null, the tri-state is carried by two
fields:
StorageStateAttached |
StorageState |
Meaning |
|---|---|---|
false |
nil |
No profile was attached. |
true |
nil |
The profile exists but is empty. |
true |
non-nil | The profile's contents. |
StorageState preserves top-level keys it does not model (in Extra), so a
Sessions.Create → Profiles.Save round trip never silently drops fields.
session, err := client.Sessions.Create(ctx, solari.CreateSessionOptions{Stealth: true})
if err != nil {
var serr *solari.SolariError
if errors.As(err, &serr) {
log.Printf("status=%d code=%s: %s", serr.Status, serr.Code, serr.Message)
}
// or, more directly:
if solari.IsCode(err, solari.CodeConcurrencyLimitExceeded) {
// at the org's live-session cap — back off and retry later
}
}Known codes: CodeFeatureRequiresPlan, CodeConcurrencyLimitExceeded,
CodePlanLimitExceeded, CodeBrowserUnhealthy. The gateway may add more, so
compare rather than switching exhaustively.
| Option | Default | Notes |
|---|---|---|
APIKey |
— | Required. Format slr_live_<id>_<secret>. |
Region |
RegionUSWest |
More regions coming soon. Ignored when BaseURL is set. |
BaseURL |
region URL | Override for staging / self-hosted gateways. |
HTTPClient |
— | Overrides the default client; TimeoutMs is then ignored. |
MaxAttempts |
2 | Tries per request, counting the first. |
BackoffMs |
500 | Fixed delay between attempts. Pointer, so 0 is distinguishable from unset. |
TimeoutMs |
90000 | Per-attempt bound. |
Client is safe for concurrent use.
The suite is fully offline — every case runs against httptest servers and
asserts request shapes and response parsing. No API key, gateway, or browser
needed.
go build ./... && go test ./...