kuiper Docs

Agent services

Browsers

Give your agents real Chrome browsers through a Steel-compatible API. Connect with Playwright or Puppeteer, hold a session for later, keep logins in profiles, replay recordings, or run sessions on your own Steel, Browserbase, Kernel, Hyperbrowser, Anchor or Browser Use account.

On this page

Browsers for your agents#

Kuiper runs real Chrome browsers for your agents. Each session gets a browser of its own, isolated from every other, and the browser is destroyed when the session ends. Nothing carries over to the next session (cookies, storage, downloads, cache) unless you ask for it:

  • Hibernating keeps one session's state for later.
  • Profiles keep a browser's logins and settings between sessions.
  • Recordings let you replay what the browser showed.

The API is Steel's. Steel's SDKs work with their base URL changed, and Playwright or Puppeteer connect over CDP to the URL a session answers with. The browsers run on Kuiper's own machines, so you need no Steel account and no Steel key. If you already have a hosted browser provider, sessions can run there with your own key through the same API.

The API lives at https://gateway.kuiper.sh/browsers and takes a gateway key (kgw_…).

From a unit on Kuiper#

Point your variables at the built-in browsers resource. Each running copy of the unit gets its own key and its region's gateway, with nothing to create first:

Terminal
kuiper env set -p proxima -u agent \
  'STEEL_BASE_URL=${{browsers.BASE_URL}}' \
  'STEEL_CONNECT_URL=${{browsers.CONNECT_URL}}' \
  'STEEL_API_KEY=${{browsers.API_KEY}}'
kuiper restart -p proxima -u agent

From anywhere else#

Make a key for the project and use it the same way, from your laptop, CI or another cloud:

Terminal
kuiper keys create laptop -p proxima --services browsers
export STEEL_BASE_URL=https://gateway.kuiper.sh/browsers
export STEEL_CONNECT_URL=wss://gateway.kuiper.sh/browsers/connect
export STEEL_API_KEY=kgw_…

With Steel's SDKs#

TypeScript
import Steel from "steel-sdk";
import { chromium } from "playwright";

const steel = new Steel({
  steelAPIKey: process.env.STEEL_API_KEY,
  baseURL: process.env.STEEL_BASE_URL,
});
const session = await steel.sessions.create({
  timeout: 15 * 60 * 1000,          // ms; 5 minutes when absent
  dimensions: { width: 1280, height: 800 },
});
const browser = await chromium.connectOverCDP(session.websocketUrl);
const context = browser.contexts()[0];
const page = context.pages()[0] ?? (await context.newPage());
await page.goto("https://example.com");
// …
await browser.close();
await steel.sessions.release(session.id);
python
from steel import Steel
client = Steel(steel_api_key=os.environ["STEEL_API_KEY"], base_url=os.environ["STEEL_BASE_URL"])
session = client.sessions.create()

Without an SDK it is plain HTTP: POST $STEEL_BASE_URL/v1/sessions with the key in the steel-api-key header, or as Authorization: Bearer.

Connect over CDP#

A session's websocketUrl is wss://gateway.kuiper.sh/browsers/connect?sessionId=<id>&token=<t>. The token is bound to that session and lapses with it, so you can hand the URL to a library as it is:

TypeScript
await chromium.connectOverCDP(session.websocketUrl);          // Playwright
await puppeteer.connect({ browserWSEndpoint: session.websocketUrl });

Steel's documented form works too. Use it to reconnect later from a process that only kept the session ID:

TypeScript
await chromium.connectOverCDP(`${process.env.STEEL_CONNECT_URL}?sessionId=${id}&apiKey=${process.env.STEEL_API_KEY}`);

A session belongs to the project, not to the process that made it. Any key of the project can find it (GET /v1/sessions/<id>), reconnect to it and release it, so a session outlives an agent's turn, a restart or a redeploy. Keys of other projects get a 404.

Timeouts#

  • timeout (in milliseconds) is the longest a session lives: 5 minutes by default, 24 hours at most. Asking for more is a 400 that names the limit.
  • inactivityTimeout releases the session after that long with no CDP or live-view traffic. Without it, only timeout ends a session you forget to release.
  • An ended session says why in releaseReason: user_requested, timeout, inactivity_timeout or hibernation_expired (with status: "released"), or browser_crashed or node_lost when the browser or its machine went away (with status: "failed").

Live view#

