API

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.

conceptsswarmssessionsstreaminputaccounterrors

concepts

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.

swarms

POST/api/v1/swarms

Create 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,…},…]}
GET/api/v1/swarms

List swarms with sessions. ?all=1 includes stopped swarms; ?sessions=0 omits sessions.

GET/api/v1/swarms/:id

One swarm with its sessions.

POST/api/v1/swarms/:id/sessions

Add sessions to a swarm. Body: count, url, proxies.

POST/api/v1/swarms/:id/goto

Navigate every session (or sessionIds) to url. Instant over CDP; falls back to the agent if a browser isn't attached yet.

POST/api/v1/swarms/:id/instruct

Send 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":…}}
POST/api/v1/swarms/:id/stop

Stop every session (or sessionIds). Stopping settles proxy bandwidth and ends browser billing.

DELETE/api/v1/swarms/:id

Stop the whole swarm.

sessions

GET/api/v1/sessions

All active sessions across swarms.

GET/api/v1/sessions/:id

Session state (status: spawning · ready · running · detached · error · stopped; url, title, last log line, liveUrl, cost, viewport vw/vh) plus its runs.

GET/api/v1/sessions/:id/logs?after=0

Agent log lines (tool calls, reasoning, results, navigation). Pass the last id you saw as after to poll for new lines.

GET/api/v1/sessions/:id/screenshot

Current viewport as JPEG.

POST/api/v1/sessions/:id/goto

Body {"url":"…"}.

POST/api/v1/sessions/:id/instruct

Body {"text":"…"}. Returns runId.

POST/api/v1/sessions/:id/input

Inject one input event (see input).

POST/api/v1/sessions/:id/stop

Stop the session.

GET/api/v1/runs/:id

Run status: queued · running · completed · failed · cancelled, with result and cost.

stream (websocket)

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' }));
};

input events

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

account

GET/api/v1/me

Balance, tier, caps, pricing, active session count.

GET/api/v1/ledger?limit=200

Every credit and debit, newest first, with the swarm/session it belongs to.

GET/api/v1/payments

Top-ups and auto-reloads.

GET/api/v1/spend

Last 30 days by kind and by day.

POST/api/v1/me/api-key

Rotate the key. The response is the only time the new key is shown.

errors and limits

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.