---
title: "Connect to a browser via CDP"
method: POST
path: "/v1/browser/connect"
tags: ["Browser"]
---

# Connect to a browser via CDP

`POST /v1/browser/connect`

## Request

- Base URL: `http://localhost:9998`
- URL: `http://localhost:9998/v1/browser/connect`
- Auth: HTTP bearer

## Request body

- object
  - `channel` 'dev' | 'canary' | 'stable' | 'brave' | 'brave-nightly' | 'chromium': Which browser to attach to. There is deliberately no default: absent means the caller did not choose, which resolves through the user's pinned channel and then auto-detection, so omitting `channel` is not the same request as sending `channel: dev`. A `default` here would be materialised by generated clients and would pin every caller to one browser, which is the hardcoded `.dev` this route was built to remove. An unrecognised value is a `400 invalid_request` listing the valid set. Naming a channel is both a destination and permission to start it: the call connects to that browser or refuses with the reason, and is never quietly served by a different one — including when a session on another browser is already live, which is displaced. Only `dev`, `canary`, `brave-nightly` and `chromium` are started when NO channel is named; `stable` and `brave` are daily drivers, attached to whenever already running and started only when you name or pin them.
  - `page` string: Which tab to attach to: a page id from `GET /v1/browser/pages`, or a substring of its url or title. Absent means the caller did not say, which resolves only when the answer is deterministic — a live attachment, or exactly one open page. Otherwise the route answers `409 target_ambiguous` with the candidate list rather than attaching to an arbitrary tab. A page another browser workspace holds is `409 target_leased`.
  - `workspace` string: The named browser workspace the caller's browser calls run in from this connect on, created on first use. Another caller of the same principal naming it shares its page; the same name under another principal is another workspace. Absent keeps the caller's current workspace.
  - `isolated` boolean: Give the workspace cookies and storage of its own: an isolated browser context and a fresh tab in it. Cannot be combined with `page`, which names a tab in the signed-in profile (`400`).
  - `browserVersion` string: The caller's own browser by the full version Chrome reports to it, in place of `channel`: the one installed browser holding that version on disk is attached to, and none is ever started. No match or several is `409 browser_not_matched`; a matched browser not serving a debug port is `409 browser_not_ready`. Cannot be combined with `channel` or `isolated` (`400`).
  - `windowId` integer: The Chrome window id of the caller's tab: only that window's tabs are candidates, read with `Browser.getWindowForTarget` before any attach. A window with no tab the caller may take is `409 target_not_found`. Cannot be combined with `isolated`.
  - `tabId` integer: The caller's Chrome tab id (`sender.tab.id`), read only from a pinned MediaPiper build's own isolated world, never the page's: the one tab holding it is attached to. With `browserVersion` matching no single browser, the browsers serving a debug port are searched, the connected one first, and none is ever started or switched away from while another workspace holds a page in it (`409 resource_admission_full`). No tab holding it is `409 target_not_found`. Needs `windowId`, so only that window's tabs are read; cannot be combined with `isolated` (`400`).

## Response `200`

Connected with page list

- object
  - `connected` true, required
  - `pages` BrowserPage[], required
    - `id` string, required
    - `url` string, required
    - `title` string, required
    - `lease` 'yours' | 'held': `yours` when the calling workspace holds the page, `held` when another workspace does; absent when no workspace holds it.
    - `holder` BrowserWorkspaceRef: The browser workspace a caller's browser calls run in: one incarnation, never matched by a re-created one. Every browser event carries the ref of the workspace whose page raised it (null for a page no workspace holds).
      - `kind` 'browser', required
      - `handle` string, required
      - `generation` integer, required
  - `activePageId` string: The page this session is attached to, when one is. Absent when connected at browser level with no page. Callers that select a page of their own must skip that step when this is present and still listed in `pages` — re-selecting reverts an explicit page choice.
  - `rung` 'explicit' | 'attached' | 'onlyPage': Which rung of the target ladder chose `activePageId`: `explicit` (the caller named it), `attached` (the session was already on it and it is still open), or `onlyPage` (exactly one tab was open, so there was nothing to disambiguate). Absent when no page was resolved. There is deliberately no fourth value — an arbitrary tab is never chosen, so any other outcome is a `409 target_ambiguous` rather than a rung.
  - `workspace` BrowserWorkspaceRef: The browser workspace a caller's browser calls run in: one incarnation, never matched by a re-created one. Every browser event carries the ref of the workspace whose page raised it (null for a page no workspace holds).
    - `kind` 'browser', required
    - `handle` string, required
    - `generation` integer, required
  - `workspaceName` string: The named workspace this connect attached, when one was named.
  - `isolated` boolean: Present and true when the workspace has storage of its own.

## Other responses

- `400`: Invalid request
- `409`: The browser is reachable but which tab to attach to is not decidable, so nothing was attached. Type `target_ambiguous`. The `message` leads with the remedy (`browser_connect page:<id>`) and then lists every candidate as `id — title — url`, which is what a caller needs to pick without a second round trip. Retrying unchanged loops forever: the tab count does not change between attempts, so re-run with `page` set instead.

---

[API](https://skmtc.dev/modelpiper/apis/toolpiper-api.md) · [All operations](https://skmtc.dev/modelpiper/apis/toolpiper-api/llms.txt) · [OpenAPI document](https://skmtc.dev/modelpiper/apis/toolpiper-api/revisions/6722bf56385c?raw)