debugUrl (also sessionViewerUrl) is a live view of the browser. Anyone with the URL can watch but not touch, so you can embed it in an iframe to show a person what the agent is doing. interactiveDebugUrl also takes clicks and keys, which is how a person takes over for a login or a CAPTCHA. Both stop working when the session ends. Treat the interactive one like a password.

Hibernate a session#

A session you will need again, but not soon, can give its browser back without losing its place. An agent waiting on a person, or a job that picks up tomorrow, are typical cases.

Terminal
curl -X POST -H "steel-api-key: $STEEL_API_KEY" $STEEL_BASE_URL/v1/sessions/$ID/hibernate
curl -X POST -H "steel-api-key: $STEEL_API_KEY" $STEEL_BASE_URL/v1/sessions/$ID/resume
  • Hibernating saves the session's cookies, site storage and open tabs, then destroys its browser. The session answers status: "hibernated" with hibernatedUntil, and isn't billed while it sleeps. The state is kept encrypted, at most 10 MB, for 7 days. After that the session ends with releaseReason: "hibernation_expired".
  • Resuming gives the session a fresh browser with the same session ID, its state and its tabs. Connecting to a hibernated session resumes it too, so an agent that only kept the URL reconnects as if nothing happened. Pages load again, so whatever lived only in a page's memory, such as an unsent form, is gone.
  • timeout counts live time only. A session with a 30-minute timeout that ran 20 minutes before hibernating has 10 minutes left when it resumes. One with no time left can't be resumed (409).
  • Automatically: "kuiper": {"onInactive": "hibernate"} in the create request hibernates the session when its inactivityTimeout passes, instead of releasing it.
  • Releasing a hibernated session deletes its state at once.

GET /v1/sessions?status=hibernated lists the project's hibernated sessions.

Profiles#

A profile is a browser's user data (cookies, storage, IndexedDB, saved logins and settings), kept between sessions the way Steel does it:

TypeScript
// Log in once, and keep what the browser has when the session ends.
const first = await steel.sessions.create({ persistProfile: true });
// … log in, then release it: the profile is saved after the release.
await steel.sessions.release(first.id);
// Later sessions start from it:
const next = await steel.sessions.create({ profileId: first.profileId });
// Or start from it and save it again when this one ends:
await steel.sessions.create({ profileId: first.profileId, persistProfile: true });
  • A profile is UPLOADING until its session ends and it is saved, then READY, or FAILED with an error. A session starts only from a READY profile (409 otherwise).
  • GET /v1/profiles, GET /v1/profiles/<id> and DELETE /v1/profiles/<id> manage them.
  • Caches are left out, and a profile is at most 300 MB compressed. A bigger one isn't saved.
  • Profiles belong to the project, are encrypted at rest, and are deleted after 30 days without a session starting from them.

Proxies#

  • Your own: proxyUrl: "http://user:pass@proxy.example:8000" sends the browser's traffic through your proxy. It isn't billed.
  • Kuiper's: useProxy: true, or useProxy: {geolocation: {country: "DE"}} for an exit in one country, routes the browser through Kuiper's proxy provider. It is available once Kuiper has turned it on for your organisation; until then the request is a 400 that says so. Its traffic is billed per GiB. The provider's credentials never reach your browser.

proxyUrl and useProxy together are a 400. A hibernated session resumes with the same proxy.

Recordings#

A session can keep a record of what its browser showed, to replay later: the pages, what changed on them, and the mouse moves, clicks, scrolls and typing that changed them. Ask for it when you create the session:

Terminal
curl -X POST -H "steel-api-key: $STEEL_API_KEY" -H "content-type: application/json" \
  -d '{"kuiper": {"record": true}}' $STEEL_BASE_URL/v1/sessions
# … use the session, release or hibernate it, then:
curl -H "steel-api-key: $STEEL_API_KEY" $STEEL_BASE_URL/v1/sessions/$ID/events > session.json
  • What it is: rrweb events, as one JSON array, the way Steel Cloud answers them. rrweb's player replays them as they are: new rrwebPlayer({ target, props: { events } }). Password fields are masked. It isn't a video, and network traffic isn't in it.
  • When: a recording is saved when the session is released or hibernated. Until then …/events answers 409 (code: "recording"). A session that hibernates and resumes goes on recording, and its events come back in order.
  • Limits: at most 100 MB of events for each live period; past that the rest of the period isn't kept. Recordings are deleted 7 days after they are saved. Only headless sessions (the default) are recorded.
  • In the dashboard: the project's Browsers page marks which ended sessions have a recording, and owners and admins replay them there. kuiper browsers recording <id> > session.json saves one.
  • Recordings are encrypted at rest and cost nothing.

