# API reference

> Routes, fields and errors of the browsers.openwaf.dev data plane, the management API under dash.openwaf.dev, the CLI and the MCP tools.

OpenWAF Browsers has two APIs:

- The **data plane** at `https://browsers.openwaf.dev`, which your agent calls with a browser key (`bdk_…`) to start, list and stop browsers and to manage profiles.
- The **management API** under `https://dash.openwaf.dev/v1/orgs/{org}/browsers`, which you call with an OpenWAF API token (`owf_…`) or the dashboard session to manage keys, settings, usage, sessions and the egress log. The [CLI](#cli) and the [MCP tools](#mcp-tools) use it.

## Data plane

Base URL `https://browsers.openwaf.dev`. Every route needs a browser key, in either header:

```http
X-API-Key: bdk_…
Authorization: Bearer bdk_…
```

JSON in and out. A key sees only the browsers it started.

### Start a browser

`POST /v1/browsers`. Every field is optional.

| Field | Type | Meaning |
|---|---|---|
| `profile` | string | Id or name of a [profile](#profiles) to load. One browser uses a profile at a time. |
| `metadata` | object | Up to 10 `key: value` string pairs, to find the browser later (`GET /v1/browsers?metadata=k=v`). |
| `keepAlive` | bool | Do not stop on the idle timeout: the browser runs until you stop it or it reaches the maximum session length. |
| `timeout` | integer | Idle timeout in seconds: the browser stops after this long with no CDP or live view client. Default 120. Takes precedence over `keepAlive`. |
| `recording` | bool | Record the session as MP4. The file appears in [files](#files) after the browser stops. |
| `viewport` | object | `{"width": 1280, "height": 800}` |
| `lang` | string | Browser language, for example `en-US` or `es-ES`. |
| `webBotAuth` | bool | Sign requests with [Web Bot Auth](https://developers.openwaf.dev/browsers/web-bot-auth/). Default: the organization's setting, `true` unless an admin turned it off. |

The answer is the browser:

```json
{"id": "bs_…", "status": "running",
 "cdpUrl": "wss://browsers.openwaf.dev/cdp/…",
 "cdpHttpUrl": "https://browsers.openwaf.dev/…",
 "liveUrl": "https://browsers.openwaf.dev/…",
 "urlsExpireAt": "2026-10-29T11:00:00Z", …}
```

- `cdpUrl` is the browser-level DevTools WebSocket: `chromium.connectOverCDP(cdpUrl)` in Playwright, `puppeteer.connect({browserWSEndpoint: cdpUrl})` in Puppeteer, `npx @playwright/mcp@latest --cdp-endpoint <cdpUrl>` for MCP.
- `cdpHttpUrl` serves the DevTools HTTP endpoints (`/json/version`, `/json/list`, …) with their WebSocket URLs pointing back through `browsers.openwaf.dev`.
- `liveUrl` opens the screen in a web browser, to watch or take over.
- The tokens in these URLs expire at `urlsExpireAt`, 1 hour after they are issued. `GET /v1/browsers/{id}` returns fresh ones while the browser runs.

### List and read browsers

- `GET /v1/browsers` lists the key's browsers. Filters: `status=running`, `metadata=k=v` (repeatable).
- `GET /v1/browsers/{id}` returns one browser, with fresh URLs while it runs.

### Stop a browser

```http
PATCH /v1/browsers/{id}
Content-Type: application/json

{"action": "stop"}
```

Stopping finalizes the recording and saves and unlocks the profile. A browser also stops on its own:

- after its idle timeout (120 s by default) with no CDP or live view client;
- when it reaches the maximum session length (60 minutes in the beta);
- when Chrome exits;
- when its key is rotated or revoked, or the organization runs out of quota.

### Files

`GET /v1/browsers/{id}/files` lists the downloads and the recording of a browser, each with a signed link that expires. Session files are kept for 7 days.

### Egress log

`GET /v1/browsers/{id}/egress` returns `{"items": […]}` with the latest requests the browser made through the egress proxy: time, method, host, port, `decision` (`allow`, `deny` or `error`), the reason of a denial, status and bytes. The organization-wide log is in the [management API](#egress).

### Profiles

A profile keeps cookies, logins and local storage between browsers.

| Route | What it does |
|---|---|
| `POST /v1/profiles` `{"name": "…"}` | Create a profile |
| `GET /v1/profiles` | List profiles |
| `GET /v1/profiles/{id or name}` | Read one profile |
| `PATCH /v1/profiles/{id or name}` | Change it |
| `DELETE /v1/profiles/{id or name}` | Delete it; `409` while a browser uses it |

### Data plane errors

Errors are JSON: `{"error": "…", "code": "…"}`.

| Status | `code` | When |
|---|---|---|
| 400 | | A field is invalid; `error` says which |
| 401 | | Missing, unknown, rotated or revoked key, or the key is disabled (organization over quota or not enabled) |
| 404 | | No such browser or profile for this key |
| 409 | | The profile is in use by another browser |
| 429 | `capacity_busy` | Every browser slot of the beta is busy. Wait for `Retry-After` (30 seconds) and retry. |
| 429 | `key_concurrency` | This key already runs its maximum of browsers (2). Stop one first. |
| 503 | `unavailable` | The service is paused for maintenance. Retry later. |

A `capacity_busy` answer is shared capacity, not your quota: retrying after `Retry-After` is the right thing to do. A `key_concurrency` answer will not clear by waiting unless one of your browsers stops.

## Management API

All routes are under `https://dash.openwaf.dev/v1/orgs/{org}`, with an API token (`Authorization: Bearer owf_…`) or the dashboard session. Reads need any role of the whole organization (`readonly` or higher). Keys and settings need `admin`. Stopping a session needs `tech` or higher. Writes take `X-Change-Reason` and are recorded in the [audit log](https://developers.openwaf.dev/change-control/audit-log/); they are not versioned configuration and do not take `dry_run`.

| Route | What it does |
|---|---|
| `GET /browsers` | Overview: status, quota, settings, usage of the month, capacity and access requirements. Readable before access is approved. |
| `PATCH /browsers/settings` | Change `egress_mode`, `allow` and `web_bot_auth` (any subset); answers the overview |
| `GET /browsers/keys` | List keys |
| `POST /browsers/keys` | Create a key: `{"name": "ci-agent"}`; answers `201 {"key", "secret"}` |
| `POST /browsers/keys/{id}/rotate` | New secret: `{"key", "secret"}`. The old secret stops at once and its live browsers end. |
| `DELETE /browsers/keys/{id}` | Revoke a key (`204`); its live browsers end |
| `GET /browsers/usage` | Usage series, per key and totals |
| `GET /browsers/sessions` | Browsers of the organization; `?status=running` |
| `POST /browsers/sessions/{id}/stop` | Stop a browser; answers the session |
| `GET /browsers/egress` | Egress log of the organization |

### Overview

```json
{"enabled": true, "status": "enabled", "available": true,
 "api_url": "https://browsers.openwaf.dev", "plan": "beta",
 "quota": {"browser_hours": 20, "egress_gb": 5, "concurrency": 2, "max_session_minutes": 60, "max_keys": 5},
 "settings": {"egress_mode": "open", "allow": [], "web_bot_auth": true},
 "period": {"start": "2026-10-01T00:00:00Z", "end": "2026-11-01T00:00:00Z"},
 "usage": {"browser_seconds": 4310, "egress_bytes": 123456789, "sessions": 31, "denied_requests": 4},
 "usage_updated_at": "2026-10-29T12:01:00Z",
 "warning": false,
 "capacity": {"max_sessions": 5, "running": 2, "busy": false},
 "requirements": {"owners_email_verified": true, "owners_two_factor": true}}
```

- `status` is `enabled`, `not_enabled` or `quota_exceeded`.
- `warning` is `true` when browser-hours or egress reach 80 % of the quota.
- `capacity` is the shared capacity of the service, or `null` when it is not known.
- `requirements` says whether every owner and admin has a verified email and two-factor authentication, which approval needs.

### Settings

```json
{"egress_mode": "allowlist", "allow": ["example.com", "*.example.com"], "web_bot_auth": true}
```

| Field | Values |
|---|---|
| `egress_mode` | `open`: any public host (the default). `allowlist`: only the hosts in `allow`. Private and special-purpose addresses are refused in both modes. |
| `allow` | Up to 100 entries. `example.com` is that host only; `*.example.com` its subdomains. |
| `web_bot_auth` | Default for new browsers. A browser can still set `webBotAuth` either way. |

### Keys

```json
{"id": "6f1c…", "name": "ci-agent", "prefix": "bdk_AbC1", "disabled": false,
 "created_at": "2026-10-29T10:00:00Z", "created_by": "…", "rotated_at": null}
```

The name matches `^[a-z0-9][a-z0-9-]{0,62}$` and is unique in the organization. `disabled` is `true` while the organization is over quota or not enabled. The `secret` of a create or rotate answer is shown once, with `Cache-Control: no-store`.

### Usage

`GET /browsers/usage?from=RFC3339&to=RFC3339&step=day|hour`. Without parameters: the current month, by day.

```json
{"from": "…", "to": "…", "step": "day",
 "series": [{"t": "2026-10-29T00:00:00Z", "browser_seconds": 1800, "egress_bytes": 52428800, "sessions": 6, "denied_requests": 1}],
 "by_key": [{"key_id": "6f1c…", "name": "ci-agent", "browser_seconds": 1800, "egress_bytes": 52428800, "sessions": 6, "denied_requests": 1}],
 "totals": {"browser_seconds": 1800, "egress_bytes": 52428800, "sessions": 6, "denied_requests": 1},
 "quota": {…}, "period": {"start": "…", "end": "…"}, "usage": {…}}
```

`usage` is the month to date, the figure the quota is checked against. Usage is collected every minute, so the numbers lag by up to about two minutes.

### Sessions

`GET /browsers/sessions?status=running`:

```json
{"items": [{"id": "bs_…", "key_id": "6f1c…", "key_name": "ci-agent", "status": "running",
            "created_at": "…", "started_at": "…", "stopped_at": null, "stop_reason": null,
            "live_url": "https://browsers.openwaf.dev/…", "metadata": {"task": "quickstart"}}]}
```

`status` is `running`, `idle` or `stopped`. `live_url` is a signed, view-only link, present only while the browser runs, valid for 1 hour.

### Egress log

`GET /browsers/egress?key=<id>&since=RFC3339&decision=deny&limit=200`. Newest first; `limit` up to 1000, default 200.

```json
{"items": [{"ts": "…", "key_id": "6f1c…", "key_name": "ci-agent", "session": "bs_…",
            "method": "CONNECT", "host": "example.com", "port": 443, "decision": "allow", "reason": "",
            "status": 200, "bytes_up": 1830, "bytes_down": 48211, "ms": 412}]}
```

`decision` is `allow`, `deny` or `error`; `reason` says why a request was denied.

### Management API errors

| Status | `code` | When |
|---|---|---|
| 403 | `feature_disabled` | OpenWAF Browsers is not enabled for the organization |
| 403 | `forbidden` | The role is too low, or the member or token is limited to a client or a zone |
| 404 | `not_found` | No such key or session in the organization |
| 409 | `conflict` | A key with that name already exists |
| 409 | `key_limit` | The organization already has its maximum of keys (5 by default); revoke one first |
| 422 | `invalid` | A field breaks the rules above; `field` names it |
| 503 | `unavailable` | The browser service cannot be reached right now |

### Operator access

Approval is an operator action, not a customer route: an operator turns OpenWAF Browsers on for the organization with a reason and, if needed, a different quota. It fails while an owner or admin lacks a verified email or two-factor authentication. To ask for it, see [get access](https://developers.openwaf.dev/browsers/#get-access).

## CLI

`openwaf-cloud ctl browsers …` calls the management API. Every change takes `--reason "…"`, shows what it will do and asks before applying it. `--yes` applies without asking (required without a terminal, as in CI); `--dry-run` only shows it. Reads take `--json` for the raw API answer. See [get started with the CLI](https://developers.openwaf.dev/cli/get-started/) and [changes](https://developers.openwaf.dev/cli/changes/#options).

| Command | What it does |
|---|---|
| `browsers status` | The overview: approval, usage of the month against the quota, per-key limits, settings and shared capacity |
| `browsers settings [--egress-mode open\|allowlist] [--allow a.example,*.example] [--web-bot-auth on\|off] --reason "…" [--yes]` | Change the settings (`admin`). `--allow` replaces the whole list; empty means none. Without options it shows the overview. |
| `browsers keys [list]` | Keys, with prefix and state |
| `browsers keys create <name> --reason "…" [--yes]` | Create a key (`admin`); the secret is shown once |
| `browsers keys rotate <key> --reason "…" [--yes]` | New secret (`admin`); the current one stops at once. `<key>` is the name or the id. |
| `browsers keys revoke <key> --reason "…" [--yes]` | Revoke a key (`admin`) |
| `browsers usage [--from RFC3339] [--to RFC3339] [--step day\|hour]` | Usage by day or hour and per key. `--from` defaults to the 1st of this month. |
| `browsers sessions [--status running\|idle\|stopped]` | Browsers of the organization |
| `browsers sessions stop <id> --reason "…" [--yes]` | Stop a browser (`tech`) |
| `browsers egress [--key <key>] [--since 1h\|24h\|7d\|RFC3339] [--deny] [--limit N]` | Requests of the browsers through the egress proxy. `--deny` keeps only denied ones; `--limit` defaults to 200, up to 1000. |

## MCP tools

The [OpenWAF MCP server](https://developers.openwaf.dev/mcp/) has 10 tools for OpenWAF Browsers. Writes need a `reason` and follow the two steps of the other non-versioned tools: `confirm=false` only checks, `confirm=true` applies, without `expected_version`.

| Tool | Arguments | Role |
|---|---|---|
| `browser_status` | | `readonly` |
| `browser_settings` | none to read; `egress_mode`, `allow`, `web_bot_auth`, `reason`, `confirm` to change | `admin` to change |
| `browser_keys_list` | | `readonly` |
| `browser_keys_create` | `name`, `reason`, `confirm`; the secret is returned once | `admin` |
| `browser_keys_rotate` | `key`, `reason`, `confirm` | `admin` |
| `browser_keys_revoke` | `key`, `reason`, `confirm` | `admin` |
| `browser_sessions_list` | `status` | `readonly` |
| `browser_sessions_stop` | `session`, `reason`, `confirm` | `tech` |
| `browser_usage` | `from`, `to`, `step` | `readonly` |
| `browser_egress_events` | `key`, `since`, `decision`, `limit` | `readonly` |

The MCP tools manage the organization. To drive a browser from an assistant, use the Playwright MCP server with the browser's `cdpUrl`, as in the [quickstart](https://developers.openwaf.dev/browsers/quickstart/#drive-it).
