Each session holds one cookie jar and its saved results.
Name
Cookies
Saved
Created
Actions
API keys
API keys authorize worker and stream operations, limited to the scopes you grant.
Label
Key
Scopes
Status
Created
Last used
Actions
Agent keys
Cookie jars submitted through API keys, who submitted them, and the runs they produced.
Marker
API key
Status
Cookies
Result
Created
Actions
SSE guide
How an external client follows a worker's progress over Server-Sent Events, with example frames for every event type.
01Create an API key
The full key looks like gaxam_<lookup>.<secret> and is shown only once. A key without stream:read gets 403 apikey_scope_denied.
Open API keys and create a key with the stream:read scope. Add worker:read if the client also fetches worker details. Every request sends it as a bearer token:
A stream is scoped to one worker. The worker must belong to the key owner; any other id returns 404 worker_not_found.
Starting a run with POST /api/v1/agent/credentials/{id}/run (scope worker:run) returns the worker_id. You can also copy it from the Workers page URL, #/workers/<worker_id>.
201 response, credential object shortened
{
"credential": { "id": "3f0c…", "status": "running", … },
"worker_id": "7c1e2a9d-4b4f-4c8e-9a51-0e6b2f3d8a10",
"session_id": "b52d7e01-93c4-4f0a-8c2e-5d1f6a7b9c33",
"message": "Worker queued for this credential"
}
03Open the stream
-N turns off curl buffering so frames print as they arrive. Lines starting with : are heartbeats (every 15 s by default); ignore them.
Send a GET with the bearer header. The connection stays open and the server pushes frames until the worker finishes.
Every frame has an event: name, a JSON data: line and, for stored events, an id: with the event sequence number.
04Reconnect without losing events
Default limits: a stream lives at most 2 hours, then sends stream_timeout. One reconnect replays up to 500 missed events.
Remember the last id: you processed and send it back as Last-Event-ID. Each new connection starts with a fresh snapshot, then replays everything after that id, then continues live.
Stop reconnecting after a terminal event. A 4xx response (bad key, missing scope, unknown worker) will not fix itself on retry.
05Client examples
The browser EventSource API cannot send an Authorization header, so call the stream from your backend. Both examples parse frames by hand and need no SSE library.
Node.js 18+ (built-in fetch)
const BASE = "https://your-host/api/v1";
const KEY = process.env.GAXAM_KEY;
const WORKER = process.argv[2];
let lastId = "";
async function follow() {
const headers = { Authorization: `Bearer ${KEY}`, Accept: "text/event-stream" };
if (lastId) headers["Last-Event-ID"] = lastId;
const res = await fetch(`${BASE}/workers/${WORKER}/stream`, { headers });
if (!res.ok) {
const err = new Error(`${res.status} ${await res.text()}`);
err.fatal = res.status < 500;
throw err;
}
const decoder = new TextDecoder();
let buffer = "";
for await (const chunk of res.body) {
buffer += decoder.decode(chunk, { stream: true });
let cut;
while ((cut = buffer.indexOf("\n\n")) !== -1) {
const raw = buffer.slice(0, cut);
buffer = buffer.slice(cut + 2);
let event = "message", id = null;
const data = [];
for (const line of raw.split("\n")) {
if (line.startsWith(":")) continue; // heartbeat
const i = line.indexOf(":");
const field = i === -1 ? line : line.slice(0, i);
const value = i === -1 ? "" : line.slice(i + 1).replace(/^ /, "");
if (field === "event") event = value;
else if (field === "data") data.push(value);
else if (field === "id") id = value;
}
if (!data.length) continue;
if (id && event !== "snapshot") lastId = id;
console.log(event, JSON.parse(data.join("\n")));
if (event === "terminal") return true;
}
}
return false; // closed early: timeout or network drop
}
for (;;) {
try {
if (await follow()) break;
} catch (err) {
console.error(err.message);
if (err.fatal) break;
}
await new Promise((r) => setTimeout(r, 3000));
}
Python 3 (httpx)
import json, os, sys, time
import httpx
BASE = "https://your-host/api/v1"
KEY = os.environ["GAXAM_KEY"]
WORKER = sys.argv[1]
state = {"last_id": None}
def follow():
headers = {"Authorization": f"Bearer {KEY}", "Accept": "text/event-stream"}
if state["last_id"]:
headers["Last-Event-ID"] = state["last_id"]
timeout = httpx.Timeout(10.0, read=60.0) # heartbeats arrive every 15 s
url = f"{BASE}/workers/{WORKER}/stream"
with httpx.stream("GET", url, headers=headers, timeout=timeout) as res:
res.raise_for_status()
event, data, seq = "message", [], None
for line in res.iter_lines():
if line == "":
if data:
if seq and event != "snapshot":
state["last_id"] = seq
print(event, json.loads("\n".join(data)))
if event == "terminal":
return True
event, data, seq = "message", [], None
elif line.startswith(":"):
continue # heartbeat
else:
field, _, value = line.partition(":")
value = value[1:] if value.startswith(" ") else value
if field == "event":
event = value
elif field == "data":
data.append(value)
elif field == "id":
seq = value
return False # closed early: timeout or network drop
while True:
try:
if follow():
break
except httpx.HTTPStatusError as exc:
if exc.response.status_code < 500:
raise
except httpx.TransportError as exc:
print("disconnected:", exc)
time.sleep(3)
06Event reference
Values below are examples. Timestamps in the opening snapshot use a space (2026-10-13 08:01:12+00:00); stored events use ISO 8601 with T. Parse both.
snapshotFirst frame on every connection. Full current state of the worker.
Its id is the latest stored sequence number. If the worker already finished, a terminal frame follows and the stream closes.
lifecycleStatus change: the worker was queued, or a stop was requested.
Only two are emitted today: queued on creation and stopping with "reason": "stop_requested". There is no separate "running" lifecycle event; the first item_started means it is running.
terminalThe worker finished. The server closes the stream after this frame.
status is completed, failed or cancelled. When the reaper fails a silent worker the payload is short: {"status": "failed", "error_code": "worker_timeout", "stopping": false}.
errorThe worker disappeared between authentication and the first frame.
Rare: usually the worker was deleted at that moment. Do not retry. Authentication errors never arrive as frames; they come back as a normal JSON response with a 4xx status.
event: error
data: {"code": "worker_not_found"}
HTTP 4xxRejected before the stream opens. Fix the request; retrying will not help.
Common codes: 401 apikey_not_found, apikey_revoked, apikey_expired; 403 apikey_scope_denied; 404 worker_not_found.
HTTP/1.1 403 Forbidden
Content-Type: application/json
{"error": {"code": "apikey_scope_denied", "message": "This API key does not carry the scope required for this operation", "request_id": "…"}}
New exam session
Replace cookie jar
Create API key
API key created
This value cannot be retrieved again after you close this dialog.