# The Browser Farm

> Hosted Chromium for agents. Browserbase-compatible REST at https://api.thebrowserfarm.com. Reading these docs does not require a sign-in.

## Docs

- [REST API](https://www.thebrowserfarm.com/docs): the reference below plus curl / Node / Python quickstarts and the device flow
- [OpenAPI](https://api.thebrowserfarm.com/openapi.json): the routes this API actually serves
- [MCP](https://api.thebrowserfarm.com/llms.txt): tool-using agents (`open_browser`, `navigate`, …). Separate from REST
- [Quickstart](https://www.thebrowserfarm.com/quickstart): the Node snippet, with your key filled in after sign-up

## REST

### 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.
