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

# Browser sessions

Claim a dedicated browser for your agent, drive it with batched browser actions in one round trip, or connect Playwright and Puppeteer over CDP.

A session is one whole browser process, yours alone, claimed from a warm pool and ready to drive. Use a session when your agent needs to click through an app, fill forms, or keep state across many steps. For a single page, a [scrape](/vision/scrape) with `actions` is cheaper.

## Create a session

**curl**

```bash
    curl -s https://beta-api.b3os.org/v1/vision/sessions \
      -H "Authorization: Bearer $B3OS_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"viewport": {"width": 1440, "height": 900}, "timeout_s": 600}'
    ```
  **TypeScript**

```ts
    import { B3OS } from "@b3os/sdk";

    const b3os = new B3OS();
    const session = await b3os.vision.createSession({ viewport: { width: 1440, height: 900 }, timeout_s: 600 });
    console.log(session.id, session.cdp_url);
    ```
  **Python**

```python
    from b3os import B3OS

    b3os = B3OS()
    session = b3os.vision.create_session({"viewport": {"width": 1440, "height": 900}, "timeout_s": 600})
    print(session["id"], session["cdp_url"])
    ```
  **MCP**

```text
    Open https://app.example.com/login in a browser, keep it open, and tell me what the page asks for.
    ```

    The agent calls `vision_act` with `url` and `keep_session: true`. Without `keep_session` the tool closes the session it created when the call ends; with it, the result carries the session ID for the next call. `vision_act` is on the local MCP server only, and it takes no profile: to start signed in, create the session from code with `profile` and give the agent its ID.

The session is `active` until you close it or `timeout_s` passes (300 seconds by default, at most 1,800). It bills `vision.browser_ms` from claim to close. Other fields: `profile` to start signed in, `persist_profile` to save cookies back when it closes, `locale`, `timezone`, `user_agent`, `block_ads` and `block_resources`.

## Drive it with batched actions

`POST /v1/vision/sessions/{session_id}/act` runs a list of browser actions in order on one page and returns every result in one round trip, so an agent pays one call for a whole sequence:

**curl**

```bash
    curl -s "https://beta-api.b3os.org/v1/vision/sessions/$SESSION_ID/act" \
      -H "Authorization: Bearer $B3OS_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"actions": [
            {"kind": "navigate", "url": "https://app.example.com/login"},
            {"kind": "type", "target": {"selector": "#email"}, "text": "agent@example.com"},
            {"kind": "click", "target": {"role": "button", "name": "Continue"}},
            {"kind": "wait", "for": {"selector": "[data-test=\"dashboard\"]"}},
            {"kind": "markdown"}
          ]}'
    ```
  **TypeScript**

```ts
    const result = await b3os.vision.act(session.id, {
      actions: [
        { kind: "navigate", url: "https://app.example.com/login" },
        { kind: "type", target: { selector: "#email" }, text: "agent@example.com" },
        { kind: "click", target: { role: "button", name: "Continue" } },
        { kind: "wait", for: { selector: '[data-test="dashboard"]' } },
        { kind: "markdown" },
      ],
    });
    console.log(result.ok, result.results.at(-1)?.markdown);
    ```
  **Python**

```python
    result = b3os.vision.act(session["id"], {"actions": [
        {"kind": "navigate", "url": "https://app.example.com/login"},
        {"kind": "type", "target": {"selector": "#email"}, "text": "agent@example.com"},
        {"kind": "click", "target": {"role": "button", "name": "Continue"}},
        {"kind": "wait", "for": {"selector": '[data-test="dashboard"]'}},
        {"kind": "markdown"},
    ]})
    print(result["ok"], result["results"][-1].get("markdown"))
    ```
  **MCP**

```text
    In the open session, log in with agent@example.com, wait for the dashboard and read it back to me.
    ```

    The agent calls `vision_act` with the session's `session_id` and the steps, which run as one batch.

