# Streams

## 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 without a gap — the stream
carries the same events the webhooks deliver, byte for byte, which is
why a webhook signature and a stream frame can never disagree.

**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.

**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 fixed minimum retention period in beta, and an environment reset
may erase it. 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` rather than answered
with silence. A
cursor beyond the latest committed event is refused with HTTP 422
`invalid_paging`, 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.

**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"
```

## Domain Types

### Stream Events Response

- `StreamEventsResponse = string`
