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. ForconnectOverCDP, PuppeteerbrowserWSEndpoint, chromedp, or your own socket.playwright_url:wss://back.tilion.dev/v1/session/{id}/playwright?token=..., a native Playwright server endpoint forchromium.connect(...).
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 theconnect_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 sendBrowser.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
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.
Node: connectOverCDP
Withplaywright-core (no local browser needed):
Raw CDP for other clients
connect_url is an ordinary CDP browser endpoint. One frame in, one out:
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_urlis 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=observeror 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 withPOST /v1/fetch, preferably withasync_mode: true. For a challenge on the current page,POST /v1/solvecan 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: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:
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.