Cloud REST API
Programmatic access to your browsergateway.com workspace over HTTP with a bearer token.
What it is
The Cloud REST API is the same control plane the dashboard at app.browsergateway.com talks to. Most of what you do in the dashboard (list sessions, manage providers and routers, create profiles, read recordings and traces, configure webhooks, read billing) can be scripted through this API.
This is distinct from the other surfaces on the platform:
| Surface | Base URL | Auth | Purpose |
|---|---|---|---|
| Cloud REST API (this page) | https://app-api.browsergateway.com/v1 | Authorization: Bearer bga_... | Control plane: workspaces, routers, providers, profiles, sessions, billing |
| Cloud CDP endpoint | wss://cdp.browsergateway.io/v1/connect?token=bg_... | Router key | Open a browser session over CDP (Puppeteer, Playwright) |
| Cloud Data API | https://cdp.browsergateway.io/v1/* | Router key | One-shot screenshot, content and scrape |
| Self-hosted gateway REST | http://your-host:9500/v1/* | BG_TOKEN | The gateway you run yourself |
Account tokens (bga_..., this page) and router keys (bg_..., for CDP, the Data API and MCP) are separate credentials.
Get a token
Sign in to app.browsergateway.com, open Keys & access, and create an access token. Give it a label so you can spot it in the list later (ci-runner, analytics-dashboard).
The plaintext token is shown once, right after creation. Copy it immediately. If you lose it, rotate or revoke the token.
Each user can hold 10 active tokens. Tokens belong to your user, not to a workspace: a token acts with your membership in every workspace you belong to, with your role in each.
Auth
Every request carries the bearer in the Authorization header:
curl https://app-api.browsergateway.com/v1/me \
-H "Authorization: Bearer bga_YOUR_TOKEN_HERE"A few endpoints refuse bearer tokens and need a signed-in dashboard session. See Cookie-only endpoints.
Endpoints that change configuration, return a credential, or return decrypted profile data need the owner or admin role in the workspace. A member gets 403 forbidden on those.
Choosing the workspace and router
| Header | Effect |
|---|---|
X-Workspace-Id: <id> | The workspace this request acts on. If you are not a member, the request fails with 403 not_a_member. |
X-Router-Id: <id> | The router this request acts on, for endpoints scoped to one router (providers, router keys, inspections). |
Send both headers on every bearer call if you belong to more than one workspace or your workspace has more than one router. Without them, a bearer call uses one of your workspaces and its first router, which is not necessarily the one selected in the dashboard. Some endpoints also accept ?routerInstanceId=<id> to target a router.
Common response shape
Success:
{ "ok": true, "data": { "...": "..." } }Error:
{
"ok": false,
"error": {
"code": "not_found",
"message": "Profile not found."
}
}The billing endpoints are the exception: their errors carry a plain string, for example { "ok": false, "error": "bad_request" }.
Error codes
| Code | HTTP | Meaning |
|---|---|---|
validation_error | 400 | Request body or parameters failed validation. Some endpoints use bad_request, invalid_input, invalid_url or invalid_id. |
unauthenticated | 401 | Missing or invalid bearer |
fair_use_limit_reached | 402 | A fair-use ceiling was hit: 100 routers, 500 profiles or 100 router keys per workspace, or 1,000 members |
cookie_required | 403 | Endpoint refuses bearers (see below) |
forbidden | 403 | Your role in the workspace does not allow this action |
not_a_member | 403 | X-Workspace-Id names a workspace you do not belong to |
not_found | 404 | Resource missing or in another workspace |
conflict | 409 | A router, provider or profile with that slug already exists |
last_router | 409 | A workspace must keep at least one router |
limit_reached | 409 | You already have 10 active access tokens |
payload_too_large | 413 | A profile is larger than 10 MB |
profile_storage_cap_exceeded | 413 | Saving the profile would pass the workspace's 500 MB profile storage |
rate_limited | 429 | Daily bearer cap reached (see Rate limits) |
provider_calibrating | 503 | A browserserve provider is still starting. Retry shortly. |
Rate limits
Bearer calls to the providers, routers, profiles, sessions, replays, inspections and webhooks endpoints count against a per-workspace daily cap. Other endpoints do not count.
| Account state | Daily cap |
|---|---|
| Wallet balance of zero or below | 5,000 |
| Wallet balance above zero | 100,000 |
| Enterprise | 100,000 |
The count resets at midnight UTC. Counted responses carry:
X-RateLimit-Limit: 100000
X-RateLimit-Remaining: 99871
X-RateLimit-Reset: 1735689600X-RateLimit-Reset is Unix seconds. Over the cap, responses are 429 rate_limited with a Retry-After header. The cap follows your wallet: a workspace whose balance drops to zero goes back to 5,000 calls per day.
Dashboard traffic from a signed-in session never counts.
Endpoints
Grouped by resource. Every path is under https://app-api.browsergateway.com.
User and workspaces
GET /v1/me
The authenticated user, their workspaces, and the workspace and router this request resolved to.
{
"ok": true,
"data": {
"user": {
"id": "usr_abc123",
"email": "you@example.com",
"name": "Ada Lovelace",
"emailVerified": true,
"image": null
},
"workspaces": [
{ "id": "ws_personal", "slug": "ada", "name": "Ada", "role": "owner" },
{ "id": "ws_team", "slug": "acme", "name": "Acme", "role": "member" }
],
"activeWorkspaceId": "ws_personal",
"activeWorkspacePlan": "free",
"activeWorkspaceAccountState": "paid",
"routers": [
{ "id": "ri_main", "slug": "main", "name": "Main" }
],
"activeRouterInstanceId": "ri_main",
"isAdmin": false
}
}activeWorkspaceAccountState is "free", "paid" or "enterprise". "paid" means the wallet balance is above zero right now.
GET /v1/workspaces
Every workspace you belong to: { "items": [{ "id", "slug", "name", "role" }] }.
POST /v1/workspaces/active
Set the workspace the dashboard opens: { "workspaceId": "ws_team" }. Bearer calls do not read this; they use X-Workspace-Id.
GET /v1/workspaces/current/members
{
"ok": true,
"data": {
"items": [
{
"id": "wm_abc",
"userId": "usr_abc",
"role": "owner",
"joinedAt": "2026-09-01T09:12:04.000Z",
"name": "Ada",
"email": "you@example.com",
"image": null
}
]
}
}POST /v1/workspaces/current/leave
Leave the workspace. The owner cannot leave; delete the workspace instead.
DELETE /v1/workspaces/current/members/:userId
Remove a member. Owner or admin, cookie session only. The owner cannot be removed.
DELETE /v1/workspaces/current
Delete the workspace and everything in it, immediately. Owner only, cookie session only. The body must confirm the slug: { "confirm": "acme" }.
Invitations
Owners and admins invite people by email. The invitation link is valid for 7 days.
| Endpoint | Who | What it does |
|---|---|---|
GET /v1/invitations | Any member | Pending invitations: id, email, role, expiresAt, createdAt, invitedByName |
POST /v1/invitations | Owner or admin, cookie session only | Invite { "email": "...", "role": "admin" | "member" }. Sends the email. |
POST /v1/invitations/:id/resend | Owner or admin | New link, new 7-day expiry, email sent again |
DELETE /v1/invitations/:id | Owner or admin | Cancel a pending invitation |
GET /v1/invitations/:token/lookup | Anyone with the link | Workspace name, inviter, role, expiry |
POST /v1/invitations/:token/accept | The invited user, signed in | Join the workspace |
POST /v1/invitations/:token/decline | The invited user, signed in | Decline |
Accept and decline only work when you are signed in with the invited email address.
Routers
A router is one routing endpoint with its own providers, router keys, strategy and settings. A workspace can hold up to 100 routers.
GET /v1/routers
{
"ok": true,
"data": {
"items": [
{
"id": "ri_main",
"slug": "main",
"name": "Main",
"description": null,
"kind": "route",
"strategy": "round-robin",
"cooldownDefaultMs": 30000,
"cooldownFailureThreshold": 0.5,
"cooldownMinRequestVolume": 3,
"connectionTimeoutMs": 15000,
"sessionIdleTimeoutMs": 300000,
"queueMaxSize": 50,
"queueTimeoutMs": 30000,
"healthCheckIntervalMs": 30000,
"providerCount": 2,
"apiKeyCount": 1,
"createdAt": "2026-07-24T10:00:00.000Z",
"updatedAt": "2026-09-01T10:00:00.000Z"
}
],
"total": 1
}
}GET /v1/routers/:id returns one router in the same shape.
POST /v1/routers
{ "name": "CI runner", "slug": "ci-runner", "description": "Nightly jobs" }slug is optional (derived from name) and must be 2 to 40 lowercase letters, digits or hyphens. A new router gets its first router key. Response 201 with the router row.
PATCH /v1/routers/:id
Any subset of:
| Field | Range | Default |
|---|---|---|
name, description | text | |
strategy | priority-chain, round-robin, least-connections, latency-optimized, weighted | round-robin |
connectionTimeoutMs | 1,000 to 30,000 | 15,000 |
sessionIdleTimeoutMs | 10,000 to 840,000 | 300,000 |
queueMaxSize | 0 to 10,000 | 50 |
queueTimeoutMs | 0 to 86,400,000 | 30,000 |
healthCheckIntervalMs | 5,000 to 3,600,000 | 30,000 |
cooldownDefaultMs | 0 to 86,400,000 | 30,000 |
cooldownFailureThreshold | 0 to 1 | 0.5 |
cooldownMinRequestVolume | 0 to 1,000,000 | 3 |
Response: the full router row.
DELETE /v1/routers/:id
Deletes the router, its providers and its router keys. Response { "ok": true, "data": { "id": "ri_ci", "slug": "ci-runner" } }. Deleting a workspace's only router returns 409 last_router.
GET /v1/routers/:id/settings
The router's feature switches. All are true on a new router.
{
"ok": true,
"data": {
"routerInstanceId": "ri_main",
"webhooksEnabled": true,
"profilesEnabled": true,
"replayEnabled": true,
"observabilityEnabled": true
}
}PATCH /v1/routers/:id/settings takes any subset of the four booleans. replayEnabled is the Recording switch and observabilityEnabled is the Traces switch. See Router switches for what each one blocks.
GET /v1/routers/:id/agent-settings
Defaults for browsers that agents open through Cloud MCP without asking for these options themselves:
{
"ok": true,
"data": {
"routerInstanceId": "ri_main",
"recordDefault": false,
"observabilityDefault": false,
"defaultProfile": null,
"saveProfileDefault": false
}
}PATCH /v1/routers/:id/agent-settings takes any subset. defaultProfile must name an existing profile, or null.
POST /v1/routers/active
Set the router the dashboard opens: { "routerInstanceId": "ri_ci" }. Bearer calls use X-Router-Id instead.
Providers
Providers are the browser upstreams a router routes to. These endpoints act on the router from X-Router-Id, or on ?routerInstanceId=<id> when given.
GET /v1/providers
{
"ok": true,
"data": {
"items": [
{
"id": "prv_abc",
"ownerId": "ws_personal",
"slug": "browserless-eu",
"name": "Browserless EU",
"url": "wss://browserless.example.com/",
"adapterKind": null,
"priority": 100,
"weight": 100,
"maxConcurrent": 5,
"profile": null,
"capabilities": {
"providerKind": "generic",
"browserCookies": "supported",
"targetCreate": "supported",
"targetGetTargets": "supported",
"fetchInterception": "supported",
"pageScreencast": "supported",
"targetCreateLatencyMs": 41,
"advertisedMaxConcurrent": null,
"probedAt": "2026-08-25T14:12:11.000Z",
"probeDurationMs": 830,
"errors": []
},
"headers": null,
"enabled": true,
"createdAt": "2026-08-25T14:12:11.000Z",
"updatedAt": "2026-08-25T14:12:11.000Z"
}
],
"total": 1,
"page": 1,
"pageSize": 1
}
}The url is returned without its query string or user credentials, and headers is always null, so credentials never appear in list responses. GET /v1/providers/:slug returns one provider in the same shape.
GET /v1/providers/:slug/plaintext
The stored url and headers in full, credentials included. Owner or admin.
POST /v1/providers
Add a provider by its connection address:
{
"slug": "my-browserless",
"name": "My browserless",
"url": "wss://production-sfo.browserless.io/?token=YOUR_TOKEN",
"headers": { "X-Region": "sfo" },
"priority": 100,
"weight": 100,
"maxConcurrent": 3
}| Field | Rules |
|---|---|
slug | Required. Letters, digits and single hyphens, up to 64 characters, unique on the router. |
url | A ws:// or wss:// address. |
headers | Optional. Sent on every connection to this provider, up to 20. |
priority, weight | 1 to 1000. Both default to 100. |
maxConcurrent | 1 to 1000. Omit for no limit on our side. A browserserve provider's advertised capacity is used when you omit it. |
profile | Optional profile slug. Sends only sessions using that profile to this provider. Omit to take every session; any provider can save any profile either way. null is not accepted on create. |
The provider is tested and its capabilities recorded when it is added. Response 201 with the provider row.
Or add a provider that creates a browser for each session through its own API, using a key instead of an address:
{
"slug": "kernel",
"adapterKind": "kernel",
"credentials": { "apiKey": "YOUR_KERNEL_KEY" },
"options": { "region": "us-east" }
}The key is checked with the provider before the provider is saved, without starting a paid browser. POST /v1/providers/verify-key runs the same check on its own: { "adapterKind", "credentials", "options" } returns { "verified": true, "provider": "Kernel" }, 401 when the provider rejects the key, or 502 when the provider cannot be reached. The providers available this way are listed on the dashboard's Providers page.
adapterKind | Provider | options | When a session ends |
|---|---|---|---|
kernel | Kernel | region (us-east, eu-west, ap-southeast), stealth, timeoutSeconds (10 to 259200) | The browser is kept for 15 minutes so a reconnect reuses it, then stopped. |
browser-use | Browser Use | timeoutMinutes (1 to 240, default 240), proxyCountryCode (two lowercase letters, for example de; off by default) | The browser is stopped through Browser Use's API, whether the client disconnects or calls browser.close(), so Browser Use refunds the unused part of its upfront charge. |
{
"slug": "browser-use",
"adapterKind": "browser-use",
"credentials": { "apiKey": "YOUR_BROWSER_USE_KEY" }
}PATCH /v1/providers/:slug
Any subset of name, url, headers, priority, weight, maxConcurrent, profile and enabled. profile: null unpins the provider. A new url is tested again. Providers added with a key have no address, so url and headers are refused on them.
enabled: false switches a provider off without deleting it: the router behaves as if it were not there, and sessions already running on it continue. enabled: true switches it back on.
Response: the provider row.
DELETE /v1/providers/:slug
Response { "ok": true, "data": { "slug": "my-browserless" } }.
POST /v1/providers/:slug/test
Opens a connection to the provider and reports the result:
{ "ok": true, "data": { "ok": true, "latencyMs": 428 } }On failure data.ok is false and data.reason explains why. For a provider added with a key, this checks the saved key instead of opening a browser.
POST /v1/providers/:slug/reprobe
Runs the capability check again and stores the result. Response: the provider row. Not available for providers added with a key.
POST /v1/providers/test-url and POST /v1/providers/probe
Check an address before adding it. Both take { "url": "wss://...", "headers": { } }. test-url returns the same shape as /test. probe returns { "detectedKind": "browserserve" | "generic" | null, "advertisedMaxConcurrent": 16 | null }.
Router keys
Router keys (bg_...) are the credential for the CDP endpoint, the Data API and MCP. Each key belongs to one router. These endpoints act on the router from X-Router-Id, or on routerInstanceId in the query or body.
GET /v1/api-keys
{
"ok": true,
"data": {
"items": [
{
"id": "ak_abc",
"routerInstanceId": "ri_main",
"label": "Default",
"prefix": "bg_yYrx0aQ",
"lastUsedAt": "2026-09-01T14:03:22.000Z",
"revokedAt": null,
"createdAt": "2026-08-25T14:12:11.000Z"
}
],
"total": 1
}
}Revoked keys stay in the list with revokedAt set. GET /v1/api-keys/:id returns one key.
POST /v1/api-keys
Add another key to a router: { "label": "staging", "routerInstanceId": "ri_main" }. A router can hold several live keys. Owner or admin. Response 201:
{
"ok": true,
"data": {
"id": "ak_new",
"routerInstanceId": "ri_main",
"label": "staging",
"prefix": "bg_9KfrL0a",
"lastUsedAt": null,
"revokedAt": null,
"createdAt": "2026-09-01T14:12:11.000Z",
"plaintextKey": "bg_9KfrL0aQwxDeMt..."
}
}Store plaintextKey immediately.
POST /v1/api-keys/rotate
Revokes every live key on the router and issues one new key, in the same response shape as create. The old keys stop working immediately, so update every place that uses them right after rotating. Optional body: { "routerInstanceId": "...", "label": "..." }.
DELETE /v1/api-keys/:id
Revokes one key immediately.
GET /v1/api-keys/current/plaintext
The router's newest live key in plaintext: { "apiKeyId": "ak_abc", "plaintextKey": "bg_..." }. Owner or admin. This is what the dashboard uses to fill in your key.
Chat links
The chat link is the credential chat apps use to reach Cloud MCP. One per router. ?routerInstanceId=<id> selects the router.
| Endpoint | What it does |
|---|---|
GET /v1/mcp-links/current | The current link: { "id", "routerInstanceId", "token", "lastUsedAt", "createdAt" }, or null when there is none |
POST /v1/mcp-links/rotate | Replace the link. The old one stops working immediately. Response 201. |
DELETE /v1/mcp-links/current | Remove the link |
Profiles
Saved browser state: cookies and localStorage per origin, plus IndexedDB for profiles saved from browserserve providers. A workspace holds up to 500 profiles, 10 MB each and 500 MB in total.
GET /v1/profiles
{
"ok": true,
"data": {
"items": [
{
"id": "prf_abc",
"workspaceId": "ws_personal",
"slug": "acme-checkout",
"name": "Acme checkout state",
"sizeBytes": 4218,
"capturedOrigins": ["https://acme.com"],
"lastUsedAt": "2026-09-01T13:44:02.000Z",
"createdAt": "2026-08-01T09:00:00.000Z",
"updatedAt": "2026-09-01T13:44:02.000Z"
}
],
"total": 1,
"page": 1,
"pageSize": 1
}
}GET /v1/profiles/:slug returns one profile's metadata in the same shape.
POST /v1/profiles
Create an empty profile. The first session that connects with ?profile=acme-checkout fills it.
{ "slug": "acme-checkout", "name": "Acme checkout state" }Or seed it with saved state:
{
"slug": "acme-checkout",
"name": "Acme checkout state",
"captured": {
"version": 1,
"capturedAt": "2026-09-01T12:00:00.000Z",
"cookies": [
{ "name": "session", "value": "abc123", "domain": ".acme.com", "path": "/" }
],
"storage": {
"https://acme.com": {
"localStorage": { "cart": "[1,2]" },
"sessionStorage": {}
}
},
"meta": { "capturedOrigins": ["https://acme.com"], "skippedOrigins": [], "durationMs": 0 }
}
}Slugs are 1 to 64 letters, digits, dots, underscores or hyphens. Omit captured for an empty profile; null is not accepted. Response 201 with the profile row. Owner or admin.
POST /v1/profiles/import-playwright
Create a profile from a Playwright storageState (as saved by context.storageState()):
{
"slug": "acme-checkout",
"name": "Acme checkout state",
"storageState": { "cookies": [], "origins": [] }
}Response 201 with the profile row.
GET /v1/profiles/:slug/export
The profile's current state, decrypted. ?format=native (the default) returns the same shape as captured above; ?format=playwright returns a Playwright storageState. Owner or admin. The response contains live session cookies, so treat it like a password.
DELETE /v1/profiles/:slug
Deletes the profile and all its saved versions. Owner or admin. Response { "ok": true, "data": { "slug": "acme-checkout" } }.
Sessions
GET /v1/sessions
Sessions across the whole workspace, newest first.
| Query | Default | Notes |
|---|---|---|
page | 1 | |
pageSize | 25 | Up to 100 |
state | all | active, closed or errored |
provider | all | Provider slug on the request's router |
since | 30 days ago | Unix milliseconds. Up to 180 days back. |
kind | all | route for CDP connections, agent for Cloud MCP browsers |
routerInstanceId | all routers | Limit to one router |
{
"ok": true,
"data": {
"items": [
{
"id": "sess_202609_01H8XY_s0",
"state": "closed",
"kind": "route",
"startedAt": "2026-09-01T14:00:03.000Z",
"endedAt": "2026-09-01T14:00:41.000Z",
"durationMs": 38000,
"bytesIn": 12488,
"bytesOut": 3902,
"totalBytes": 16390,
"messageCount": 214,
"billedCents": 0,
"errorReason": null,
"provider": { "id": "prv_abc", "slug": "browserless-eu", "name": "Browserless EU" },
"hasRecording": false
}
],
"total": 1284,
"page": 1,
"pageSize": 25
}
}GET /v1/sessions/:id
One session, in the same shape plus hasReplay, which is true when the session has a trace. hasRecording is true when it has a video recording.
POST /v1/sessions/:id/close
Marks a session that is stuck as active as closed. It does not end the browser on the provider; the provider's own timeout does that. Owner or admin. Response { "id", "state": "closed", "closedAt" }, or alreadyClosed: true when it had already ended.
Recordings
Video recordings of sessions opened with session_record=true. See Recordings.
GET /v1/replays
Recordings from the last 30 days, newest first. Query: limit (default 50, up to 200), session_id, routerInstanceId.
{
"ok": true,
"data": {
"count": 1,
"replays": [
{
"id": "rep_abc",
"sessionLogId": "sess_202609_01H8XY_s0",
"routerInstanceId": "ri_main",
"providerId": "prv_abc",
"profileId": null,
"startedAt": "2026-09-01T14:00:03.000Z",
"endedAt": "2026-09-01T14:00:41.000Z",
"durationMs": 38000,
"frameCount": 428,
"chunkCount": 3,
"sizeBytes": 8842112,
"droppedFrames": 0,
"duplicatesSkipped": 12,
"format": "jpeg",
"complete": true,
"capStopped": false,
"truncated": null,
"mp4Status": "ready",
"mp4SizeBytes": 1204331,
"mp4ReadyAt": "2026-09-01T14:01:12.000Z",
"mp4Error": null
}
]
}
}mp4Status is queued, running, ready or failed. truncated is null, byte-cap or wallet-drained.
GET /v1/replays/:id returns one recording in the same shape plus orphaned.
POST /v1/replays/:id/export
Starts encoding the recording to MP4. Encoding already starts on its own when the session ends, so use this when mp4Status is failed or the video never appeared. Returns { "mp4Status": "running" }, or alreadyReady: true when the video exists. A recording still in progress returns 409 not_complete.
GET /v1/replays/:id/export/download
The MP4 file (Content-Type: video/mp4). Returns 409 not_ready until mp4Status is ready.
Frames
The raw frames stay available alongside the video: GET /v1/replays/:id/manifest (frame list with timings), GET /v1/replays/:id/chunks/:index (one chunk of frames) and GET /v1/replays/:id/complete (the end-of-session summary).
DELETE /v1/replays/:id
Deletes the recording and its video. Owner or admin.
Traces
Traces of sessions opened with observability=true. See Traces. These endpoints only show traces from the request's router.
GET /v1/inspections
Query: limit (default 50, up to 200), since (Unix milliseconds, default 24 hours ago).
{
"ok": true,
"data": {
"count": 1,
"inspections": [
{
"id": "sess_202609_01H8XY_s0",
"sessionLogId": "sess_202609_01H8XY_s0",
"routerInstanceId": "ri_main",
"providerId": "prv_abc",
"startedAt": "2026-09-01T14:00:03.000Z",
"endedAt": "2026-09-01T14:00:41.000Z",
"durationMs": 38000,
"eventCount": 128,
"sizeBytes": 43112,
"droppedEvents": 0,
"complete": true
}
]
}
}GET /v1/inspections/:id returns one trace in the same shape plus orphaned.
GET /v1/inspections/:id/events
The trace's events.
- No query: every event as one JSON array.
?page=0,?page=1, ...: one page,{ "events": [], "page": 0, "pages": 4, "eventCount": 128 }.?download=jsonor?download=gz: the whole trace as a file, plain or gzipped.
While the session is still running this returns 409 trace_in_progress with Retry-After: 10.
DELETE /v1/inspections/:id
Deletes the trace. Owner or admin.
Webhooks
Webhooks belong to the workspace. Each one can be limited to one router with routerInstanceId; leave it null to receive events from every router.
GET /v1/webhooks
{
"ok": true,
"data": {
"items": [
{
"id": "whk_abc",
"workspaceId": "ws_personal",
"routerInstanceId": null,
"label": "Session alerts",
"url": "https://your.app/hooks/bg/3f9c2a7e",
"events": ["session.ended", "session.error"],
"active": true,
"createdAt": "2026-08-01T09:00:00.000Z",
"updatedAt": "2026-08-01T09:00:00.000Z",
"lastDeliveredAt": "2026-09-01T14:01:12.000Z",
"lastFailureAt": null,
"lastFailureReason": null
}
],
"total": 1,
"page": 1,
"pageSize": 1
}
}GET /v1/webhooks/:id returns one webhook in the same shape.
POST /v1/webhooks
{
"label": "Session alerts",
"url": "https://your.app/hooks/bg/3f9c2a7e",
"events": ["session.ended", "session.error"],
"routerInstanceId": null
}label and url are required. Omit events to receive every event. Response 201 with the webhook row.
Events
| Event | Sent when |
|---|---|
session.ended | A CDP or agent session closes normally |
session.error | A CDP or agent session closes with an error |
The API also accepts session.started, provider.up, provider.down, provider.cooldown, credit.low, credit.exhausted and workspace.suspended in events, but none of these is sent today. Data API calls and playground sessions do not send webhooks. Nothing is sent for a router whose Webhooks switch is off.
Each delivery is a POST with this body:
{
"version": "1",
"timestamp": "2026-09-01T14:00:41.000Z",
"event": "session.ended",
"status": "resolved",
"source": "browser-gateway/cloud",
"data": {
"sessionId": "sess_202609_01H8XY_s0",
"providerSlug": "browserless-eu",
"durationMs": 38000,
"bytesIn": 12488,
"bytesOut": 3902,
"billedCents": 0,
"errored": false
}
}status is resolved for session.ended and firing for session.error, which also carries errorReason. A delivery that does not get a 2xx response within 5 seconds is retried three times, after 0.5, 2 and 8 seconds.
Deliveries are not signed. To make sure a request came from us, put an unguessable secret in the webhook URL (a long random path segment or query parameter), check it on your side, and use an https:// URL.
PATCH /v1/webhooks/:id
Any subset of label, url, events, routerInstanceId and active.
DELETE /v1/webhooks/:id
Response { "ok": true, "data": { "id": "whk_abc" } }.
POST /v1/webhooks/:id/test
Sends one webhook.test event, without retries. Response { "delivered": true, "attempts": 1 }.
Billing
Money amounts are in US cents (...Cents) or in thousandths of a cent (...Millicents, 100,000 per dollar).
GET /v1/billing/summary
{
"ok": true,
"data": {
"accountState": "paid",
"plan": "free",
"suspended": false,
"period": {
"startedAt": "2026-09-01T00:00:00.000Z",
"endsAt": "2026-10-01T00:00:00.000Z"
},
"sessions": {
"usedCount": 1244,
"includedCount": 10000,
"usedPct": 0.1244,
"overageRateCents": 0.05
},
"wallet": {
"balanceUsdCents": 8721,
"minTopupCents": 500,
"maxTopupCents": 2500000,
"cardFeePercent": 0,
"cardFeeBasisPoints": 0,
"cardFeeFixedCents": 0,
"cryptoFeePercent": 0,
"cryptoFeeBasisPoints": 0,
"cryptoFeeFixedCents": 0
},
"rates": {
"recordingPerMinuteCents": 0.7,
"observabilityPer1kEventsCents": 5
}
}
}GET /v1/credits/balance returns only the balance: { "workspaceId", "balanceUsdCents", "updatedAt" }.
GET /v1/billing/usage
Query: days (default 30, up to 90).
{
"ok": true,
"data": {
"days": 30,
"series": [
{
"day": "2026-09-01",
"sessions": 128,
"bytesIn": 8912,
"bytesOut": 2211,
"totalBytes": 11123,
"billedCents": 0,
"billedMillicents": 12000,
"errored": 0
}
],
"totals": { "sessions": 12442, "bytes": 1298112, "billedCents": 0, "billedMillicents": 4218000, "errored": 4 },
"breakdown": {
"sessionMillicents": 0,
"recordingMillicents": 800000,
"observabilityMillicents": 100000,
"byokOverageMillicents": 0,
"resellerMillicents": 0,
"topupMillicents": 10000000,
"refundMillicents": 0,
"grantMillicents": 0
}
}
}billedMillicents is what was drawn from the wallet that day.
GET /v1/billing/ledger
Wallet activity rolled up per day and kind.
| Query | Default | Notes |
|---|---|---|
limit | 90 | Up to 400 rows |
months | 3 | 1 to 12 |
kind | all | session, recording, observability, byok_overage, reseller, topup, refund, grant |
sinceDay | none | YYYY-MM-DD |
{
"ok": true,
"data": {
"rows": [
{
"workspaceId": "ws_personal",
"day": "2026-09-01",
"kind": "recording",
"count": 4,
"totalMillicents": -84000,
"updatedAt": 1756735200000
}
]
}
}Rows are sorted newest day first. totalMillicents is negative for charges and positive for top-ups and grants.
GET /v1/billing/transactions
The workspace's last 100 top-ups and refunds:
{
"ok": true,
"data": {
"items": [
{
"id": "ord_xyz",
"provider": "paddle",
"providerTxnId": "txn_...",
"amountUsdCents": 10000,
"status": "paid",
"kind": "topup",
"receiptUrl": "https://...",
"createdAt": "2026-09-01T13:45:00.000Z",
"paidAt": "2026-09-01T13:45:12.000Z",
"refundedAt": null
}
]
}
}GET /v1/billing/transactions/:providerTxnId returns one transaction by its payment processor id.
POST /v1/billing/checkout
Starts a wallet top-up: { "topupUsdCents": 2000 } (500 to 2,500,000). Returns { "checkoutUrl", "creditCents", "feeCents", "chargeCents" }; open checkoutUrl in a browser to pay. An invalid amount returns { "ok": false, "error": "bad_request" }.
Access tokens
GET /v1/access-tokens
Your account tokens, metadata only:
{
"ok": true,
"data": {
"items": [
{
"id": "aat_abc",
"label": "ci-runner",
"prefix": "bga_JLNh3kQ2",
"scopes": "account:full",
"lastUsedAt": "2026-09-01T14:03:22.000Z",
"revokedAt": null,
"createdAt": "2026-08-25T14:12:11.000Z"
}
],
"total": 1,
"cap": 10
}
}lastUsedAt updates at most once an hour. Creating, rotating, revoking and deleting tokens needs a cookie session (see below).
Account
DELETE /v1/account
Marks your account for deletion, together with the workspaces you own alone, and signs you out. You have 30 days to undo it with POST /v1/account/undelete; after that the account is removed. If you own a workspace that has other members, this returns 409 workspaces_have_other_members: delete those workspaces first.
Cookie-only endpoints
These endpoints refuse bearer tokens and need a signed-in dashboard session:
POST /v1/access-tokens: create an account tokenPOST /v1/access-tokens/:id/rotate: rotate an account tokenPOST /v1/access-tokens/:id/revoke: revoke an account tokenDELETE /v1/access-tokens/:id: delete an account tokenDELETE /v1/workspaces/current: delete the workspaceDELETE /v1/workspaces/current/members/:userId: remove a memberPOST /v1/invitations: invite a member
A bearer request to one of them gets:
{ "ok": false, "error": { "code": "cookie_required", "message": "This endpoint requires a session cookie, not a bearer token." } }Rotating and deleting access tokens
Rotating an account token gives it a new value. The token keeps its label and creation date, and the old value returns 401 immediately, so update every place that uses it right after rotating.
Revoking switches a token off but keeps it in the list. Deleting removes it from the list, whether it is active or revoked.
Router key rotation (POST /v1/api-keys/rotate) also takes effect immediately: every live key on the router stops working.
End-to-end example
List your profiles, create a new one, and read it back:
BASE=https://app-api.browsergateway.com/v1
TOKEN=bga_YOUR_TOKEN_HERE
WS=ws_YOUR_WORKSPACE_ID
curl -s "$BASE/profiles" -H "Authorization: Bearer $TOKEN" -H "X-Workspace-Id: $WS" | jq
curl -s -X POST "$BASE/profiles" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WS" \
-H "Content-Type: application/json" \
-d '{"slug":"acme-checkout","name":"Acme checkout state"}' | jq
curl -s "$BASE/profiles" -H "Authorization: Bearer $TOKEN" -H "X-Workspace-Id: $WS" | jq '.data.items[] | .slug'What's not on this API
- Opening a browser session. Use the Cloud CDP endpoint with a router key, not a bearer.
- One-shot browser actions (screenshot, content, scrape). Those live on the Cloud Data API, and each successful call counts as one session against the 10,000 per month ceiling.
- Self-hosted gateway management. That is
http://your-host:9500/v1/*withBG_TOKEN. See Gateway REST API.