> ## Documentation Index
> Fetch the complete documentation index at: https://docs.polymorfa.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Stream events in real time

> Receive a project's events over a server-sent event stream with a team key or project credential, and resume after a disconnect.

An event stream delivers a project's events to your server over one HTTP
connection as they happen. It carries the same events as your webhooks and
[event history](/api/events), so you can use it instead of a public webhook
endpoint or alongside one.

<Note>
  Event streams are a beta. A team owner or admin enrolls the team on the
  project's **API Keys** page in the Console by accepting the beta terms. Without
  enrollment, opening a stream returns `403` with the `feature_unavailable` code.
</Note>

## Before you start

You need:

* a team key (`pmfa_…`) or a project credential (`pmfa_pt_…`) with the
  `events:listen` permission. A project credential can stream only its own
  project. Client tokens cannot open a stream.
* the project ID.

Keep these credentials on your server. Do not use them in a browser.

## Open a stream

```bash theme={null}
curl -N "https://api.polymorfa.com/platform/projects/$PROJECT_ID/events/stream?types=message.*" \
  -H "Authorization: Bearer $POLYMORFA_KEY" \
  -H "Accept: text/event-stream"
```

The response is a server-sent event stream. Each message has an `event` name,
a JSON `data` field, and, for events, an `id`:

```text theme={null}
event: ready
data: {"v":1,"type":"ready","streamId":"…","sequence":1,"projectId":"…","ack":"auto","types":["message.*"],"heartbeatIntervalMs":15000,"idleTimeoutMs":1800000,"connectionExpiresAt":"…",…}

id: lc1.eyJ2Ijox…
event: event
data: {"v":1,"type":"event","streamId":"…","sequence":2,"cursor":"lc1.eyJ2Ijox…","event":{"id":"…","type":"message.received","payloadAvailability":"available","payload":{"encoding":"base64","contentType":"application/json","data":"eyJpZCI6…"},…}}
```

The first message is always `ready`. `sequence` counts messages on this
connection and starts again at 1 on every connection. Use `cursor`, not
`sequence`, to track your position.

## Filter event types

Pass `types` as a comma-separated list, or repeat the parameter. A trailing `*`
matches a prefix:

```text theme={null}
?types=message.*,session.connected
```

Omit `types` to receive every event type. You can send up to 32 filters. The
`ready` message lists the filters the stream applied.

## Read an event

The `event` object in each `event` message has the same fields as a project
event from the [event history API](/api/events): `id`, `type`, `source`,
`environment`, `createdAt`, `payloadAvailability`, `replayableUntil`,
`metadataExpiresAt`, and `payload`.

When `payloadAvailability` is `available`, `payload.data` is the base64 of the
exact webhook body for that event. Decode it to read the same JSON your webhook
endpoint receives.

The body is present only when all of these are true:

* hosted message storage is enabled for the number that produced the event;
* the body is within the storage size limit; and
* that storage is still enabled, and the number is connected, when the stream
  sends the event.

Otherwise `payload` is `null` and `payloadAvailability` explains why:

| Value          | Meaning                                                                                                                                                                                                                                                                                    |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `not_retained` | Hosted message storage was off for the number, so Polymorfa did not keep the body.                                                                                                                                                                                                         |
| `unavailable`  | The body is larger than the limit, storage was turned off or the number disconnected after the event, or the stream could not confirm storage for the body when it sent the event. Retrieve the event from [event history](/api/events#retrieve-payload-bytes) with `includePayload=true`. |

Use the event `id` to deduplicate. The same event has the same `id` on the
stream, in webhooks, and in event history.

## Resume after a disconnect

Every `event` message carries a `cursor`, also sent as the message `id`. To
continue where you stopped, reconnect with the last cursor you processed, either
as the `Last-Event-ID` header or as the `cursor` query parameter:

```bash theme={null}
curl -N "https://api.polymorfa.com/platform/projects/$PROJECT_ID/events/stream?types=message.*" \
  -H "Authorization: Bearer $POLYMORFA_KEY" \
  -H "Accept: text/event-stream" \
  -H "Last-Event-ID: $LAST_CURSOR"
```

Standard server-sent event clients send `Last-Event-ID` automatically when they
reconnect. The stream then delivers the events after that cursor.

A cursor:

* is valid for 24 hours after its event;
* works only with the same team, project, and `types` filter; and
* must not be modified.

A cursor for another project or filter returns `400 stream_cursor_invalid`. An
older cursor returns `410 stream_cursor_expired`: reconnect without a cursor and
read what you missed from [event history](/api/events).

If events after your cursor are no longer kept, the stream sends one `gap`
message with `reason: "retention_exceeded"`, then continues with the next
available event. The message includes `missedEvents` when Polymorfa can count
the missing events; without `missedEvents`, some events may be missing and
their number is unknown. Read the missing events from event history.

## Acknowledge events

By default (`ack=auto`) the stream releases each event as soon as it is written
to the connection. Save the cursor after you process each event, and resume from
it after a disconnect.

To have the stream wait for your processing, open it with `ack=manual`. The
stream then keeps at most `maxInFlight` unacknowledged events outstanding.
Acknowledge everything up to a message with its `cursor` and `sequence`, using
the same credential that opened the stream:

```bash theme={null}
curl -X POST "https://api.polymorfa.com/platform/projects/$PROJECT_ID/events/stream/$STREAM_ID/ack" \
  -H "Authorization: Bearer $POLYMORFA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"cursor":"lc1.eyJ2Ijox…","sequence":42}'
```

Acknowledgements are cumulative. After each acknowledgement the stream sends a
`checkpoint` message with the acknowledged cursor. If an event stays
unacknowledged for 30 seconds, the stream sends `backpressure`; if it is still
unacknowledged 30 seconds later, the stream sends `dropped` and closes. Reconnect
from the last acknowledged cursor.

## Keep the connection healthy

| Message                        | What to do                                                                                                                                                                                                  |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `heartbeat`                    | Sent about every `heartbeatIntervalMs`. Treat a missing heartbeat for twice that interval as a lost connection.                                                                                             |
| `expiry`                       | The connection reached `connectionExpiresAt` (one hour) or had no events for `idleTimeoutMs` (30 minutes). Reconnect from your last cursor.                                                                 |
| `revoked`                      | The credential was revoked or lost `events:listen`, the project or team was deactivated, or the team left the beta. Fix the cause before you reconnect; retrying with the same credential fails until then. |
| `gap` with `recoverable: true` | The stream could not continue. Reconnect from `requestedCursor`.                                                                                                                                            |
| `dropped`                      | Manual acknowledgements stopped. Reconnect from your last acknowledged cursor.                                                                                                                              |

The stream checks your credential, project, and beta enrollment at every
heartbeat, so revocation takes effect within one heartbeat interval.

In `ack=auto` mode, a client that stops reading for 30 seconds is disconnected.

Reconnect with exponential backoff and jitter, starting at one second and
capping at 30 seconds.

## Limits

* A team can have 10 open event streams at once. Another connection returns
  `429 stream_connection_limit_reached` with `Retry-After`.
* Opening streams counts toward a per-credential rate limit. Exceeding it
  returns `429 rate_limit_exceeded` with `Retry-After`.

See [Errors](/api/errors) for recovery steps for each code, and the
[Stream project events](/guides/operate/api/stream-project-events) reference for
the full message schema.