Your own provider#

Kuiper's browsers need no setup and no other account. If your organisation already pays for a hosted browser provider, your sessions can run there instead, with your own key. Your agents don't change: they still call Kuiper's API with a gateway key and connect to the websocketUrl it answers with. The providers are Steel, Browserbase, Kernel, Hyperbrowser, Anchor Browser and Browser Use.

Add the provider's key#

Owners and admins add it in the dashboard, under Access, then Providers, or with the CLI. --key - reads the key from standard input:

Terminal
kuiper providers add browserbase --key -
kuiper providers add steel --key - --projects proxima
kuiper providers test browserbase
  • The kinds are steel, browserbase, kernel, hyperbrowser, anchor and browser-use. The key's name defaults to its kind.
  • A Browserbase key may name a Browserbase project: --field project_id=…. Without it, sessions go to the key's own project.
  • A Browser Use key from its EU deployment needs --field region=eu.
  • test makes one cheap call with the key (it lists your sessions) and says ok, invalid or unreachable.

The key is write-only, like every provider key. It never reaches your units or your agents.

Choose it for a project, or for one session#

A project's sessions run on Kuiper's browsers until you choose a key for it, on the project's Browsers page or with the CLI:

Terminal
kuiper browsers provider browserbase -p proxima   # this project's sessions run on Browserbase
kuiper browsers provider -p proxima               # where they run now, and the keys you can choose
kuiper browsers provider kuiper -p proxima        # back to Kuiper's own

One session can choose for itself in Kuiper's own options:

TypeScript
await steel.sessions.create({ kuiper: { provider: "steel" } });   // this one on your Steel account
await steel.sessions.create({ kuiper: { provider: "kuiper" } });  // this one on Kuiper's browsers

The project must be allowed to use the key (--projects when you add it). A key that doesn't exist or isn't shared with the project is a 400 or a 403. If you remove the key a project uses, its sessions are refused (409 provider_missing) until you choose again. They never move to Kuiper's browsers, or onto Kuiper's bill, by themselves.

What stays the same#

  • The API and the connect URL are Kuiper's. A session gets Kuiper's own ID and a websocketUrl on the gateway. Kuiper connects to the provider for you, so your provider key, and the provider's own connect URL, never reach your agent.
  • Any key of the project can find, connect to and release the session, from any gateway, as with Kuiper's browsers.
  • timeout is Kuiper's. Kuiper ends the session at the provider when it passes. Disconnecting doesn't end it, as with Steel: Kuiper keeps Browserbase sessions alive (keepAlive), reconnects to Hyperbrowser with keepAlive=true, and sets Kernel's and Anchor's idle timeouts so they don't end a session early.
  • Releasing (POST /v1/sessions/<id>/release, the dashboard, kuiper browsers release) releases it at the provider.
  • The answer says where the session runs: "kuiper": {"provider": "browserbase", "providerKind": "browserbase", "providerSessionId": "…", "billedBy": "provider"}.

What differs#

Each provider has its own settings. Kuiper maps Steel's options onto the provider's. An option the provider has no setting for is a 400 that names it, for example userAgent isn't available with Browserbase keys. Kuiper never drops an option silently.

