Everything the dashboard does, as REST calls plus one websocket. Base URL is this host. Authenticate with Authorization: Bearer sk_… (your key is on the billing page). Money fields are integer micro-dollars: 1000000 = $1.
A swarm is a named group of sessions. A session is one cloud Chrome with its own agent and optional proxy. Sending an instruction to a session creates a run: the agent works the task in that browser and produces a result. Instructing a session mid-run interrupts the run. Each session also accepts direct control (navigate, input, screenshot) over CDP, which does not involve the agent and costs only browser time. Spawning runs a short bootstrap task to provision the browser (open the start url, reply "ready"); expect roughly a cent of agent cost per session at spawn.
Concurrency is capped per account (2 on the free tier, 5 once you've topped up). Swarms created here appear in the web dashboard immediately, so you can watch, click into, and debug API-driven browsers by hand.
/api/v1/swarmsCreate a swarm and spawn its sessions. Body: count (1–200, subject to your cap), url start url (optional), proxies array of "us"/"de"/"none"/"host:port:user:pass" assigned round-robin (optional), model (optional), name (optional). Returns the swarm and its sessions; sessions become ready a few seconds later.
curl -X POST $HOST/api/v1/swarms -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"count":3,"url":"https://example.com","proxies":["us","de"],"name":"price-check"}'
{"swarm":{"id":"sw_1a2b3c","name":"price-check","source":"api","model":"gpt-5.6-luna","created":…},
"sessions":[{"id":"s_9f8e7d","swarmId":"sw_1a2b3c","n":1,"status":"spawning","url":"","proxy":"us","cost":0,…},…]}
/api/v1/swarmsList swarms with sessions. ?all=1 includes stopped swarms; ?sessions=0 omits sessions.
/api/v1/swarms/:idOne swarm with its sessions.
/api/v1/swarms/:id/sessionsAdd sessions to a swarm. Body: count, url, proxies.
/api/v1/swarms/:id/gotoNavigate every session (or sessionIds) to url. Instant over CDP; falls back to the agent if a browser isn't attached yet.
/api/v1/swarms/:id/instructSend text to every agent (or sessionIds). Returns one run id per session. Poll GET /api/v1/runs/:id for status and result.
curl -X POST $HOST/api/v1/swarms/sw_1a2b3c/instruct -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"text":"What is the main heading on this page? Reply with just the heading."}'
{"runs":[{"sessionId":"s_9f8e7d","runId":"r_4c5d6e"},…]}
curl $HOST/api/v1/runs/r_4c5d6e -H "Authorization: Bearer $KEY"
{"run":{"id":"r_4c5d6e","status":"completed","result":"Example Domain","cost":5957,"text":"…","created":…,"finished":…}}
/api/v1/swarms/:id/stopStop every session (or sessionIds). Stopping settles proxy bandwidth and ends browser billing.
/api/v1/swarms/:idStop the whole swarm.
/api/v1/sessionsAll active sessions across swarms.
/api/v1/sessions/:idSession state (status: spawning · ready · running · detached · error · stopped; url, title, last log line, liveUrl, cost, viewport vw/vh) plus its runs.
/api/v1/sessions/:id/logs?after=0Agent log lines (tool calls, reasoning, results, navigation). Pass the last id you saw as after to poll for new lines.
/api/v1/sessions/:id/screenshotCurrent viewport as JPEG.
/api/v1/sessions/:id/gotoBody {"url":"…"}.
/api/v1/sessions/:id/instructBody {"text":"…"}. Returns runId.
/api/v1/sessions/:id/inputInject one input event (see input).
/api/v1/sessions/:id/stopStop the session.
/api/v1/runs/:idRun status: queued · running · completed · failed · cancelled, with result and cost.
Connect to wss://HOST/api/v1/stream?key=sk_… (optionally &swarm=sw_… to scope). This is exactly what the dashboard uses, so you can render your own grid. Text frames are JSON. Binary frames are live JPEGs: the first two bytes are the session number n (big-endian uint16), the rest is the image.
// server → client
{"t":"hello","limits":{…},"balance":1830000,"models":[…],"pricing":{…}}
{"t":"snapshot","swarms":[…],"sessions":[…]} // full state on connect / after spawn / stop
{"t":"state","session":{…}} // one session changed
{"t":"log","sessionId":"s_…","id":123,"ts":…,"line":"⚙ click 'Add to cart'"}
{"t":"balance","balance":1790000}
{"t":"spawned","req":"my-ref","swarm":{…},"sessionIds":[…]}
{"t":"error","text":"concurrency limit: 2 browsers on the free tier (2 active)"}
<binary> [n hi][n lo][jpeg bytes…]
// client → server
{"t":"spawn","count":2,"url":"https://…","proxies":["us"],"model":"gpt-5.6-luna","swarmId":null,"req":"my-ref"}
{"t":"goto","url":"https://…","sessionIds":["s_…"]} // sessionIds optional → all (in scope)
{"t":"instruct","text":"…","sessionIds":["s_…"]}
{"t":"stop","sessionIds":["s_…"]}
{"t":"focus","id":"s_…"} // sharper frames for this session (null to clear)
{"t":"input","id":"s_…","ev":{…}}
{"t":"quality","profile":"tile"|"focus"} // frame size hint for the tiles
{"t":"filter","swarmId":"sw_…"} // change scope, resends snapshot
// minimal renderer
const ws = new WebSocket(`wss://${HOST}/api/v1/stream?key=${KEY}`); ws.binaryType = 'arraybuffer';
const byN = new Map();
ws.onmessage = (e) => {
if (typeof e.data === 'string') { const m = JSON.parse(e.data); if (m.t === 'snapshot') for (const s of m.sessions) byN.set(s.n, s); if (m.t === 'state') byN.set(m.session.n, m.session); return; }
const n = new DataView(e.data).getUint16(0);
img(byN.get(n).id).src = URL.createObjectURL(new Blob([e.data.slice(2)], { type: 'image/jpeg' }));
};
Coordinates are normalized 0–1 over the viewport (the frame you received), so they are independent of how you scale the image. Same shape on the websocket (ev) and on POST /sessions/:id/input.
{"type":"move","x":0.5,"y":0.3}
{"type":"down","x":0.5,"y":0.3,"button":0,"clicks":1} // then "up" — button 0 left, 1 middle, 2 right
{"type":"wheel","x":0.5,"y":0.5,"dx":0,"dy":300}
{"type":"keydown","key":"a","code":"KeyA","keyCode":65} // then "keyup"; printable keys type text
{"type":"keydown","key":"Enter","code":"Enter","keyCode":13}
// modifiers: add "shift":true, "ctrl":true, "alt":true, "meta":true
/api/v1/meBalance, tier, caps, pricing, active session count.
/api/v1/ledger?limit=200Every credit and debit, newest first, with the swarm/session it belongs to.
/api/v1/paymentsTop-ups and auto-reloads.
/api/v1/spendLast 30 days by kind and by day.
/api/v1/me/api-keyRotate the key. The response is the only time the new key is shown.
Errors are {"error":"…"} with a meaningful status: 400 bad input, 401 bad key, 402 no credits, 404 not yours or not found, 409 browser not attached yet, 410 session stopped, 429 concurrency cap. When the balance reaches zero all sessions are stopped, unless auto-reload is on and a card is saved. Browser time is billed per started minute; stop sessions you're done with.