## Stream events (SSE)

**get** `/v1/streams/events`

Streams the brand's events as server-sent events, one `data:` frame per
event, `id:` carrying the event's cursor. Reconnect with `Last-Event-ID`
(or the `cursor` query parameter) to resume after the last position you
processed. On read-back surfaces — the transcript, event log, event stream
and replays — zero-day messages carry empty `parts` and
`zero_retention: true`. Webhook deliveries and the send response carry
the content; an idempotent retry of a zero-day send returns empty `parts`
and `zero_retention: true`. When ephemeral messages
are enabled, expired messages and their related events are omitted, so the
stream's `id:` values can have retention gaps.

**A frame.** `id:` is the resume cursor and nothing else; `event:` is
the event's kind; `data:` is one JSON object
`{"id","type","timestamp","data":{...}}` — `id` there is the event's own
stable identity (the same value a webhook of it carries, so both sinks
deduplicate against one key), and the inner `data` addresses what moved:
`chat_id` plus the `seq_from`/`seq_to` range of that chat's event
sequence. Read message content and history from
`GET /v1/chats/{chat}/events` using that range. Kinds today are
`message.received`, `message.sent`, `message.delivered`, `message.read`,
`message.failed`, `chat.read` and `chat.window.opened`; the set grows additively, so
skip a kind you do not handle rather than failing on it, and expect new
members inside `data` too.

**One message can produce several events.** An inbound message produces
`message.received` and, when it opens the customer window,
`chat.window.opened`. Your reply then produces `message.sent` and may
later produce `message.delivered`. Do not treat every event as a reason
to reply; choose the event kinds that are actual triggers for your
integration. The window event can arrive first: a message carrying
attachments can be recorded once they resolve, so its `chat.window.opened`
(the next `seq`) may precede its `message.received`.

**Events with attachments.** A `message.received` for a message carrying
attachments can be recorded, and so take its log position, once every
attachment has been captured or has failed, or ten minutes after the
message arrived. It can therefore appear after events for later messages
of the same chat: `seq_from` can decrease relative to arrival. Never
discard an event because its `seq_from` is lower than one already
received; deduplicate by the event `id`, and order the chat by
`seq_from`. For live processing, buffer briefly or reconcile with
`GET /v1/chats/{chat}/events`.

**Connecting.** Without a cursor the stream starts at the present
moment — it does NOT replay history. During ordinary operation, use the
account event log to recover missed account events and
`GET /v1/chats/{chat}/events` to read chat content. Account-event history
has no published minimum retention period yet. The server sends a
`retry:` hint on connect and a `: keepalive` comment while idle;
neither is an event. There is no scheduled account-event pruning today.
If history is no longer available, a cursor older than its retained
beginning is refused with HTTP 410 `cursor_expired` (a 2xxx
`error.code`, 2032) rather than answered with silence. A
cursor beyond the latest committed event is refused with HTTP 422
`invalid_paging` (a 1xxx `error.code`, 1016), while a cursor exactly at
the latest event is the ordinary caught-up position. An empty cursor is
refused too: omit it to start at the present position.

**Cursors are scoped to the brand and the test/live partition selected
by the key.** Each partition has its own dense sequence from 1, while
that partition is deliberately absent from the wire. If you reconnect
at cursor 42 with a key for the other partition, the server answers 200
and resumes at that partition's event 42; `cursor_expired` cannot signal
the swap. Keep separate cursors for `sk_test_` and `sk_live_` keys.

**A key that acts in an environment cannot open this stream.** It is
refused with HTTP 403 `environment_stream_unavailable` (a 2xxx
`error.code`, 2055) before anything is read; an environment's events
reach you through the webhook endpoints registered under its key. A
key that acts in no environment — every key issued before environments
existed — streams the events of numbers in none. Events of numbers in
an environment share this stream's cursor sequence but are not
delivered on it, so the `id:` values you see may skip; resume from the
last `id:` you processed, and nothing you are shown is lost.

**Webhooks.** The same events are delivered as signed webhooks
(Standard Webhooks: `webhook-id`, `webhook-timestamp`,
`webhook-signature`, verifiable with any off-the-shelf library).
Register a destination for them under `/v1/webhook_endpoints`.

### Query Parameters

- `cursor: optional string`

  Resume cursor — the last event id you processed. Empty and beyond-latest values are refused. The header form wins when both are set.

### Header Parameters

- `"Last-Event-ID": optional string`

### Example

```http
curl https://whatsapp.messages.api.linqapp.com/v1/streams/events \
    -H "Authorization: Bearer $LINQ_WHATSAPP_API_KEY"
```
