---
title: "Direct CDP"
description: "Drive a Scalebrowser profile over the Chrome DevTools Protocol: getting the WebSocket URL, what the daemon hands back, and the Python and Node SDKs built on it."
canonical: "https://scalebrowser.net/docs/cdp"
---

> ## Documentation Index
> Fetch the complete documentation index at: https://scalebrowser.net/llms.txt
> Use this file to discover all available pages before exploring further.

# Direct CDP

> Drive a Scalebrowser profile over the Chrome DevTools Protocol: getting the WebSocket URL, what the daemon hands back, and the Python and Node SDKs built on it.


Start a profile, take its WebSocket URL, and drive the browser over the Chrome DevTools Protocol, with the Python and Node SDKs or with your own client.

<Note>

**You get a socket, not a driver.** The daemon starts the browser and hands back Chromium's own browser-level DevTools endpoint. Everything above that (targets, navigation, evaluation) is plain CDP, so any client that speaks the protocol works. Nothing is wrapped, and nothing is injected into the page.

</Note>

## Get a connection
One call. `POST /v1/profiles/<id>/start` launches the profile and answers with its endpoint. The body is optional, and the only field it reads is `headless`. Every `/v1` call carries the daemon's bearer token in an `Authorization` header. The daemon prints it on first start, or mints one with `--generate-token`; see [Install & run](/docs/install#the-api-token).

```bash
$ curl -sX POST http://127.0.0.1:8787/v1/profiles/$ID/start \
    -H "Authorization: Bearer $SCALEBROWSER_BEARER_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"headless": true}'
```

```json
{
  "profile_id": "prf_7d2a…",
  "cdp_ws": "ws://127.0.0.1:51234/devtools/browser/8f3c…",
  "debug_port": 51234,
  "headless": true,
  "pid": 48122,
  "started_at": 1754352019
}
```

| Field        | What it is                                                                                                                                         |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cdp_ws`     | The browser-level DevTools WebSocket. This is the one you connect to.                                                                              |
| `debug_port` | The TCP port parsed out of that URL, or `null` if it has none. Convenient for tools that want a port rather than a URL.                           |
| `headless`   | What this launch actually did. Omit `headless` from the request and you get a headless browser: an unattended start is the assumption on this API. |
| `pid`        | The browser process id, when the backend knows one.                                                                                                |
| `started_at` | Unix seconds.                                                                                                                                      |

Stopping is `POST /v1/profiles/<id>/stop`, and it is idempotent: stopping a profile that is not running is a success, not an error. Do stop it: one left running holds a capacity slot and its share of RAM. The full request and response shapes are on [REST API](/docs/rest).

### Where that endpoint lives
On the daemon's own machine, on loopback, on a port Chromium picks fresh at every launch. The daemon starts the engine with `--remote-debugging-port=0` and `--remote-debugging-address=127.0.0.1`, then reads back the port the browser chose. Three consequences worth knowing before you build around it:

- **The URL changes every launch.** Never cache it across a stop and a start. Read it from the start response each time.
- **It is reachable only from the daemon's host.** If the daemon runs on a server and your code does not, forward the port; an SSH tunnel is enough. The REST API can be reached over TLS from anywhere, this socket cannot.
- **It carries no token of its own.** As with Chromium's DevTools port everywhere else, whatever can reach the socket can drive the browser. That is exactly why it stays on loopback: the bearer token protects `/v1`, not this.

## Attach to a page
The endpoint is _browser-level_: it can list and create targets, but `Page.navigate` needs a session bound to a page. Three commands get you there, and they are the ones both SDKs send:

1. `Target.getTargets`, then take the first entry whose `type` is `"page"`.
2. `Target.createTarget` with `{"url": "about:blank"}` if there is none.
3. `Target.attachToTarget` with `{"targetId": …, "flatten": true}`. The `sessionId` it returns goes on every later frame.

```python
import asyncio, json, websockets   # no SDK, one dependency

CDP_WS = "ws://127.0.0.1:51234/devtools/browser/8f3c…"

async def main():
    async with websockets.connect(CDP_WS, max_size=None) as ws:
        n = 0

        async def send(method, params=None, session=None):
            nonlocal n
            n += 1
            frame = {"id": n, "method": method, "params": params or {}}
            if session:
                frame["sessionId"] = session
            await ws.send(json.dumps(frame))
            while True:                        # events share the socket
                msg = json.loads(await ws.recv())
                if msg.get("id") == n:
                    return msg["result"]

        targets = await send("Target.getTargets")
        page = next(t for t in targets["targetInfos"] if t["type"] == "page")
        s = (await send("Target.attachToTarget",
                        {"targetId": page["targetId"], "flatten": True}))["sessionId"]

        await send("Page.navigate", {"url": "https://example.com"}, s)
        title = await send("Runtime.evaluate",
                           {"expression": "document.title", "returnByValue": True}, s)
        print(title["result"]["value"])

