Docs · Interfaces
REST API
The daemon's own HTTP API: create and start profiles, assign proxies, follow events — the surface every other interface sits on.
Base URL and authentication
Everything lives under /v1 on the native API, which binds127.0.0.1:8787 by default. Every /v1 route requires a bearer token; it is compared in constant time, and a missing, malformed or wrong one is a401 carrying error code 4010.
Authorization: Bearer <token>GET /health is the one exception. It sits outside the auth layer and answers {"status":"ok","service":"scalebrowser-api"} without a token, which is what makes it usable as a monitor probe. The token itself comes from your config file, from SCALEBROWSER_BEARER_TOKEN, or fromscalebrowser-daemon --generate-token — seeInstall & run.
Loopback may serve plaintext; anything else must serve TLS. A non-loopback bind without a TLS configuration is refused at startup, so on a remote host the base URL is always https://.
Request and response shape
Bodies are JSON in both directions, and a success is the resource itself — a profile, a list of profiles — never an envelope. Errors are always these two fields:
{ "code": 4005, "message": "preflight failed: …" }code is a stable number for the failures you are expected to handle, andnull for the generic ones — bad input, a conflict, an internal error. The full list is at the end of this page.
Updates are PATCH: partial, with absent fields left unchanged.PUT is routed to the identical handler on every updatable resource, so an older client that sends it keeps working; new code should send PATCH.
Profiles
| Method and path | What it does |
|---|---|
GET /v1/profiles | List profiles, newest first. Filters group, state, q (name substring) and enabled; paging limit (default 100) and offset; ordering sort and order. |
GET /v1/profiles/ids | The same filters, unpaged, ids only → { count, ids }. What a “select all matches” needs. |
POST /v1/profiles | Create one profile → the Profile. |
GET /v1/profiles/:id | Fetch one, with its protection verdict. |
PATCH /v1/profiles/:id | Update name, enabled, geo_mode, expected_country, group_id, proxy_id. |
DELETE /v1/profiles/:id | Delete the row and remove the profile's browser directory. A running browser is stopped first. |
POST /v1/profiles/:id/start | Launch the browser → the CDP endpoint. |
POST /v1/profiles/:id/stop | Stop it → { "stopped": true }. Idempotent. |
POST /v1/profiles/:id/input | Dispatch one humanized gesture: move, click, type or scroll. See Human input. |
/v1/profiles/ids is registered ahead of /v1/profiles/:id, so the literal ids is never read as a profile id. /v1/proxies/checkis handled the same way.
A listed or fetched profile carries a protection object alongside its own fields. That verdict is produced by the same functions the launch runs, soblocked means “this profile will refuse to start” rather than a second opinion. Coherence & proxies explains what is checked.
host_mode and seed are fixed at create time.A PATCH carrying a different host_mode is refused with 400 instead of being quietly dropped: the identity was sampled for that tier, so changing it means creating a new profile. An unchanged echo is accepted, so read-modify-write clients keep working.
The two calls everyone makes first
Create a profile. Only name is required — everything else is drawn from a seed, and an omitted host_mode takes the daemon's default for its own operating system.
$ curl -X POST http://127.0.0.1:8787/v1/profiles \ -H "Authorization: Bearer $SCALEBROWSER_BEARER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "shop-01", "geo_mode": "follow_exit", "proxy_id": "px_7f3a"}'The reply is the created profile. It is long — the identity alone is dozens of fields — so these are the parts you act on:
{"id": "pr_9c21e4", "name": "shop-01", "runtime_state": "stopped", "enabled": true, "host_mode": "linux_engine_spoof", "geo_mode": "follow_exit", "proxy_id": "px_7f3a", "engine_version": "…", "seed": "…", "created_at": 1754300000, "persona": { /* the whole identity — see /coherence */ }}The other accepted create fields are seed (reproduce a known identity),engine_version, host_mode, expected_country andgroup_id. Then start it:
$ curl -X POST http://127.0.0.1:8787/v1/profiles/pr_9c21e4/start \ -H "Authorization: Bearer $SCALEBROWSER_BEARER_TOKEN" \ -H "Content-Type: application/json" \ -d '{"headless": false}'{"profile_id": "pr_9c21e4", "cdp_ws": "ws://127.0.0.1:41337/devtools/browser/2f1c…", "debug_port": 41337, "headless": false, "pid": 48221, "started_at": 1754300112}cdp_ws points at the browser's own DevTools socket on loopback, on a port the engine picks per launch — not at the daemon. That endpoint is how you drive the page yourself, and Direct CDP covers it. The request body is optional: an omitted headless means headless, and false asks for a visible window.
A start is where the pre-launch checks run, so this is the call that returns4002, 4003, 4004, 4005 or4006.
Bulk operations
| Method and path | Body | What it does |
|---|---|---|
POST /v1/bulk/profiles | { preset_id, count, … } | Create N profiles from a preset. All-or-nothing: a mid-batch failure rolls back what it already created. |
POST /v1/bulk/start | { ids, headless? } | Start the batch, one after another. |
POST /v1/bulk/stop | { ids } | Stop each. |
POST /v1/bulk/delete | { ids } | Delete each. |
POST /v1/bulk/assign-proxy | { ids, proxy_id } | Set the proxy on each; null clears it. |
Every bulk call answers with a per-id list of { id, ok, error? }, so one bad id does not fail the batch. bulk/start adds three fields:started (how many browsers came up), remaining (ids never attempted) and stopped_reason when it ended early. headless is one decision for the whole batch.
A bulk start stops at the first capacity refusal. Running out of memory or slots says nothing about the profile that happened to be next, so continuing would report fifty identical failures that read like fifty broken profiles. Look at remaining: those ids were never tried, and retrying them once capacity is free is the right move. Any other per-profile error does not stop the run.
Groups and presets
A group is a flat label a profile belongs to. A preset is a stored operating configuration that POST /v1/bulk/profiles stamps out. Both are plain CRUD:
| Method and path | What it does |
|---|---|
GET /v1/groups · POST /v1/groups | List · create. |
GET|PATCH|DELETE /v1/groups/:id | Fetch · rename · delete. Deleting a group leaves its profiles alone; their group_id becomes null. |
GET /v1/presets · POST /v1/presets | List · create. |
GET|PATCH|DELETE /v1/presets/:id | Fetch · update name, constraints or config · delete. |
Proxies
| Method and path | What it does |
|---|---|
GET /v1/proxies · POST /v1/proxies | List · create. |
GET|PATCH|DELETE /v1/proxies/:id | Fetch · update · delete. Profiles referencing a deleted proxy keep existing, with proxy_id set to null. |
POST /v1/proxies/:id/check | Run the real check through a saved proxy: reachability, exit address and country, and the fingerprint verdict. Stores the result and emits a proxy_checked event. |
POST /v1/proxies/check | The same check on an unsaved configuration — test before you persist. Stores nothing, emits nothing, and adds latency_ms. |
Credentials are write-only. No endpoint returns them, and the type that carries them cannot be serialised at all — so a proxy password cannot leave through a response by accident. An unreachable proxy is reported as healthy: false, not as an error.
Changing the connection identity clears the cached exit. APATCH touching host, port, kind or credentials drops the stored country and health, because the geo gate must not judge a new endpoint by the previous one's exit. Run POST /v1/proxies/:id/check afterwards. Changing only the rotation setting keeps the cache.
A proxy whose stored credentials cannot be decrypted — a database restored without its key — is flagged credentials_unreadable. It stays listable and deletable, and every path that would use it fails closed: an authenticated proxy is never run without its credentials, because that would send the session out over the host's real address.
Extensions
Two levels: a daemon-wide library of packages, and which of them a profile loads.
| Method and path | What it does |
|---|---|
POST /v1/extensions | Upload a package. The body is the raw .crx file, not JSON — the route raises its body limit to 128 MiB for it. |
GET /v1/extensions | The library, newest first. |
GET /v1/extensions/:id · DELETE /v1/extensions/:id | Fetch · remove the package, its files and every profile's assignment of it. |
GET /v1/profiles/:id/extensions | The profile's assigned set, plus the exact switches its next launch will emit. |
POST /v1/profiles/:id/extensions | { ext_ref } — assign. Idempotent. |
DELETE /v1/profiles/:id/extensions | { ext_ref } — unassign. Idempotent. |
ext_ref is a library id, never a path. Note that the unassign call carries it in the request body: an HTTP client that silently drops bodies onDELETE will unassign nothing and report success.
Session bundles
| Method and path | What it does |
|---|---|
POST /v1/profiles/:id/session/export | { password, kinds? } → a password-encrypted bundle of cookies, localStorage, IndexedDB and service-worker state. |
POST /v1/profiles/:id/session/import | { password, bundle } → the bundle is unsealed into the target profile. |
An export that could only capture cookies is flagged as degraded in its own response rather than passed off as complete.
Live detector audit
Drive a running profile through the external detector pages and keep the verdict against the launch it was measured on. A run takes minutes of real network traffic, so it is two calls rather than one:
| Method and path | What it does |
|---|---|
POST /v1/profiles/:id/audit | Start a run and answer immediately. 409 if the profile is not running, or if a run is already in flight for it. |
GET /v1/profiles/:id/audit | State — idle, running or finished — plus the last report. |
Metrics
GET /v1/metrics answers with the resource picture behind the dashboard: the live running count, the configured capacity and budgets, host memory, CPU and GPU figures, and a per-profile breakdown. It is served from a background sampler, so the request path probes nothing and the numbers are at most one sampling interval old.
Each measured value is paired with a probe state of measured,unsupported or unavailable, so “zero” and “we could not read this” never look alike. A separate Prometheus endpoint exists for a deployed daemon; it is off by default and configured under [observability].
Event streams
Two transports, the same events, both behind the bearer token:GET /v1/events is Server-Sent Events with a 15-second keep-alive, andGET /v1/ws is a WebSocket. Both are server-to-client only. Each connection subscribes before its response is produced, so nothing published between your request and the first frame is lost, and a slow client skips events rather than being disconnected.
Each message is one JSON object with a type field:
type | Fields besides at |
|---|---|
profile_state_changed | profile_id, from, to |
profile_started | profile_id, cdp_ws, headless |
profile_stopped | profile_id |
profile_crashed | profile_id, reason |
preflight_failed | profile_id, detail |
proxy_checked | proxy_id, healthy, country |
capacity_rejected | profile_id |
at is a Unix timestamp in seconds. /v1/ws is this event stream and nothing else — it is not a CDP connection, which comes back fromstart.
The MCP endpoint
/v1/mcp is mounted inside the same bearer-auth layer, but it is a nested service rather than a plain route: it speaks Streamable HTTP, which usesPOST, GET and DELETE on that one path.MCP server is the page for it.
GET /v1/mcp/sse answers 410 Gone. The old HTTP+SSE transport was removed rather than left to fail quietly, and the response names both the replacement and the breaking change: over MCP,profile.start no longer returns a cdp_ws. The RESTstart above still does.
Error codes
| Code | HTTP | Meaning, and what to do |
|---|---|---|
4001 | 404 | Not found. The id does not exist — do not retry it. |
4002 | 409 | The proxy or geo check refused the launch: under strict_expected the exit country did not match, or could not be determined. Re-check the proxy, or change geo_mode. |
4003 | 429 | Capacity exceeded. Three limits apply and the strictest wins: configured concurrency, configured memory budget, and the host's measured free memory. Stop something and retry. |
4004 | 503 | No engine for this profile's tier. Install one — engine install. |
4005 | 409 | The pre-launch check refused the start. message names the reason; retrying unchanged fails identically. |
4006 | 409 | Already running. Treat it as success, or stop the profile first. |
4010 | 401 | Unauthorized. The token is missing, malformed or wrong. |
null | 400 · 409 · 500 | Invalid input, a conflict, or an internal error. message is the whole story; only a 500 is worth retrying. |
These numbers are part of the contract and do not change. 4007 is retired and reserved — it belonged to a tier that no longer exists, and nothing returns it.
Next
- Direct CDP — what to do with the
cdp_wsa start hands back. - MCP server — the same daemon, driven by an AI agent.
- AdsPower adapter — the compatibility surface for existing scripts.
- Coherence & proxies — what the checks behind
4002and4005actually verify.