api reference.

Create a browser, drive it over CDP, delete it. Everything here works with curl.

# basics
base url   https://api.guise.sh
auth       x-api-key: <key>          (get one at guise.sh/keys)
content    application/json
────────────────────────────────────────────────────────────────

POST/v1/sessions

Create a browser and get back a CDP endpoint. Usually about 180ms, or a couple of seconds if none are warm.

# request
curl -s -X POST https://api.guise.sh/v1/sessions \
  -H "x-api-key: $GUISE_KEY" \
  -d '{"ttlSec":300}'
# response — 200
{
  "id": "0d66042b-e723-4fa7-9d7d-c7ef61317bd8",
  "status": "ready",
  "browser": "Chrome/152.0.7977.54",
  "connectUrl": "https://api.guise.sh/v1/sessions/0d66042b-…/cdp",
  "wsUrl": "wss://api.guise.sh/v1/sessions/0d66042b-…/cdp",
  "egress": { "tier": "residential", "pending": true },
  "createdAt": "2026-08-17T01:48:40.965Z",
  "expiresAt": "2026-08-17T02:18:40.965Z",
  "secondsRemaining": 1800,
  "pooled": true
}

GET/v1/sessions

Your live sessions, as a plain array. pooled appears only on create.

# response — 200
[ { "id": "93a8f457-…", "status": "ready",
    "browser": "Chrome/152.0.7977.54",
    "connectUrl": "https://api.guise.sh/v1/sessions/93a8f457-…/cdp",
    "wsUrl": "wss://api.guise.sh/v1/sessions/93a8f457-…/cdp",
    "egress": { "proxyId": 2311918, "tier": "residential" },
    "createdAt": "…", "expiresAt": "…", "secondsRemaining": 1743 } ]

GET/v1/sessions/:id

One session, same shape as a list entry. Returns 404 once it is gone. Ids are never reused.

POST/v1/sessions/:id/takeover

Create a link that lets a person drive the browser themselves. Useful for a login, a captcha, or watching what the agent is doing.

# request
curl -s -X POST https://api.guise.sh/v1/sessions/$ID/takeover \
  -H "x-api-key: $GUISE_KEY" \
  -d '{"ttlSec":300}'
# response — 200
{
  "url": "https://api.guise.sh/takeover#<sessionId>.<token>",
  "expiresAt": "2026-08-17T14:05:00.000Z",
  "sessionExpiresAt": "2026-08-17T14:18:40.965Z"
}

Open the link in any browser. The token sits after the #, so it is never sent in the request and never reaches a server log. It does stay in that browser's history — revoke the link when you are done. There is one link per session, and it cannot outlast the session: 409 if under 30 seconds remain.

DELETE/v1/sessions/:id/takeover

Revoke the link and disconnect anyone using it. The browser keeps running.

DELETE/v1/sessions/:id

Delete the browser now. Returns { "deleted": "<id>" }.

GET/v1/usage

Your credit and usage. Sessions bill from create to delete at one flat rate. At zero, new sessions return 402 and running ones finish.

# response — 200
{
  "rateCentsPerBrowserHour": 10,
  "browserHours": 12.408,
  "sessions": 87,
  "creditCents": 1000,
  "spentCents": 186.12,
  "remainingCents": 813.88
}
────────────────────────────────────────────────────────────────

## connect

connectUrl speaks CDP over https — playwright fetches /json/version beneath it first. wsUrl is the same endpoint as a websocket, which is what puppeteer wants. Anything that speaks CDP works, with no SDK in between. The session id in the URL is the only credential, so treat the whole URL as a secret.

# playwright (node)
const b = await chromium.connectOverCDP(session.connectUrl);
const page = await b.contexts()[0].newPage();
await page.goto("https://example.com");
# puppeteer
const b = await puppeteer.connect({ browserWSEndpoint: session.wsUrl });
const page = await b.newPage();
# playwright (python)
b = playwright.chromium.connect_over_cdp(session["connectUrl"])
page = b.contexts[0].new_page()
────────────────────────────────────────────────────────────────

## proxy tiers

────────────────────────────────────────────────────────────────

## errors