# browserfarm

> Isolated cloud browsers (real Chromium). Drive one over the REST API or over MCP.

## REST API

### Auth

Every `/v1` request needs `X-BB-API-Key: <key>` or `Authorization: Bearer <key>`. If both are sent, `X-BB-API-Key` wins, including when that header is empty. MCP accepts the same two headers. The key is shown once at creation and starts with `bf_live_`; only its hash is stored. A missing or unknown key is `401`. A revoked key is `403`.

### Create a session

`POST /v1/sessions` returns `201` and one session object. An empty body is valid. Fields that do something:

- `projectId`: echoed back. Missing or empty becomes `"default"`. There is no project store.
- `userMetadata`: an object, echoed back. It is not searchable.
- `browserSettings.viewport`: `{ "width": 1280, "height": 720 }` with positive numbers. Anything else is ignored.
- `browserSettings.fingerprint.devices`: include `"mobile"` for a mobile viewport hint. Otherwise desktop.
- `keepAlive`: default `false`. See Timeouts, keepAlive and idle reaping.
- `isolation`: `"context"` (the default) or `"vm"`. Anything else is `400`. `context` is a browser context in a shared Chromium. `vm` is a Firecracker microVM of its own, on the dev tier only; that pool is small, so when it is full create returns `503` with `Retry-After`. The production API currently serves `context` only: `vm` is accepted there and runs as a `context` session.

Ignored, not errors: `proxies`, `region`, `timeout`, `api_timeout`, `browserSettings.blockAds`, `solveCaptchas`, `recordSession`, `contextId`, `extensionId`.

The session object has `id` (UUID), `createdAt`, `updatedAt`, `startedAt`, `endedAt` (null until it ends), `projectId`, `status` (`RUNNING` while the browser is yours, then `COMPLETED`, `TIMED_OUT`, or `ERROR`), `keepAlive`, `region`, `connectUrl`, `userMetadata`, `seleniumRemoteUrl: null`, and `signingKey`. There is no `expiresAt` and no `proxyBytes`.

### Connect over CDP

`connectUrl` is `wss://<api host>/<id>/cdp?signingKey=<bfsk_… token>`. Pass it through unchanged to Playwright `connectOverCDP` or Puppeteer `connect`. The path id is not a credential. The handshake requires that `signingKey`, or the owning customer's API key in the `X-BB-API-Key` header (not as a query parameter). A presented `signingKey` that does not match does not fall through to the API key. A live session with no valid credential is `401` (plain text).

`signingKey` is a per-session `bfsk_…` token, the same value as the `signingKey` query on `connectUrl`, valid for 24 hours from issue; `GET` the session for a fresh `connectUrl`. To revoke a leaked `connectUrl`, release the session: the key is stateless until it expires. It is not a Selenium secret. A released or unknown id is `404` even with a valid key. A non-WebSocket request to that path returns `426`.

### List and get

`GET /v1/sessions` returns a JSON array of this account's sessions (not wrapped in an object). `?status=` filters to PENDING, RUNNING, COMPLETED, ERROR, TIMED_OUT; omitting it returns every status, and an unknown value is `400`. `?q=` is ignored. `GET /v1/sessions/<id>` returns one session, or `404` `session not found` (including a malformed id).

This list is the gateway process's memory, not a durable archive. On the public API a restart drops it, including running sessions. A terminal session may still include `connectUrl`; reconnecting returns `404` `unknown or released session` (plain text, not JSON).

### Release

`POST /v1/sessions/<id>` with `{"status":"REQUEST_RELEASE"}` ends the session. The response is `200` and the session object with status `COMPLETED`. Live CDP sockets are closed. Releasing again is `200` and does not double-destroy. Any other status is `400`.

### Errors

Every error body is one JSON object with a `message` string. There is no `error` or `code` field on `/v1`.

