> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.westiq.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.westiq.ai/_mcp/server.

# Session Events (SSE)

> **Note**
>
> [Completed Update Delivery](/guides/completed-delivery) is a separate prepared
> contract, **disabled by default and not available on deployed endpoints**.
> These session-event frames and cursors are not completed-delivery updates,
> receipts or acknowledgements. Do not use one API's cursor with the other.

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](https://developer.mozilla.org/docs/Web/API/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](/guides/sessions#sse-endpoints-name-the-session-in-the-url)). The
`{id}` follows the same key-safety rules; an unsafe or unresolvable id fails with
`400 session.unresolved`.

## Live stream

```bash
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](#frame-shape).

> **Note**
>
> **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](/guides/errors) — 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:

```bash
curl "https://api.staging.westiq.ai/v1/sessions/seat-12A-f3d9c1/state?since=0" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

```json
{
  "schemaVersion": "1",
  "airlineId": "demo_airline",
  "sessionId": "seat-12A-f3d9c1",
  "frames": [
    {
      "schemaVersion": "1",
      "airlineId": "demo_airline",
      "sessionId": "seat-12A-f3d9c1",
      "sequence": 4,
      "eventType": "session.diagnostic",
      "payload": {},
      "freeTextFields": [],
      "createdAtMs": 1751558490000
    }
  ],
  "nextCursor": { "sequence": 4, "createdAtMs": 1751558490000 },
  "correlationId": "req_0197c0a1b2c3d4e5f60718293a4b5c6d"
}
```

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 \[#frame-shape]

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

| Field            | Meaning                                                                               |
| ---------------- | ------------------------------------------------------------------------------------- |
| `sequence`       | Monotonic cursor within the session. The value you carry forward as `since`.          |
| `eventType`      | The frame discriminator (see below).                                                  |
| `payload`        | A flat object of scalar fields for that event type.                                   |
| `freeTextFields` | Which `payload` properties are passenger-visible free text (already egress-scrubbed). |
| `createdAtMs`    | Server-stamped creation time, epoch ms. Informational — never used for ordering.      |

### Frame types

| `eventType`           | Meaning                                                                                             |
| --------------------- | --------------------------------------------------------------------------------------------------- |
| `session.diagnostic`  | A transport-level probe frame. Carries no free text.                                                |
| `suggestions.updated` | Defined for pushing a [suggestions](/guides/suggestions) result (`behavioralInsight` summary only). |
| `enrichment.ready`    | Defined for pushing an [enrichment](/guides/enrichment) result (`reasoningNarrative` summary only). |

> **Warning**
>
> **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`](/guides/suggestions) and
> [`/v1/enrichment`](/guides/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.