Quickstart

View as Markdown

Get a running session, connect to it, take a screenshot, and tear it down — all in under five minutes.

Prerequisites

An API key from the Chaser dashboard. Sign up, create a workspace, and generate a key under API Keys. Set it as an environment variable:

$export CHASER_KEY="cha_live_..."

Step 1 — Create a session

$curl -s https://api.chaser.sh/v1/sessions \
> -X POST \
> -H "Authorization: Bearer $CHASER_KEY" \
> -H "Content-Type: application/json" \
> -d '{
> "ttl_seconds": 600
> }' | jq

Response:

1{
2 "id": "sess_019ec0d0-a88a-73f2-9741-8030dc82a854",
3 "object": "session",
4 "status": "creating",
5 "cdp_url": "wss://cdp.chaser.sh/sessions/sess_019ec0d0-a88a-73f2-9741-8030dc82a854",
6 "created_at": "2026-06-13T11:49:20.651182Z",
7 "expires_at": "2026-06-13T11:59:20.650185Z"
8}

The session is ready when status is "ready". The cdp_url is a standard WebSocket endpoint.

Step 2 — Connect and automate

Use patchright (a drop-in Playwright replacement) or any CDP client:

1// npm i patchright
2import { chromium } from "patchright";
3
4const browser = await chromium.connectOverCDP(cdpUrl);
5const context = browser.contexts()[0];
6const page = context.pages()[0] ?? await context.newPage();
7
8await page.goto("https://www.cloudflare.com");
9const title = await title.innerText();
10console.log(title);

Or with Python:

1# pip install patchright
2from patchright.sync_api import sync_playwright
3
4with sync_playwright() as p:
5 browser = p.chromium.connect_over_cdp(cdp_url)
6 context = browser.contexts[0]
7 page = context.pages[0] or context.new_page()
8 page.goto("https://www.cloudflare.com")
9 print(page.title())

Why patchright? The Chromium bundled with stock Playwright and Puppeteer ships with known automation artifacts — navigator.webdriver === true, missing chrome.runtime, and other detectable properties. Patchright patches these at the browser level so the session looks like an unmodified user browser.

Step 3 — Take a screenshot

Use Chaser’s screenshot endpoint rather than page.screenshot(). The endpoint captures the framebuffer directly and avoids compositor-readback issues:

$curl -s https://api.chaser.sh/v1/sessions/sess_019ec0b4-b174-7e90-91b3-02032470cb24/screenshot \
> -X POST \
> -H "Authorization: Bearer $CHASER_KEY" \
> -o page.png

The response is a raw PNG. From code:

1const png = await fetch(
2 `https://api.chaser.sh/v1/sessions/${sessionId}/screenshot`,
3 {
4 method: "POST",
5 headers: { Authorization: `Bearer ${process.env.CHASER_KEY}` },
6 }
7).then((r) => r.arrayBuffer());
8
9writeFileSync("page.png", Buffer.from(png));

Step 4 — Tear down

$curl -s https://api.chaser.sh/v1/sessions/sess_019ec0b4-b174-7e90-91b3-02032470cb24 \
> -X DELETE \
> -H "Authorization: Bearer $CHASER_KEY"

Deleting a session is idempotent. If it already expired or was deleted, you get a 409 session_not_open — that’s fine. The session stops accumulating egress the moment it’s deleted.

Sessions also auto-expire at the expires_at timestamp. Set ttl_seconds on creation (default: 1800, max: 3600) to control how long a session lives without explicit deletion.

What’s next