api reference.
Create a browser, drive it over CDP, delete it. Everything here works with curl.
base url https://api.guise.sh
auth x-api-key: <key> (get one at guise.sh/keys)
content application/jsonPOST/v1/sessions
Create a browser and get back a CDP endpoint. Usually about 180ms, or a couple of seconds if none are warm.
ttlSecnumberSelf-destruct timer.30–3600, default300. Out-of-range values are clamped, not rejected. The browser is destroyed when it fires. You pay until it is destroyed, so a session you never delete costs the fullttlSec. Closing your CDP connection does not end it — onlyDELETEor the timer does.proxystringOptional egress tier:"residential"or"dc". Omit it and we pick for you. Every session is proxied;falseis rejected.targetUrlstringOptional. An absolute http(s) URL naming where this browser is headed, so the exit can be picked on that host's history. Nothing is fetched.proxyIdnumberOptional. Pin one specific exit by id, for A/B tests or a sticky address. Must be ACTIVE and in the requested tier.
curl -s -X POST https://api.guise.sh/v1/sessions \
-H "x-api-key: $GUISE_KEY" \
-d '{"ttlSec":300}'{
"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.
statusstring"ready"normally."takeover"while a human holds the session through a takeover link — your CDP commands are answered with an error until they let go. A session that no longer exists is a404, not a status.secondsRemainingnumberTime left before the browser destroys itself, counted on our clock so you need not trust yours. Reaches0, never negative.egressobjectThe exit this browser leaves through.tieris always there;proxyIdappears once an exit is actually bound, and until then you get"pending": trueinstead — a fresh session normally reports pending, because the exit is chosen on the browser's first request. Once bound it does not change for the life of the session. Pass theproxyIdback on a later create to pin the same exit. No IP: the address is not returned, and a browser that needs its own can just ask a site.
[ { "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.
ttlSecnumberHow long the link stays valid.30–600, default300— a different range from a session. It is also capped by the time the session has left, so a takeover can never outlive the browser.
curl -s -X POST https://api.guise.sh/v1/sessions/$ID/takeover \
-H "x-api-key: $GUISE_KEY" \
-d '{"ttlSec":300}'{
"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.
{
"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.
const b = await chromium.connectOverCDP(session.connectUrl);
const page = await b.contexts()[0].newPage();
await page.goto("https://example.com");const b = await puppeteer.connect({ browserWSEndpoint: session.wsUrl });
const page = await b.newPage();b = playwright.chromium.connect_over_cdp(session["connectUrl"])
page = b.contexts[0].new_page()## proxy tiers
"residential"A residential IP, one per session, kept for the whole session."dc"A datacenter IP. Faster, and fine for APIs and development.
## errors
400bad body — unknown proxy tier, proxy:false, or a targetUrl that is not an absolute http(s) URL401missing or unknown x-api-key402credits exhausted — new sessions blocked until you top up; running ones are untouched404no such session, or a proxyId that is not ACTIVE in that tier409takeover asked for with under 30s of session left502we failed to bring a browser up — safe to retry503no ACTIVE proxies in the requested tier, or the usage ledger is unavailable — new sessions pause, running ones are untouched