asyncio.run(main())
```

<Warning>

**Your WebSocket client must not send an `Origin` header.** Chromium's DevTools endpoint refuses that handshake, and the failure reads like a plain connection error. Node's built-in global `WebSocket` sends one, so choose a client that lets you leave it off, or build the handshake yourself.

</Warning>

One protocol habit is worth copying from the SDKs: they never call `Runtime.enable`. It is not needed, because `Runtime.evaluate` works without it, and enabling it is observable from inside the page. For code that should not be visible to the page at all, open an isolated world with `Page.createIsolatedWorld` and pass the returned `executionContextId` to `Runtime.evaluate` as `contextId`.

## Why direct CDP and not Playwright
Because the automation framework is itself a signal. Anti-bot stacks fingerprint the shape of the control plane that a Playwright or Puppeteer client presents, and they block on it _regardless of how good the browser's fingerprint is_. A perfectly coherent profile driven through a Playwright control plane still fails those gates, and no amount of work on the browser fixes it, because the tell is not in the browser.

So the recommended path speaks CDP directly, the way `nodriver`-style tools do, and both SDKs carry their own small CDP client instead of wrapping someone else's. Two things follow for you:

- **You can still point Playwright at `cdp_ws`.** It is a normal DevTools socket and nothing stops you. You simply give up the argument this product makes. Treat it as a way to reuse existing code against forgiving targets, not as the way to reach sharp ones.
- **There is no chromedriver.** None is shipped, for any platform, so a Selenium script has to bring its own. The [AdsPower adapter](/docs/adspower) returns an empty `webdriver` field for this exact reason.

## Human input goes through the daemon
Dispatching `Input.dispatchMouseEvent` yourself works, and it moves the pointer in a straight line at an inhuman speed with a machine-perfect cadence. The daemon has a second endpoint for exactly that reason: `POST /v1/profiles/<id>/input` builds the gesture (approach curve, overshoot, per-key typing rhythm, momentum scrolling) and dispatches it over the profile's CDP socket for you.

```bash
$ curl -sX POST http://127.0.0.1:8787/v1/profiles/$ID/input \
    -H "Authorization: Bearer $SCALEBROWSER_BEARER_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"action": "click", "x": 120, "y": 240, "width": 88}'
```

| Action   | Fields                                              |
| -------- | --------------------------------------------------- |
| `move`   | `x`, `y`, optional `width`                          |
| `click`  | `x`, `y`, optional `button`, `click_count`, `width` |
| `type`   | `text`                                              |
| `scroll` | `x`, `y`, optional `delta_x`, `delta_y`             |

`width` is the width of the thing you are aiming at: a button's bounding box, not a point. A real pointer lands somewhere inside its target and takes longer to reach a small one; passing the width is what keeps that true. Adding `"humanize": false` to any of the four gives you the minimal direct sequence instead; it defaults to `true`.

The response acknowledges the gesture and reports whether it reached the browser: `dispatched`, plus a `detail` when it did not (a stopped profile, an unreachable socket, no page target). What the timings are modelled on, and why added jitter is a tell rather than a disguise, is on [Human input](/docs/behaviour).

## The SDKs

There is a Python client and a Node/TypeScript one, and each is a typed REST client plus exactly the direct-CDP driver this page describes: the target attach, the frame bookkeeping and the humanized input are already wired up. Both install from the public registries, and both are MIT-licensed.

<CodeGroup>
```bash Python
pip install scalebrowser
```

```bash Node
npm install @scalebrowser/sdk
```
</CodeGroup>

Full surface, examples and error handling on [SDKs](/docs/sdks).

## Next
- [REST API](/docs/rest): creating profiles, proxies and groups, bulk operations, error codes.
- [Human input](/docs/behaviour): what the humanized gestures are modelled on.
- [MCP server](/docs/agents/mcp-server): if an AI agent is the thing driving, that is the better interface: it deliberately never hands out a CDP endpoint, so every input stays on the humanized path.
- [Coherence & proxies](/docs/coherence): what is checked before a launch is allowed at all.
