SSE: all run status changes

Server-Sent Events stream for all run status changes in the org. Supports cookie auth and API key auth via ?token=apst_... query parameter. Each channel reaches only a caller who may receive it; see Channel access below.

Event format: event: run_update\ndata: {"id":"run_...","status":"running","packageId":"@scope/name",...}\n\n

Event types: run_update (status change), run_log (log entry), run_metric (running cumulative cost + token usage), connection_update (INSERT/UPDATE/DELETE on integration_connections, actor-scoped to the caller's own rows), chat_session_update (a change signal on one of the caller's own chat sessions, emitted when the chat module is enabled). Heartbeat: a named SSE event: ping frame (empty data) sent immediately on connect and every 30s thereafter.

Each SSE frame carries an id: field of the form ${subscriberId}:${monotonic}. Ids are globally unique across reconnects (each new EventSource gets a fresh subscriberId). Client-side dedup on id is safe. Server-side replay via Last-Event-ID is NOT implemented — reconnect lands on the live tail; missed events are not replayed.

Channel selection: pass channels= with a comma-separated subset (e.g. channels=run_update,connection_update) to receive only those frames. The filter is applied server-side before serialization. Omit it to receive every channel the caller may receive. Note that dropping run_log is what keeps a dashboard-wide stream off the per-log firehose.

Channel access: run_update, run_log and run_metric need runs:read or runs:read-all in the space (for an API key, among its scopes). chat_session_update needs chat:read in the space, as every /api/chat route does; an API key never holds it. connection_update carries only the caller's own rows: a session always receives it, an API key needs integrations:read. A channel the caller may not receive is dropped from the subscription; the stream is refused with 403 only when none of the requested channels (every channel, when channels is omitted) remains.

Run visibility: run_update, run_log and run_metric carry only the runs the caller may read — every run in the space with runs:read-all, otherwise the runs the caller launched. The single-run stream refuses a run the caller may not read with 404, the same answer as GET /api/runs/{id}.

GET/api/realtime/runs

Authorization

better-auth.session_token<token>

Cookie session from Better Auth. Requires X-Org-Id header for org-scoped routes.

In: cookie

Query Parameters

orgId*string

Organization ID. Required for SSE auth (cookies cannot carry X-Org-Id header on EventSource).

Formatuuid
view_as?string

Role preview for this stream — the same value, grammar and refusals as the X-View-As header (see that parameter). It is a query parameter here because EventSource cannot send headers — presenting it as the X-View-As header on these routes is 400 invalid_view_as. Sessions only: with ?token=apst_… it is 400 view_as_unsupported. A stream opened under a persona sees what that role would see and stops where that role would stop (403 not_a_space_member, or 404 for a private space), and carries X-View-As-Active: 1.

spaceId?string

Space ID. Required for cookie auth (SSE cannot send X-Space-Id header). Not needed for API key auth (space resolved from key).

token?string

API key (apst_ prefix) for SSE authentication. EventSource cannot send Authorization headers, so API key auth uses this query parameter instead.

verbose?boolean

When true, include full payload with result and data fields. Default (false) strips large user-content fields for safer consumption by external agents.

Defaultfalse
channels?string

Comma-separated list of SSE channels to subscribe to (run_update, run_log, run_metric, connection_update, chat_session_update). Omit to receive every channel the caller may receive (default). Unknown names are ignored; if nothing is recognised the stream falls back to that same default. Declaring only the channels you consume avoids fanning the run_log firehose out to a stream that discards it.

curl -X GET "https://your-instance/api/realtime/runs?orgId=497f6eca-6276-4993-bfeb-53cbbbba6f08"
"string"

{
  "type": "https://docs.appstrate.dev/errors/invalid-view-as",
  "title": "Invalid View-As Header",
  "status": 400,
  "detail": "X-View-As could not be parsed: space and role must be provided together",
  "code": "invalid_view_as",
  "param": "X-View-As",
  "request_id": "req_abc123"
}

{
  "type": "https://docs.appstrate.dev/errors/unauthorized",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Invalid or missing session",
  "code": "unauthorized",
  "request_id": "req_abc123"
}
{
  "type": "https://docs.appstrate.dev/errors/forbidden",
  "title": "Forbidden",
  "status": 403,
  "detail": "Insufficient permissions",
  "code": "forbidden",
  "request_id": "req_abc123"
}
{
  "type": "https://docs.appstrate.dev/errors/not-found",
  "title": "Not Found",
  "status": 404,
  "detail": "Resource not found",
  "code": "not_found",
  "request_id": "req_abc123"
}