- `401` No API key: `missing API key: send X-BB-API-Key or Authorization: Bearer <key>`
- `401` Key not recognized: `unknown API key`
- `403` Key revoked: `API key has been revoked`
- `400` Body is not JSON: `invalid JSON body`
- `400` Update status is not REQUEST_RELEASE: `unsupported update: only { "status": "REQUEST_RELEASE" } is implemented in Phase 0a`
- `400` ?status= is not PENDING, RUNNING, COMPLETED, ERROR, or TIMED_OUT: `invalid status "NOPE": expected one of PENDING, RUNNING, COMPLETED, ERROR, TIMED_OUT`
- `400` isolation is present and is not context or vm: `invalid isolation "process": expected one of context, vm`
- `404` No such session for this account, including a malformed id (someone else's session looks the same): `session not found`
- `404` A /v1 route we do not implement (logs, /debug, contexts, projects, …): `not implemented in Phase 0a`
- `429` Account is at its concurrent-session cap. Header Retry-After: 5: `concurrency limit reached: N of M sessions running. Release a session (or wait for one to idle out) and retry.`
- `503` No browser capacity, including a full vm pool on the dev tier. Header Retry-After (seconds; 5 when the host does not name one): `no browser capacity available right now; retry shortly`
- `502` The browser could not be allocated for any other reason: `failed to allocate a browser session`
- `426` Plain HTTP GET on /<id>/cdp. That path is a WebSocket: `upgrade required: this endpoint speaks WebSocket`
- `500` Unexpected failure: `internal error`

### Concurrency

The only enforced limit is how many sessions an account may have running at once. The default is 10 (`concurrentCap` on the customer; all of that account's keys share it). At the cap, create returns `429` and `Retry-After: 5`. There is no requests-per-second or other rate limit. `503` is different: the farm has no capacity to place a browser, and it also sends `Retry-After`.

### Timeouts, keepAlive and idle reaping

Closing CDP ends the session unless it was created with `keepAlive: true`. When the last CDP connection closes (Playwright or Puppeteer `browser.close()`, or a dropped or failed connection) the status becomes `COMPLETED` and the next connect is `404`. With `keepAlive: true` the session stays `RUNNING` after a disconnect and the same `connectUrl` accepts another client until you release it or it idles out. A CDP `Browser.close` command, which is what Puppeteer's `browser.close()` sends, ends the session either way. There is no maximum lifetime; `timeout` on create is ignored. A client that keeps sending CDP frames, including a heartbeat, holds the session indefinitely.

- 10 minutes without a CDP frame (`CONTEXT_IDLE_TIMEOUT_MS`, default 600000) releases the session as `TIMED_OUT`. The reaper sweeps every 30 seconds, so the cut lands on the next sweep after those 10 minutes.
- 15 minutes (`BROWSER_IDLE_TIMEOUT_MS`) is how long a shared Chromium stays up after its last session ends, in the in-process and sidecar setups. It is not a session lifetime. Hosted placement does that browser-level idle in the host daemon, not as a second timer on your session.
- The public API does not restore sessions after a restart. Where a gateway does restore a running placement, one nobody reconnects to is reaped after a 60 second grace, not the full 10 minutes.

### Region

`region` in the response is always the string `us-west-2`. Nothing in the API chooses a region. A `region` on the create body is ignored. Treat the field as informational so typed clients do not choke on it.

### Health

`GET /healthz` and `GET /api/health` need no API key. Both return HTTP 200, including when the browser backend is down. The body's `status` is `"ok"` or `"degraded"`, `service` is `"gateway"`, and `sessions` counts live and total sessions. `environment` is the process `NODE_ENV`, not which site you called.

### Not implemented

These are not live. Calling the REST paths returns `404` with `{"message":"not implemented in Phase 0a"}`.

- Session logs (`GET /v1/sessions/<id>/logs`).
- Live view (`GET /v1/sessions/<id>/debug`) and replay / recording.
- Contexts, projects, extensions, uploads, and downloads.
- Residential egress. Proxies on create are ignored.

- REST guide: https://www.thebrowserfarm.com/docs
- OpenAPI: /openapi.json

## MCP

- [MCP endpoint](https://api.thebrowserfarm.com/mcp): Streamable HTTP (stateless, JSON responses), `POST` JSON-RPC
- Auth: `Authorization: Bearer <api key>` (or `X-BB-API-Key: <api key>`)

## Tools

- open_browser(label?) — Open a NEW, isolated cloud browser (a real Chromium) that you control.
- navigate(browser_id, url) — Load an absolute http(s) URL in the given browser and wait for it to load.
- snapshot(browser_id) — Get a fresh compact accessibility snapshot of the current page: headings, text and every interactive element with a [ref=eN] handle.
- click(browser_id, ref, element?) — Click the element with the given ref from the LATEST snapshot (links, buttons, tabs, checkboxes…).
- type(browser_id, ref, text, submit?) — Type text into an input/textarea/searchbox identified by a ref from the latest snapshot (replaces existing text).
- extract(browser_id, query?) — Read the current page's main readable text (main/article content, else the whole body), truncated to ~8k chars.
- close_browser(browser_id) — Close the browser and free its slot.
- list_browsers() — List open browsers for this API key, with browser_id, label and current URL.

## Loop

open_browser → navigate → snapshot → click/type (by ref) → extract → close_browser. Pass the browser_id from open_browser on every call.

## Rules

- Always call close_browser when finished (or giving up). Unclosed browsers hold a slot.
- One browser per website/worker. Open several in parallel for parallel work.
- Never close a browser you did not open (list_browsers shows every browser on the key).
- Private network is blocked: localhost, 10/8, 172.16/12, 192.168/16, 169.254/16, ::1 etc.,
  on navigate AND on every redirect, link, form post, subresource, fetch/XHR and websocket.
- Idle browsers are reaped after 10 minutes without activity.
- Each API key has a concurrency cap. Over it, open_browser returns
  "concurrency limit reached: N of M sessions running" — close browsers, then retry.
- Refs (e12) go stale when the page changes: call snapshot again and use fresh refs.

## Humans

- A live wall shows every open browser's latest screenshot and action, so people can watch the bots work.
