RPC Wire Contract
The NDJSON /rpc action API for browser sandboxes - frame shape, auth, response shapes, and the confirmed action catalog.
Alongside /cdp, every browser sandbox exposes a lightweight NDJSON action API over a single WebSocket. It drives the browser with high-level verbs (page.navigate, page.snapshot, page.evaluate, …) and needs no Playwright. The SDK's RpcClient wraps this contract; this page documents the raw wire format.
Endpoint
wss://{sandboxID}.{domain}/ndjsonOpen the WebSocket against the bare {sandboxID} subdomain with the literal path /ndjson. The control-plane proxy rewrites that exact path to the in-sandbox daemon's RPC route on the same passthrough listener (port 7879) that serves /cdp:
/ndjson -> /session/{sandboxID}/rpc (port 7879)You receive the ndjsonEndpoint field on the browser-sandbox record from POST /browser-sandboxes and GET /browser-sandboxes/{sandboxID}. The SDK also derives it from a wsEndpoint by swapping the trailing /cdp for /ndjson.
Authentication
Identical to /cdp: the bearer token is carried as a WebSocket subprotocol on the upgrade.
Sec-WebSocket-Protocol: bearer.<accessToken>The daemon echoes the subprotocol back on the 101 upgrade. Token-in-URL is rejected; a tokenless upgrade returns 401 when traffic-token enforcement is on.
Frame shape
The transport is NDJSON - exactly one JSON object per WebSocket text frame (not batched). A request looks like:
{ "id": "1", "action": "page.navigate", "params": { "url": "https://example.com" }, "session": "{sandboxID}" }| Field | Required | Notes |
|---|---|---|
id | yes | Correlation id. Must be a string - an integer id is rejected by the daemon. |
action | yes | The verb. The wire field is action, not method - a method-keyed frame is rejected with bad request: missing field \action``. |
params | per-action | Action arguments. Omit when the action takes none. |
session | no | The session to bind to. Defaults to the sole session (the daemon auto-binds it under --concurrent 1); pass the sandbox ID explicitly for safety. |
Two easy mistakes the daemon rejects: a numeric id, and using method instead of action.
Response shapes
A success echoes the request id and nests all result fields under data:
{ "id": "1", "success": true, "data": { "page_id": "page-1", "url": "https://example.com/" } }There are two error shapes:
-
Application error (e.g. an unknown action) - echoes the request
id:{ "id": "1", "success": false, "error": "<message>" } -
Protocol / parse error (e.g. a frame missing
action) - carries an emptyid, so it cannot be correlated to a specific request:{ "id": "", "success": false, "error": "<message>" }The SDK's
RpcClientfails the oldest in-flight request (FIFO) on an id-lesssuccess:falseframe, so a malformed request fails promptly instead of hitting the request timeout.
Action catalog
The in-sandbox daemon owns the full action vocabulary (tab.*, session.*, page.network.*, and more). The actions confirmed and wrapped by the SDK are:
| Action | params | Success data |
|---|---|---|
page.navigate | { url } | { page_id, url } |
page.snapshot | none | { tree_yaml, title, element_count, page_id, url } |
page.evaluate | { expression } | { value, page_id } |
page.snapshot returns an accessibility-tree YAML (tree_yaml) plus the page title and element count - a compact, token-efficient page representation well suited to agents. page.evaluate runs JavaScript in the page and returns the result under data.value.
For any action not in the table above, send the raw frame and read the whole response envelope - its data carries the result. The SDK's RpcClient.send(action, params) does exactly that.
Minimal raw client
import WebSocket from 'ws';
const ws = new WebSocket(ndjsonEndpoint, [`bearer.${accessToken}`]);
await new Promise((res) => ws.on('open', res));
ws.send(JSON.stringify({
id: '1',
action: 'page.navigate',
params: { url: 'https://example.com' },
session: sandboxId,
}));
ws.on('message', (data) => {
const frame = JSON.parse(data.toString());
// { id: '1', success: true, data: { page_id, url } }
});Prefer the RpcClient for production use - it handles frame reassembly, id correlation, timeouts, and the error shapes above.
See also
- Browser SDK -
RpcClient- the wrapped client. - CDP Wire Contract - the full DevTools Protocol on the same sandbox.