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.
Feature coverage across all five languages is on the SDK feature matrix.
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,
SessionTimeoutMs: 600000, // end earlier than the plan maximum, in ms (at least 60000)
KeepAlive: true, // hold the session if the client drops, until its deadline or Release
UserMetadata: map[string]interface{}{"job": "nightly-42"}, // stored with the session
})
view, err := client.Sessions.Get(ctx, session.ID) // current view
page, err := client.Sessions.List(ctx, solari.ListSessionsOptions{
Limit: 50,
Metadata: map[string]string{"job": "nightly-42"}, // optional: match userMetadata
}) // page.Sessions, page.NextCursor
err = client.Sessions.Release(ctx, session.ID) // idempotent; waits for the ack
err = client.Sessions.ReleaseAndWait(ctx, session.ID) // same call, named like TS releaseAndWait / Python release_and_wait
link, err := client.Sessions.ReplayURL(ctx, session.ID) // presigned replay link
raw, err := client.Sessions.DownloadReplay(ctx, session.ID) // replay bytes (NDJSON)All durations are milliseconds. SessionTimeoutMs is rounded up to whole seconds
for the gateway (1001 ms is 2 s) and the gateway refuses anything under 60 s; a
negative or too-large value is an error and zero leaves the plan maximum. Launch's probe bound is
LaunchOptions.ProbeTimeoutMs.
Deprecated: CreateSessionOptions.TimeoutSeconds and LaunchOptions.ProbeTimeout
still work; use SessionTimeoutMs and ProbeTimeoutMs. When both are set the new
one wins. They will not be removed before the next major release.
List never returns the websocket endpoints (they are bearer capabilities): call
Get for a single session's endpoints. Pass page.NextCursor as
ListSessionsOptions.Cursor for the next page; it is empty on the last one.
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) // idempotentProfiles.Edit opens the profile editor on an existing profile and returns an
*Editor. Send the person to ed.URL; it is valid for about 30 minutes
(ed.ExpiresAt). There is no auto-save and no save without closing.
ed, err := client.Profiles.Edit(ctx, prof.ID, "refresh login") // reason is optional ("")
fmt.Println(ed.URL, ed.ExpiresAt)
st, err := ed.Status(ctx) // st.EditorStatus: idle|launching|editing|saving|error
version, err := ed.SaveAndClose(ctx) // store the state and close the editor
err = ed.Exit(ctx) // close without saving
version, err = ed.WaitForSave(ctx, 10*time.Minute) // poll until someone saves from the pageAn expired or used link fails with a *SolariError of status 404, an editor
that is not running with 409, and a failed save with 502 (the editor goes
to error: Exit and start a new Edit; a second save gets 409). Save and exit are never retried automatically. WaitForSave fails with status 408 on timeout or link expiry.
Attaching 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 ./...