Documentation

Cookies & sessions

The headline question this guide answers is: how do I make the next session look like the same user as the last one? Two pieces of state control that — the cookies the browser is carrying, and the fingerprint the browser is wearing — and both are designed to survive across rentals.

TL;DR
  • Cookies live in the browser context. GetCookies / SetCookies / ClearCookies are the entire surface.
  • The cookie identity is the (name, domain, path) tuple. SetCookies upserts on that key.
  • Every session is rented with a fingerprint id — either one you pin via WithFingerprint(...) / withFingerprint(...), or one the server picks based on your country code. Read it back with browser.Fingerprint() (Go) / browser.getFingerprint() (TS).
  • To impersonate the same user across runs: store the fingerprint id + cookies on first rental, replay both on the next.
  • ConnectSession (Go) / createBrowserClient (TS) reattach to an existing rental from another process. They do not extend the rental lifetime.

The cookie surface

There are exactly three calls, and they all operate on the whole browser context (across every tab and frame). No flags, no filters — read everything, write everything, clear everything.

// Read all cookies.
cookies, _ := browser.GetCookies(ctx)
for _, c := range cookies {
  fmt.Println(c.Name, "=", c.Value, "@", c.Domain, c.Path)
}

// Write or upsert cookies.
_ = browser.SetCookies(ctx, []wrc.CookieParam{
  wrc.Cookie("session", "abc123", "example.com", "/"),
  wrc.Cookie("locale",  "en-US",  "example.com", "/"),
})

// Wipe all cookies (no per-cookie deletion).
_ = browser.ClearCookies(ctx)

A CookieParam is four fields, all required: name, value, domain, path. The server uses the (name, domain, path) tuple as the cookie's identity — calling SetCookies with the same tuple overwrites the existing cookie rather than adding a duplicate.

Persisting cookies between runs

The simplest "make me look logged in next time" recipe: do the login once, dump the cookie jar to disk, restore it on the next rental.

// First run: log in, then persist.
_, _ = browser.Navigate(ctx, "https://example.com/login", 0)
_, _ = browser.Fill(ctx, wrc.CSS("#email"), "alice@example.com")
_, _ = browser.Fill(ctx, wrc.CSS("#password"), "hunter2")
_, _ = browser.Click(ctx, wrc.CSS("button[type=submit]"))

cookies, _ := browser.GetCookies(ctx)
buf, _ := json.Marshal(cookies)
_ = os.WriteFile("cookies.json", buf, 0o600)

// Next run (same script or a different process): restore.
buf, _ := os.ReadFile("cookies.json")
var cookies []wrc.CookieParam
_ = json.Unmarshal(buf, &cookies)
_ = browser.SetCookies(ctx, cookies)
_, _ = browser.Navigate(ctx, "https://example.com/dashboard", 0)

Two things this doesn't solve on its own:

  • The new session ships with a different fingerprint — same cookies on a brand-new browser identity is the classic "weird session" signal that triggers re-auth on cautious sites.
  • The new session is on a different exit IP — a session cookie followed by an IP swap can also force a re-login.

