This guide creates a browser key, starts a browser, drives it from your code and stops it. It takes about five minutes.
Before you start
- Access. OpenWAF Browsers must be enabled for your organization. Until it is, the writes answer
403 feature_disabled. See get access. - An organization-wide
adminto create keys. Reading usage and sessions only needsreadonly. - Node.js 18 or later for the Playwright, Puppeteer and MCP examples.
1. Create a key
A browser key (bdk_…) authenticates your agent at https://browsers.openwaf.dev. An organization has up to 5 keys. Each key runs up to 2 browsers at the same time. The secret is shown once; afterwards the dashboard and the API only show its prefix (bdk_ and 4 characters).
- Open Browsers in the organization menu, then the Keys tab.
- Choose Create key and give it a name, for example
ci-agent(lowercase letters, digits and hyphens). - Copy the secret now. You will not see it again.
openwaf-cloud ctl browsers keys create ci-agent --reason "Browser for the CI agent"The CLI shows what it will create and asks before applying it; then it prints the secret once. Add --yes to skip the question (required without a terminal), or --dry-run to only see it. See get started with the CLI for the API address and token.
curl -sS -X POST "https://dash.openwaf.dev/v1/orgs/$ORG/browsers/keys" \
-H "Authorization: Bearer $OPENWAF_TOKEN" -H "Content-Type: application/json" \
-H "X-Change-Reason: Browser for the CI agent" \
-d '{"name": "ci-agent"}'The 201 answer carries the key and its secret, with Cache-Control: no-store:
{"key": {"id": "6f1c…", "name": "ci-agent", "prefix": "bdk_AbC1", "disabled": false,
"created_at": "2026-10-29T10:00:00Z", "created_by": "…", "rotated_at": null},
"secret": "bdk_…"}Ask your assistant, connected to the OpenWAF MCP server with an admin credential, to create a browser key. It calls browser_keys_create with name and a reason: first with confirm=false to check, then with confirm=true. The answer carries the secret once, so it passes through the conversation: rotate the key later if that matters to you.
Store the secret where your agent reads its secrets. The examples below read it from OPENWAF_BROWSERS_KEY:
read -rs OPENWAF_BROWSERS_KEY && export OPENWAF_BROWSERS_KEY # paste bdk_…2. Start a browser
curl -sS -X POST https://browsers.openwaf.dev/v1/browsers \
-H "X-API-Key: $OPENWAF_BROWSERS_KEY" -H "Content-Type: application/json" \
-d '{"metadata": {"task": "quickstart"}}'Every field of the body is optional. The answer:
{"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", …}Open liveUrl in your own browser to watch. cdpUrl and liveUrl carry a token valid for 1 hour; GET /v1/browsers/{id} returns fresh ones.
If every slot of the beta is busy you get 429 with {"code": "capacity_busy"} and Retry-After: 30. If this key already runs 2 browsers you get 429 with {"code": "key_concurrency"}. See errors.
3. Drive it
import { chromium } from "playwright";
const api = "https://browsers.openwaf.dev/v1/browsers";
const headers = { "X-API-Key": process.env.OPENWAF_BROWSERS_KEY, "Content-Type": "application/json" };
const b = await (await fetch(api, { method: "POST", headers, body: "{}" })).json();
const browser = await chromium.connectOverCDP(b.cdpUrl);
const page = await browser.contexts()[0].newPage();
await page.goto("https://example.com");
console.log(await page.title());
await browser.close(); // disconnects; the browser keeps running until stopped or idle
await fetch(`${api}/${b.id}`, { method: "PATCH", headers, body: JSON.stringify({ action: "stop" }) });import puppeteer from "puppeteer-core";
const api = "https://browsers.openwaf.dev/v1/browsers";
const headers = { "X-API-Key": process.env.OPENWAF_BROWSERS_KEY, "Content-Type": "application/json" };
const b = await (await fetch(api, { method: "POST", headers, body: "{}" })).json();
const browser = await puppeteer.connect({ browserWSEndpoint: b.cdpUrl });
const page = await browser.newPage();
await page.goto("https://example.com");
console.log(await page.title());
await browser.disconnect();
await fetch(`${api}/${b.id}`, { method: "PATCH", headers, body: JSON.stringify({ action: "stop" }) });Start a browser with curl (step 2), then give its cdpUrl to the Playwright MCP server. Your assistant's browser tools then drive the hosted Chrome:
npx @playwright/mcp@latest --cdp-endpoint "wss://browsers.openwaf.dev/cdp/…"In an MCP client configuration:
{"mcpServers": {"browser": {"command": "npx",
"args": ["@playwright/mcp@latest", "--cdp-endpoint", "wss://browsers.openwaf.dev/cdp/…"]}}}The CDP token expires after 1 hour. For a longer task, get a fresh cdpUrl with GET /v1/browsers/{id} and restart the server.
4. Stop the browser
Closing the CDP connection does not stop a browser: it keeps counting browser time until it has had no CDP or live view client for 120 seconds. Stop it when the task is done:
curl -sS -X PATCH "https://browsers.openwaf.dev/v1/browsers/$ID" \
-H "X-API-Key: $OPENWAF_BROWSERS_KEY" -H "Content-Type: application/json" \
-d '{"action": "stop"}'Then list what it left: the recording (if you asked for one) and the downloads, with links that expire.
curl -sS "https://browsers.openwaf.dev/v1/browsers/$ID/files" -H "X-API-Key: $OPENWAF_BROWSERS_KEY"A member with the tech role or higher can also stop any running browser of the organization: Browsers → Sessions → Stop, openwaf-cloud ctl browsers sessions stop ID --reason "…" or the browser_sessions_stop MCP tool.