Session Events (SSE)

Live server-sent events for a passenger session, with a poll/replay fallback.

A passenger session has a server-side event log — a ring buffer of frames the platform appends as things happen. Two endpoints read it:

  • GET /v1/sessions/{id}/stream — a live server-sent events stream.
  • GET /v1/sessions/{id}/state — a poll/replay of the same log, for when you can’t hold an open connection.

Both are session-scoped, but unlike the personalization POSTs they take the session id as the {id} path segment, not the X-Session-Id header (see Sessions). The {id} follows the same key-safety rules; an unsafe or unresolvable id fails with 400 session.unresolved.

Live stream

$curl -N "https://api.staging.westiq.ai/v1/sessions/seat-12A-f3d9c1/stream?since=0" \
> -H "Authorization: Bearer $ACCESS_TOKEN"
  • The response is text/event-stream.
  • since (optional, non-negative integer cursor, default 0) replays buffered frames from that cursor first, then tails live.
  • A : heartbeat comment line arrives every 15–30s to keep the connection alive through proxies. Ignore it.
  • Each event frame is delivered as an SSE data: line whose payload is a frame object.

Failure timing matters. If the stream can’t be opened at all (including the client having already disconnected), it fails before streaming as a normal JSON error envelope — e.g. 400 session.unresolved for a bad id, or 422 for a bad since. Once the stream has started, a later fault arrives in-band as a terminal event: error SSE frame, never a JSON body. So handle both: a JSON error on connect, and an error frame mid-stream.

Poll / replay

When a persistent connection isn’t practical (or to reconcile after a drop), poll the same log:

$curl "https://api.staging.westiq.ai/v1/sessions/seat-12A-f3d9c1/state?since=0" \
> -H "Authorization: Bearer $ACCESS_TOKEN"
1{
2 "schemaVersion": "1",
3 "airlineId": "demo_airline",
4 "sessionId": "seat-12A-f3d9c1",
5 "frames": [
6 {
7 "schemaVersion": "1",
8 "airlineId": "demo_airline",
9 "sessionId": "seat-12A-f3d9c1",
10 "sequence": 4,
11 "eventType": "session.diagnostic",
12 "payload": {},
13 "freeTextFields": [],
14 "createdAtMs": 1751558490000
15 }
16 ],
17 "nextCursor": { "sequence": 4, "createdAtMs": 1751558490000 },
18 "correlationId": "req_0197c0a1b2c3d4e5f60718293a4b5c6d"
19}

Pass nextCursor.sequence as the since of your next poll. When nothing new has arrived, frames is empty and nextCursor echoes your since back unchanged — your cursor never goes backwards.

Frame shape

Every frame — streamed or polled — has the same shape:

FieldMeaning
sequenceMonotonic cursor within the session. The value you carry forward as since.
eventTypeThe frame discriminator (see below).
payloadA flat object of scalar fields for that event type.
freeTextFieldsWhich payload properties are passenger-visible free text (already egress-scrubbed).
createdAtMsServer-stamped creation time, epoch ms. Informational — never used for ordering.

Frame types

eventTypeMeaning
session.diagnosticA transport-level probe frame. Carries no free text.
suggestions.updatedDefined for pushing a suggestions result (behavioralInsight summary only).
enrichment.readyDefined for pushing an enrichment result (reasoningNarrative summary only).

Live push of personalization results is not yet enabled. The suggestions.updated and enrichment.ready frame types are defined, but the producers do not publish them yet (a concurrency fix is pending). Today, treat the HTTP response of /v1/suggestions and /v1/enrichment as the source of truth for those results, and use state/stream for session-diagnostic frames and forward compatibility. When the push lands, the full per-item arrays will still ride the HTTP response only — a pushed frame carries just the scalar summary field.