Both are fixable. The fingerprint side is the rest of this guide; the IP side comes down to reusing the same proxy (or pinning a country code if you're using managed proxies — see BrowserConfig / TS).

Session lifecycle in one paragraph

Quick refresher (covered in depth in Core concepts): RentBrowser rents a session for the duration in your config and hands you a CloudBrowser handle. The session lives until the rental timer runs out or you call StopBrowser / Close. From a different process, ConnectSession (Go) / createBrowserClient (TS) attaches to an existing session by its id and gRPC URL — useful when you persisted those across a restart. Closing an attached handle closes only the connection, the underlying rental keeps running until the rent timer expires or someone calls the standalone wrc.StopBrowser(apiKey, sessionId).

That gives you the second persistence axis: you can pause your code, restart your process, and reattach to the same browser without ever losing cookies, navigation state, or fingerprint.

Fingerprint persistency

This is where WRC differs from spinning up a fresh headless Chrome every run. Each rental is provisioned with a fingerprint — a server-side bundle of UA string, navigator.* quirks, screen size, canvas/audio/WebGL responses, etc. — and that fingerprint has a stable id you can hold on to and replay.

Default: server picks per country

If you don't pass WithFingerprint / withFingerprint, the server picks one for you based on the rental's country code. Different rentals → different fingerprints, every run.

Pinning: read it, store it, send it back

After every rental you can read which fingerprint the session is wearing:

browser, _ := wrc.RentBrowser(ctx, cfg)
fmt.Println("fingerprint id:", browser.Fingerprint())

Hold on to that string and feed it back into the next rental to get the exact same browser identity again:

cfg := wrc.NewBrowserConfig(apiKey, 600, "", 0, "", "").
  WithCountryCode("DE").
  WithFingerprint(savedFingerprint)
browser, _ := wrc.RentBrowser(ctx, cfg)

What "same identity" gets you in practice:

  • UA string stays identical (Chrome version, OS, build).
  • navigator.* properties match (platform, hardwareConcurrency, deviceMemory, plugins, etc.).
  • Canvas/audio/WebGL fingerprints reproduce — the deterministic pixel data and audio buffer the site can hash for tracking.
  • Screen + viewport defaults stay the same.
  • Locale + timezone carry across when you keep the same WithCountryCode / WithTimezone.

To the target site, that's a returning user, not a new browser install on the same account.

Stable identity across runs — putting it together

Cookies + fingerprint + matching geography is the full "this is the same user" combo. The first run captures everything; later runs restore it:

// ── First run ──
cfg := wrc.NewBrowserConfig(apiKey, 600, "", 0, "", "").
  WithCountryCode("DE")
browser, _ := wrc.RentBrowser(ctx, cfg)

_, _ = browser.Navigate(ctx, "https://example.com/login", 0)
// … perform login …

cookies, _ := browser.GetCookies(ctx)
identity := struct {
  Fingerprint string             `json:"fingerprint"`
  Country     string             `json:"country"`
  Cookies     []wrc.CookieParam  `json:"cookies"`
}{
  Fingerprint: browser.Fingerprint(),
  Country:     browser.CountryCode(),
  Cookies:     cookies,
}
buf, _ := json.Marshal(identity)
_ = os.WriteFile("identity.json", buf, 0o600)
_ = browser.Close()

// ── Later run (same or different process) ──
buf, _ := os.ReadFile("identity.json")
var saved struct {
  Fingerprint string
  Country     string
  Cookies     []wrc.CookieParam
}
_ = json.Unmarshal(buf, &saved)

cfg = wrc.NewBrowserConfig(apiKey, 600, "", 0, "", "").
  WithCountryCode(saved.Country).
  WithFingerprint(saved.Fingerprint)
browser, _ = wrc.RentBrowser(ctx, cfg)
_ = browser.SetCookies(ctx, saved.Cookies)
_, _ = browser.Navigate(ctx, "https://example.com/dashboard", 0)

If you're routing through your own proxy, persist (proxyHost, proxyPort, proxyUsername, proxyPassword) the same way — a stable exit-IP region on top of stable cookies and a stable fingerprint is the cleanest "returning user" signal you can send.

Reattaching to a live session

For long-running flows that outlive your process — overnight crawls, human-in-the-loop pauses, reconnecting after a script crash — store the sessionId and grpcUrl and reattach later:

// First process: rent, persist the address.
browser, _ := wrc.RentBrowser(ctx, cfg)
_ = os.WriteFile("session.json", []byte(fmt.Sprintf(
  `{"sessionId":%q,"grpcUrl":%q}`,
  browser.SessionId(), browser.GrpcUrl(),
)), 0o600)
// Do NOT call Close — the rental should keep running.

// Later process: reattach to the same session.
buf, _ := os.ReadFile("session.json")
var s struct{ SessionId, GrpcUrl string }
_ = json.Unmarshal(buf, &s)
browser, _ = wrc.ConnectSession(ctx, s.GrpcUrl, apiKey, s.SessionId)
// browser drives the same context as before — same cookies, same fingerprint,
// same open page.

A reattached handle is a thin wrapper: closing it only closes the transport, not the rental. To actually release the rental from a process that only has the id, use the standalone wrc.StopBrowser(ctx, apiKey, sessionId) (Go) / stopBrowser(apiKey, sessionId) (TS).

Gotchas

  • Cookies are upserted, not appended. SetCookies with an existing (name, domain, path) tuple replaces the value — handy for refreshing a session cookie, surprising if you expected a duplicate entry. There is no per-cookie delete; clear the whole jar with ClearCookies if you need to start over.
  • CookieParam is intentionally minimal. It carries just name / value / domain / path. Server-side, cookies are written with reasonable defaults — there are no SDK-level knobs for expires, Secure, HttpOnly, or SameSite today.
  • Fingerprint ids are server-side handles. They're meaningful as long as the server still has that fingerprint in its catalog. If you persist an id for months and later get a "fingerprint not found" error on rental, fall back to letting the server pick a new one and persist the new id.
  • Don't mix and match identity signals. Replaying cookies and a US fingerprint while exiting through a DE IP is a louder signal than running with all-defaults. If you persist one of the three (fingerprint / cookies / IP region), persist all three.
  • Reattaching is not the same as resuming a stopped session. ConnectSession / createBrowserClient assumes the rental is still alive on the server. Once the rent duration expires or someone explicitly calls StopBrowser, the context is gone — its cookies, fingerprint and open pages disappear with it.
  • Closing an attached handle doesn't release the rental. That's intentional — multiple processes can attach to the same session. Use the standalone StopBrowser(ctx, apiKey, sessionId) when you do want to release from outside the renting process.
See also