Skip to main content
Connect to a session using a Chrome DevTools Protocol client such as Playwright, Puppeteer, or chromedp. The connection grants browser control, including access to tabs, cookies, and script evaluation. Connection URLs contain short-lived session tokens. Use CDP for interactive automation that needs the full browser API. For protected-page reads, POST /v1/fetch provides recovery and a typed content outcome. Main-client navigation receives automatic recovery when cdp_observe: true, the default. Setting it to false disables that behavior. Observer commands pass directly to the browser. Recovery never switches exits on a custom-proxy session. Call fetch before attaching the main client, then attach to work with the page it opened.

The URLs and the token

POST /v1/session returns two URLs; GET /v1/session/{id}/connect mints fresh ones for a live session.
  • connect_url: wss://back.tilion.dev/v1/session/{id}/cdp?token=..., a browser-level CDP endpoint on port 443. For connectOverCDP, Puppeteer browserWSEndpoint, chromedp, or your own socket.
  • playwright_url: wss://back.tilion.dev/v1/session/{id}/playwright?token=..., a native Playwright server endpoint for chromium.connect(...).
The token is the same in both, is bound to this one session and your account, and lasts connect_expires_in seconds (1800). It is not single use. A bad or expired token is refused at the WebSocket handshake with HTTP 403 and the body {"detail": {"error": "invalid_token", "message": "missing, expired or invalid token for this session; mint a new one"}}; a handshake for an ended session is refused the same way with the session’s 410 or 404 answer. Mint a new URL with /connect whenever a process might have waited more than half an hour. The session lifetime is independent of the token: idle_timeout is measured from the last drive or CDP activity, and ttl is the absolute lifetime. The accepted ttl range is 5 to 604800 seconds, subject to the plan cap. Playwright activity resets the idle clock. Playwright’s browser.close() on a CDP-connected browser disconnects that client; the session keeps running and billing until DELETE /v1/session/{id} or its deadline. The raw CDP command Browser.close has different behavior, described below.

Multiple CDP clients

A session supports up to three CDP clients when the feature is enabled for the account: one main client and two observers. These roles apply to the connect_url CDP endpoint. Clients can connect and disconnect in any order. An observer can connect before the main client and remain connected after the main client leaves. All clients share the same browser. Observers can change page state; coordinate commands to avoid interfering with another client’s actions. Custom-proxy sessions keep their proxy exit during recovery. Disconnect the main client before driving over REST. Observers can remain connected. A native Playwright client or VNC client also holds the session; native Playwright connections return close code 4409 when the browser is busy. For raw CDP observers, close the WebSocket to disconnect. For Playwright clients attached over CDP, browser.close() disconnects the client without sending the raw Browser.close command. See Connect multiple CDP clients for Python and Node.js examples.

Observer protocol restrictions

Observers cannot send Browser.close, Browser.crash, or Browser.crashGpuProcess, regardless of how the method name is written. They also cannot use Target.sendMessageToTarget or Target.exposeDevToolsProtocol, which can carry another command such as Browser.close. Address targets with sessionId instead, as Playwright and Puppeteer already do. Each observer WebSocket frame must be text containing one JSON object with no repeated keys. Binary frames, other JSON values, multiple objects in one frame, and repeated keys are refused with CDP error code -32700.

Connection close codes

Python: connect_over_cdp

Default context or a new one. A fresh session has exactly one context with one page. browser.contexts[0] is the session: the cookies /save-auth and /context work with, the persona, the exit, the page the live view shows. browser.new_context() gives a second context with an empty cookie jar and storage, isolated from the first but in the same browser and on the same exit; it costs a renderer. Use the default context unless you need isolation inside one session.

Python: native Playwright connect

playwright_url is a Playwright server endpoint, so chromium.connect works too. It reports no existing context (contexts=0), so create one; it lives in the same browser.
The native endpoint matches a gateway to your client’s Playwright version; a version it does not have is refused with close code 1008 and a reason naming the supported range.

Node: connectOverCDP

With playwright-core (no local browser needed):

Raw CDP for other clients

connect_url is an ordinary CDP browser endpoint. One frame in, one out:
Puppeteer attaches with puppeteer.connect({ browserWSEndpoint: connectUrl }) and chromedp with the URL as the remote allocator. Two things the socket enforces: a Page.navigate or Target.createTarget to a private, loopback or link-local address is refused (the command gets an error reply), and a vanilla client’s Runtime.enable is an automation tell; if that matters for your target, use a stealth-configured client or drive the session over REST. CDP traffic is billed as the session’s egress. Troubleshooting:
  • connect_url is null on create: raw CDP is not enabled for your key. Retrying will not change it.
  • Handshake refused with HTTP 403 invalid_token: the token expired (1800 s) or belongs to another session. Mint one with /connect.
  • Handshake refused with HTTP 410 or 404: the session ended or never existed. Create a new one.
  • Socket accepted then closed with 4409: a main client already holds the session. Use &role=observer or wait for it to disconnect.
  • Socket closed with 4429: both observer slots are occupied.
  • Socket closed with 4403: multiple clients per session are not enabled for the account.
  • Socket closed with 4410 the session's browser crashed: create a new session.
  • Socket closed with 1011: the session has no live browser.
  • page.goto() remains on a challenge or block page: disconnect the main client and read the page with POST /v1/fetch, preferably with async_mode: true. For a challenge on the current page, POST /v1/solve can attempt clearance after the main client disconnects. Observers can remain connected.

POST /v1/session/{session_id}/tunnel

Allow one session to reach selected localhost ports on the machine running your tunnel client. Auth: Bearer API key. Request:
Use 1 to 16 ports, each from 1 to 65535. The response contains session_id, ports, expires_in (1800 seconds), and a tokenized tunnel_url WebSocket URL. Open that URL from the machine hosting the local service; minting it alone does not forward traffic. The tunnel client receives a text frame {"type": "ready", "session_id": "...", "ports": [3000, 5173]}. Subsequent messages are binary: one byte message type, four-byte stream id (big endian), then payload. Reply to OPEN with OPEN_OK or ERR within 10 seconds. Up to 64 streams can be open per tunnel. Only the listed ports are allowed. A second tunnel socket for the session replaces the first; the tunnel ends when the session ends. With the tunnel socket open, create a new browser context over CDP or Playwright:
The default browser context and REST navigation routes cannot reach tunnelled ports. Those REST requests answer 409 tunnel_needs_browser_context. Other private addresses remain refused. Errors: 404 for an unknown session or unavailable tunnel support; 409 when the session cannot support a tunnel; 422 validation list for an invalid port list; 503 when a tunnel URL cannot be minted. Invalid or altered tunnel tokens are refused at the WebSocket handshake.