Space Stream
Real-time event stream for all jobs in a space
Space Stream
GET /api/v1/realtime/spaces/:spaceId
Scope: jobs:read
Connect to a space-wide stream. You receive events for all jobs in the space: creation, status changes, progress, completions, failures, and kills.
The stream sends a connected event immediately on connect. For full-scope clients, this includes live aggregate counts and reconnect metadata.
| Param | Required | Description |
|---|---|---|
token | Yes | API key with jobs:read or higher. Must be scoped to this space if the key is space-scoped. |
since | No | Last seq you successfully processed. Used for reconnect gap detection. |
streamInstanceId | No | Last stream instance ID from connected. Used to detect actor restarts on reconnect. |
const ws = new WebSocket(
`wss://emit.run/api/v1/realtime/spaces/${spaceId}?token=${apiKey}`
);
ws.onmessage = (e) => {
const event = JSON.parse(e.data);
console.log(`[${event.jobName}] ${event.type} → ${event.status}`);
};async def watch_space(space_id: str, api_key: str):
uri = f"wss://emit.run/api/v1/realtime/spaces/{space_id}?token={api_key}"
async with websockets.connect(uri) as ws:
async for message in ws:
event = json.loads(message)
print(f"[{event['jobName']}] {event['type']} → {event['status']}")
asyncio.run(watch_space("01JLQX...", "emit_YOUR_KEY"))The space stream uses the same event shape as the job stream, with the addition of jobName and stream metadata for robust reconnect behavior. Requires jobs:read or higher — so events always include full data.
Stream metadata
| Field | Where it appears | Meaning |
|---|---|---|
seq | connected + all space events | Monotonic sequence number for this stream instance. |
streamInstanceId | connected | Identifier for the current stream actor instance. Changes after actor restart. |
missed | connected | true when your reconnect cursor does not match server state (missed events or actor restart). |
counts | connected + count-changing status events | Server-side aggregate counts by status (scheduled, pending, delivered, running, completed, failed, dead, killed). |
previousStatus | status-transition events | Prior status before this transition (useful for exact counter diffs). |
Reconnect strategy
For dashboards, store seq and streamInstanceId from incoming messages and send them back on reconnect:
let lastSeq: number | null = null;
let streamInstanceId: string | null = null;
function connectSpaceStream(spaceId: string, apiKey: string) {
const url = new URL(`wss://emit.run/api/v1/realtime/spaces/${spaceId}`);
url.searchParams.set("token", apiKey);
if (lastSeq !== null) url.searchParams.set("since", String(lastSeq));
if (streamInstanceId) url.searchParams.set("streamInstanceId", streamInstanceId);
const ws = new WebSocket(url.toString());
ws.onmessage = (e) => {
const event = JSON.parse(e.data);
if (typeof event.seq === "number") {
lastSeq = event.seq;
}
if (typeof event.streamInstanceId === "string") {
streamInstanceId = event.streamInstanceId;
}
if (event.type === "connected" && event.missed) {
revalidateDashboardState();
}
};
return ws;
}This keeps reconnects fast while preventing stale counters after network drops, background-tab throttling, or actor restarts.
Common patterns
Submit-and-watch
Create a job on your backend and immediately stream progress back to the user. The client can connect directly to the single-job stream without a token (anonymous progress scope):
// Backend: create job and return ID to client
app.post("/api/start-report", async (req, res) => {
const job = await createJob({ name: "generate-report", payload: req.body });
res.json({ jobId: job.id });
});
// Frontend: stream progress directly
const { jobId } = await startReport(params);
const ws = new WebSocket(`wss://emit.run/api/v1/realtime/${jobId}`);
ws.onmessage = (e) => {
const event = JSON.parse(e.data);
if (event.type === "init" && event.progress) {
updateProgressBar(event.progress.percent);
}
if (event.type === "progress") updateProgressBar(event.data.percent);
};Dashboard live feed
Use the space stream with a jobs:read token to power a real-time operations dashboard:
const ws = new WebSocket(
`wss://emit.run/api/v1/realtime/spaces/${spaceId}?token=${dashboardKey}`
);
ws.onmessage = (e) => {
const event = JSON.parse(e.data);
updateJobRow(event.jobId, { status: event.status, type: event.type });
};