OptionSteelBrowserbaseKernelHyperbrowserAnchorBrowser Use
timeoutyesup to 6 hoursup to 72 hoursup to 12 hoursyesup to 4 hours
inactivityTimeoutyesnonononono
userAgentyesnonononono
dimensionsyesyesyesyesyes320 to 6144 by 320 to 3456
blockAdsyesyesnoyesyesno
headlessyesnoyesnoyesno
regionSteel'sBrowserbase'sKernel'sHyperbrowser'snono (the key's region)
solveCaptchayesyesnoyesyesyes
useProxyyesyesnoyesyesyes
proxyUrlyeshttp and httpsnonot socks4not socks4http only
extensionIdsyesoneyesyesyesup to 3
deviceConfig, sessionContext, optimizeBandwidth and Steel's other optionsyesnonononono
  • region names the provider's own regions (us-east-1 for Browserbase, lax for Steel). The gateway's own region asks for nothing.
  • useProxy is the provider's own proxy, on your provider bill, and needs no setting on Kuiper's side.
  • Defaults are the provider's. An option you leave out keeps the provider's default. For example, Browserbase and Browser Use solve CAPTCHAs, and Browser Use sends traffic through its US proxy, unless you send solveCaptcha: false or useProxy: false.
  • Plans. Some providers keep an option for paid plans: Browserbase's keepAlive needs its Hobby plan or above, for example. When a provider refuses, Kuiper answers with the provider's own words: a 400 provider_refused, a 402, a 429 provider_rate_limited, or a 502 provider_key_refused when the provider refused the key.
  • Hyperbrowser stops a session once every page in it is closed, even while a client is connected.

These features are Kuiper's own, so they are a 400 on a provider: profiles (profileId, persistProfile), recordings (kuiper.record), hibernation (kuiper.onInactive: "hibernate", /hibernate, /resume), the files API and /live-details. GET /v1/sessions/<id>/context works on Steel, which has its own. Quick actions (/v1/scrape, /v1/screenshot, /v1/pdf) run on Steel when the project uses a Steel key or the request names one, and are a 400 on the other providers, which have none. Add "kuiper": {"provider": "kuiper"} to run one on Kuiper's browsers instead.

Live view#

debugUrl is the provider's own live view, when the provider gives one that holds no key of yours: a link for that one session, valid while it runs. Anyone with it can watch the browser, and with most providers take control of it, so treat it like a password. Steel's and Kernel's open read-only as debugUrl and take input as interactiveDebugUrl. Where a provider's link would carry your key, debugUrl is null.

Cost and limits#

Your provider bills these sessions; Kuiper charges nothing for them. Budgets don't count them, and neither does Kuiper's limit on concurrent sessions: your provider's own limits apply. Each key's request limit still does. The Browsers page and kuiper browsers sessions show each session's provider, and the page sums up this month's sessions on each provider.

Every endpoint#

Endpoint
POST /v1/sessionsSteel's options: sessionId, timeout, inactivityTimeout, userAgent, dimensions, deviceConfig, blockAds, optimizeBandwidth, sessionContext, proxyUrl, useProxy, profileId, persistProfile, region, namespace and headless. Kuiper adds kuiper.onInactive, kuiper.record and kuiper.provider.
GET /v1/sessions?status=live|released|hibernatedThe project's sessions. Released ones page with cursor.
GET /v1/sessions/<id>, /context, /live-detailsAs in Steel.
POST /v1/sessions/<id>/release, POST /v1/sessions/releaseRelease one session, or all of the project's live ones.
POST /v1/sessions/<id>/hibernate, /resumeHibernate a session.
GET /v1/profiles, GET or DELETE /v1/profiles/<id>Profiles.
GET /v1/sessions/<id>/eventsA recorded session's events (Recordings).
/v1/sessions/<id>/files…Steel's files API, up to 100 MB a file.
POST /v1/scrape, /v1/screenshot, /v1/pdfOne action on a browser of its own, released straight after.

region must be the gateway's own region. To use another region, use its gateway, gateway.<region>.kuiper.sh. On your own provider, region names the provider's regions instead.

Not available yet#

Asking for one of these is a 400 whose message names the option, for example solveCaptcha is not available on this Kuiper. A client that retries without the named option gets a session.

  • solveCaptcha, credentials, extensions and isSelenium.
  • /v1/credentials… and …/captchas… answer 501 not_supported.

Cost and limits#

Sessions are billed by the second while a browser is yours, from the moment it is ready until it is released or hibernated, at the price per session-hour. Quick actions (/v1/scrape, /v1/screenshot, /v1/pdf) are billed per 1,000, and Kuiper's proxy per GiB. Hibernated sessions, saved profiles and recordings cost nothing. Everything shows on the Usage page with the rest of your spend, and each session's cost on the project's Browsers page (kuiper browsers sessions). See Usage and billing.

  • Budgets cap what the organisation, a project or a key spends on browsers in a month. A used-up block budget refuses new sessions with 402 budget_exceeded. See Budgets and limits.
  • Concurrent sessions: an organisation holds at most 10 live sessions at once. Past that, creating or resuming one is a 429 too_many_sessions with Retry-After. Hibernated sessions don't count.
  • Requests: each key may make 300 requests a minute to the browser API. Past that, 429 rate_limited with Retry-After. CDP and live-view traffic aren't counted.

Every refusal has Steel's shape, {"error": …, "message": …}, plus a code, so a client can tell a budget from a limit.

Terminal
kuiper browsers sessions -p proxima --live
kuiper browsers release <id> -p proxima

Sessions on your own provider are billed by the provider and count against none of these but the request limit.