BrowserGateway

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:

SurfaceBase URLAuthPurpose
Cloud REST API (this page)https://app-api.browsergateway.com/v1Authorization: Bearer bga_...Control plane: workspaces, routers, providers, profiles, sessions, billing
Cloud CDP endpointwss://cdp.browsergateway.io/v1/connect?token=bg_...Router keyOpen a browser session over CDP (Puppeteer, Playwright)
Cloud Data APIhttps://cdp.browsergateway.io/v1/*Router keyOne-shot screenshot, content and scrape
Self-hosted gateway RESThttp://your-host:9500/v1/*BG_TOKENThe 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

HeaderEffect
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

CodeHTTPMeaning
validation_error400Request body or parameters failed validation. Some endpoints use bad_request, invalid_input, invalid_url or invalid_id.
unauthenticated401Missing or invalid bearer
fair_use_limit_reached402A fair-use ceiling was hit: 100 routers, 500 profiles or 100 router keys per workspace, or 1,000 members
cookie_required403Endpoint refuses bearers (see below)
forbidden403Your role in the workspace does not allow this action
not_a_member403X-Workspace-Id names a workspace you do not belong to
not_found404Resource missing or in another workspace
conflict409A router, provider or profile with that slug already exists
last_router409A workspace must keep at least one router
limit_reached409You already have 10 active access tokens
payload_too_large413A profile is larger than 10 MB
profile_storage_cap_exceeded413Saving the profile would pass the workspace's 500 MB profile storage
rate_limited429Daily bearer cap reached (see Rate limits)
provider_calibrating503A 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 stateDaily cap
Wallet balance of zero or below5,000
Wallet balance above zero100,000
Enterprise100,000

The count resets at midnight UTC. Counted responses carry:

X-RateLimit-Limit: 100000
X-RateLimit-Remaining: 99871
X-RateLimit-Reset: 1735689600

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

EndpointWhoWhat it does
GET /v1/invitationsAny memberPending invitations: id, email, role, expiresAt, createdAt, invitedByName
POST /v1/invitationsOwner or admin, cookie session onlyInvite { "email": "...", "role": "admin" | "member" }. Sends the email.
POST /v1/invitations/:id/resendOwner or adminNew link, new 7-day expiry, email sent again
DELETE /v1/invitations/:idOwner or adminCancel a pending invitation
GET /v1/invitations/:token/lookupAnyone with the linkWorkspace name, inviter, role, expiry
POST /v1/invitations/:token/acceptThe invited user, signed inJoin the workspace
POST /v1/invitations/:token/declineThe invited user, signed inDecline

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:

FieldRangeDefault
name, descriptiontext
strategypriority-chain, round-robin, least-connections, latency-optimized, weightedround-robin
connectionTimeoutMs1,000 to 30,00015,000
sessionIdleTimeoutMs10,000 to 840,000300,000
queueMaxSize0 to 10,00050
queueTimeoutMs0 to 86,400,00030,000
healthCheckIntervalMs5,000 to 3,600,00030,000
cooldownDefaultMs0 to 86,400,00030,000
cooldownFailureThreshold0 to 10.5
cooldownMinRequestVolume0 to 1,000,0003

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
}
FieldRules
slugRequired. Letters, digits and single hyphens, up to 64 characters, unique on the router.
urlA ws:// or wss:// address.
headersOptional. Sent on every connection to this provider, up to 20.
priority, weight1 to 1000. Both default to 100.
maxConcurrent1 to 1000. Omit for no limit on our side. A browserserve provider's advertised capacity is used when you omit it.
profileOptional 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.

adapterKindProvideroptionsWhen a session ends
kernelKernelregion (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-useBrowser UsetimeoutMinutes (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.

The chat link is the credential chat apps use to reach Cloud MCP. One per router. ?routerInstanceId=<id> selects the router.

EndpointWhat it does
GET /v1/mcp-links/currentThe current link: { "id", "routerInstanceId", "token", "lastUsedAt", "createdAt" }, or null when there is none
POST /v1/mcp-links/rotateReplace the link. The old one stops working immediately. Response 201.
DELETE /v1/mcp-links/currentRemove 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.

QueryDefaultNotes
page1
pageSize25Up to 100
stateallactive, closed or errored
providerallProvider slug on the request's router
since30 days agoUnix milliseconds. Up to 180 days back.
kindallroute for CDP connections, agent for Cloud MCP browsers
routerInstanceIdall routersLimit 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=json or ?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

EventSent when
session.endedA CDP or agent session closes normally
session.errorA 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.

QueryDefaultNotes
limit90Up to 400 rows
months31 to 12
kindallsession, recording, observability, byok_overage, reseller, topup, refund, grant
sinceDaynoneYYYY-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.

These endpoints refuse bearer tokens and need a signed-in dashboard session:

  • POST /v1/access-tokens: create an account token
  • POST /v1/access-tokens/:id/rotate: rotate an account token
  • POST /v1/access-tokens/:id/revoke: revoke an account token
  • DELETE /v1/access-tokens/:id: delete an account token
  • DELETE /v1/workspaces/current: delete the workspace
  • DELETE /v1/workspaces/current/members/:userId: remove a member
  • POST /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/* with BG_TOKEN. See Gateway REST API.

On this page

What it isGet a tokenAuthChoosing the workspace and routerCommon response shapeError codesRate limitsEndpointsUser and workspacesGET /v1/meGET /v1/workspacesPOST /v1/workspaces/activeGET /v1/workspaces/current/membersPOST /v1/workspaces/current/leaveDELETE /v1/workspaces/current/members/:userIdDELETE /v1/workspaces/currentInvitationsRoutersGET /v1/routersPOST /v1/routersPATCH /v1/routers/:idDELETE /v1/routers/:idGET /v1/routers/:id/settingsGET /v1/routers/:id/agent-settingsPOST /v1/routers/activeProvidersGET /v1/providersGET /v1/providers/:slug/plaintextPOST /v1/providersPATCH /v1/providers/:slugDELETE /v1/providers/:slugPOST /v1/providers/:slug/testPOST /v1/providers/:slug/reprobePOST /v1/providers/test-url and POST /v1/providers/probeRouter keysGET /v1/api-keysPOST /v1/api-keysPOST /v1/api-keys/rotateDELETE /v1/api-keys/:idGET /v1/api-keys/current/plaintextChat linksProfilesGET /v1/profilesPOST /v1/profilesPOST /v1/profiles/import-playwrightGET /v1/profiles/:slug/exportDELETE /v1/profiles/:slugSessionsGET /v1/sessionsGET /v1/sessions/:idPOST /v1/sessions/:id/closeRecordingsGET /v1/replaysPOST /v1/replays/:id/exportGET /v1/replays/:id/export/downloadFramesDELETE /v1/replays/:idTracesGET /v1/inspectionsGET /v1/inspections/:id/eventsDELETE /v1/inspections/:idWebhooksGET /v1/webhooksPOST /v1/webhooksEventsPATCH /v1/webhooks/:idDELETE /v1/webhooks/:idPOST /v1/webhooks/:id/testBillingGET /v1/billing/summaryGET /v1/billing/usageGET /v1/billing/ledgerGET /v1/billing/transactionsPOST /v1/billing/checkoutAccess tokensGET /v1/access-tokensAccountDELETE /v1/accountCookie-only endpointsRotating and deleting access tokensEnd-to-end exampleWhat's not on this API