Each result has its `kind`, `status` (`ok`, `failed` or `skipped`) and `duration_ms`, plus what the action produced: the URL and status code for `navigate`, the value for `evaluate`, the image for `screenshot`, the page for `markdown`. Execution stops at the first failed action unless `continue_on_error` is set; later actions are reported as `skipped`, and the failed one carries its error, such as [`target_not_found`](/errors#target_not_found). A batch holds up to 100 actions and must finish within `timeout_ms` (30 seconds by default, 60 at most). Set `target_id` to act on a page other than the most recently active one; `GET` on the session lists its pages.

### Browser actions

| Kind | Fields | Does |
|---|---|---|
| `navigate` | `url`, `wait_until` (`load`, `domcontentloaded`, `networkidle`) | Load a URL |
| `click` | `target`, `button`, `count`, `modifiers` | Click an element |
| `hover` | `target` | Move the pointer over an element |
| `type` | `text`, `target`, `clear`, `interval_ms` | Type into the focused or targeted field |
| `key` | `key` (such as `Enter`), `modifiers` | Press a key |
| `scroll` | `to`, `by` or `target` | Scroll to a point, by an amount, or until the target is in view |
| `select` | `target`, `value` | Choose an option in a select |
| `wait` | `ms`, or `for` one of `selector`, `text`, `url`, `network_idle` | Wait a fixed time or for a condition |
| `evaluate` | `expression` | Run JavaScript in the page and return its JSON result |
| `screenshot` | `target`, `full_page`, `format`, `quality` | Capture the page or an element |
| `markdown` | `only_main_content` | Return the page as Markdown |

A `target` matches by CSS `selector`, visible `text` (`exact` for a whole match) or ARIA `role` plus accessible `name`, optionally `within` a scope; `nth` picks one of several matches. Actions that wait for a target take `timeout_ms` (10 seconds by default). Render scripts use the same kinds and fields, except `evaluate`, `screenshot` and `markdown`.

## Connect over CDP

Every session has a `cdp_url`, a WebSocket that speaks the Chrome DevTools Protocol for that session only. Point Playwright or Puppeteer at it with your key in the `Authorization` header:

**Playwright**

```ts
    import { chromium } from "playwright";

    const browser = await chromium.connectOverCDP(session.cdp_url, {
      headers: { Authorization: `Bearer ${process.env.B3OS_API_KEY}` },
    });
    const page = browser.contexts()[0].pages()[0] ?? (await browser.contexts()[0].newPage());
    await page.goto("https://example.com");
    console.log(await page.title());
    ```
  **Puppeteer**

```ts
    import puppeteer from "puppeteer-core";

    const browser = await puppeteer.connect({
      browserWSEndpoint: session.cdp_url,
      headers: { Authorization: `Bearer ${process.env.B3OS_API_KEY}` },
    });
    const [page] = await browser.pages();
    await page.goto("https://example.com");
    console.log(await page.title());
    ```

The CDP proxy exposes only the session's own pages. It refuses `SystemInfo.*`, `Tethering.*`, `Extensions.*` and `Cast.*`, and every `Browser.*` method except the ones Playwright and Puppeteer need to drive a browser (`Browser.getVersion`, the permission methods, window bounds, `Browser.setDownloadBehavior` and `Browser.cancelDownload`). It also refuses other sessions' targets, host file access (`DOM.setFileInputFiles` and file drags), downloads to host paths (downloads land in the session's own directory, wiped with the browser), and navigation to anything but `http:`, `https:` and `about:blank`. A refused call gets a CDP error with code [`cdp_method_blocked`](/errors#cdp_method_blocked). Creating a browser context with its own proxy (`proxyServer`, `proxyBypassList` or `originsWithUniversalNetworkAccess`, which Playwright's `newContext({proxy})` sends) is refused with `forbidden`: all of the session's traffic goes through its own egress route. A client that falls more than a few megabytes behind the stream is disconnected.

From a browser page, where you cannot set headers, get a single-use [stream ticket](/concepts/auth#stream-tickets) with `POST /v1/vision/sessions/{session_id}/ticket` and open the socket within 30 seconds with `?ticket=tkt_...`.

## Close the session

```bash
curl -s -X DELETE "https://beta-api.b3os.org/v1/vision/sessions/$SESSION_ID" \
  -H "Authorization: Bearer $B3OS_API_KEY"
```

Closing destroys the browser and returns the session with its final `usage.browser_ms`. With `persist_profile`, cookies and storage are saved to the profile first. Closing a closed session returns it unchanged. Sessions that are closed or expired stay listed for 7 days; calling `act` on one fails with [`session_closed`](/errors#session_closed).