> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.chaser.sh/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.chaser.sh/_mcp/server.

# Playwright / Patchright

Connect to Chaser sessions with patchright, a drop-in Playwright replacement that patches detectable automation artifacts in Chromium.

## Why patchright?

The Chromium bundled with stock Playwright ships with properties that anti-bot systems check:

- `navigator.webdriver === true`
- Missing `chrome.runtime`
- `window.chrome` inconsistencies
- Headless-specific user-agent tokens

Patchright patches these at the browser level. The API is identical to Playwright — same imports, same types, same methods. Swap the import and the session looks like an unmodified user browser.

```diff
- import { chromium } from "playwright";
+ import { chromium } from "patchright";
```

## Installation

```bash
npm i patchright
```

Python:

```bash
pip install patchright
```

## Basic connection

```typescript
import { chromium } from "patchright";
import { writeFileSync } from "fs";

// 1. Create a session
const { cdp_url, id } = await fetch(
  "https://api.chaser.sh/v1/sessions",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.CHASER_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ ttl_seconds: 1800 }),
  }
).then((r) => r.json());

// 2. Connect
const browser = await chromium.connectOverCDP(cdp_url);
const context = browser.contexts()[0];
const page = context.pages()[0] ?? await context.newPage();

// 3. Automate
await page.goto("https://www.cloudflare.com");
console.log(await page.title());

// 4. Screenshot via the API (not page.screenshot())
const png = await fetch(
  `https://api.chaser.sh/v1/sessions/${id}/screenshot`,
  {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.CHASER_KEY}` },
  }
).then((r) => r.arrayBuffer());
writeFileSync("page.png", Buffer.from(png));

// 5. Clean up
await fetch(`https://api.chaser.sh/v1/sessions/${id}`, { method: "DELETE" });
```

## Python

```python
import os, requests
from patchright.sync_api import sync_playwright

# 1. Create a session
session = requests.post(
    "https://api.chaser.sh/v1/sessions",
    headers={"Authorization": f"Bearer {os.environ['CHASER_KEY']}"},
    json={"ttl_seconds": 1800},
).json()

# 2. Connect
with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(session["cdp_url"])
    context = browser.contexts[0]
    page = context.pages[0] if context.pages else context.new_page()
    page.goto("https://www.cloudflare.com")
    print(page.title())

# 3. Screenshot
png = requests.post(
    f"https://api.chaser.sh/v1/sessions/{session['id']}/screenshot",
    headers={"Authorization": f"Bearer {os.environ['CHASER_KEY']}"},
).content
with open("page.png", "wb") as f:
    f.write(png)

# 4. Clean up
requests.delete(
    f"https://api.chaser.sh/v1/sessions/{session['id']}",
    headers={"Authorization": f"Bearer {os.environ['CHASER_KEY']}"},
)
```

## Waiting for the session to be ready

`POST /v1/sessions` returns immediately with `status: "creating"`. The `cdp_url` is present in the response but the session may not be ready yet. Poll until `status` is `"ready"`:

```typescript
async function waitForSession(sessionId: string): Promise<void> {
  while (true) {
    const s = await fetch(
      `https://api.chaser.sh/v1/sessions/${sessionId}`,
      { headers: { Authorization: `Bearer ${process.env.CHASER_KEY}` } }
    ).then((r) => r.json());

    if (s.status === "ready") return;
    if (s.status === "error") throw new Error("Session failed");
    await new Promise((r) => setTimeout(r, 2000));
  }
}
```

## Handling disconnection

When the session expires or is deleted, the WebSocket closes. Listen for the `disconnected` event:

```typescript
browser.on("disconnected", () => {
  console.log("Session ended");
});
```

## What not to do

- **Don't call `chromium.launch()`** — use `connectOverCDP`
- **Don't call `browser.newContext()`** — use `browser.contexts()[0]`
- **Don't use `page.screenshot()`** — use the `/screenshot` endpoint
- **Don't override the user-agent or viewport** — it breaks fingerprint consistency