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

# The event stream

> Read your account's durable event stream with cursors and gap detection, tail it live, and replay slices of history to your webhooks.

Webhooks push events to you; the event stream lets you pull them. Every event on your account is written to a durable, ordered stream you can page through, tail live, or re-send to your webhooks — so downtime never loses an event.

## Read the stream

`GET /v1/events` pages the stream oldest-first, so you read events in the order they happened. Each page is the [standard list envelope](/concepts/the-envelope) — a `data` array and a `pagination` cursor — plus a `gaps` array unique to this stream. Reading uses the `events:read` scope.

<CodeGroup>
  ```bash title="cURL" theme={null}
  curl "https://api.dairo.app/v1/events?limit=50" \
    -H "Authorization: Bearer $DAIRO_API_KEY"
  ```

  ```ts title="TypeScript" theme={null}
  const page = await dairo.events.list({ limit: 50 });
  for (const e of page.data) console.log(e.seq, e.type, e.createdAt);
  if (page.gaps.length) console.warn("missing sequences:", page.gaps);
  ```

  ```python title="Python" theme={null}
  page = dairo.events.list(limit=50)
  for e in page.data:
      print(e.seq, e.type, e.created_at)
  ```

  ```text title="MCP" theme={null}
  Tool: list_events  (scope events:read)
  { "action": "list", "limit": 50 }
  { "action": "list", "type": "message.received", "inboxId": "7c9e6679-7425-40de-944b-e07fc1f90ae7" }
  { "action": "list", "order": "newest", "limit": 10 }
  ```
</CodeGroup>

A page looks like this:

```json theme={null}
{
  "object": "list",
  "data": [
    {
      "object": "event",
      "eventId": "evt_9f8c2a1b4e7d4c1a8b2d3e4f5a6b7c8d",
      "type": "message.received",
      "seq": 412,
      "partitionKey": "inbox:7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "inboxId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "threadId": "b2c3d4e5-6f70-4a81-92b3-c4d5e6f708a9",
      "idempotencyKey": null,
      "outboundMessageId": null,
      "messageId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "providerMessageId": null,
      "occurredAt": "2026-06-12T10:00:00Z",
      "createdAt": "2026-06-12T10:00:01Z",
      "data": {
        "messageId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "from": "customer@example.com",
        "subject": "Re: Your receipt"
      }
    }
  ],
  "pagination": { "nextCursor": "eyJjcmVhdGVkQXQiOi…", "hasMore": true },
  "gaps": []
}
```

Each entry carries the same `eventId`, `type`, and `data` as the webhook delivery for that event, and adds the stream's join keys: the per-partition sequence number `seq` and the loop-wide `idempotencyKey` threaded from the originating send. Page forward with the cursor until `pagination.nextCursor` comes back `null`.

| Parameter        | Description                                                                                                          |
| ---------------- | -------------------------------------------------------------------------------------------------------------------- |
| `limit`          | Events per page, 1–100. Defaults to 50.                                                                              |
| `since`          | Opaque cursor from a prior page's `pagination.nextCursor`. `cursor` is accepted as an alias.                         |
| `inboxId`        | Only events for one inbox.                                                                                           |
| `type`           | Only one event type, for example `message.bounced`.                                                                  |
| `idempotencyKey` | Every event carrying this idempotency key — the full story of one send.                                              |
| `order`          | `oldest` (default) reads forward for catch-up; `newest` returns a recency view with the most recent event on page 1. |
| `wait`           | Long-poll hold in seconds, 0–12. See the tail loop below.                                                            |
| `tail`           | When `true`, returns `data: []` plus the current head cursor.                                                        |

## Detect gaps

Every event carries a `seq` — a contiguous, 1-based sequence number within its partition. The `partitionKey` names the resource the events belong to, such as `inbox:<id>` or `letter:<id>`. If a page's window is missing a sequence number it should contain, the page reports it under `gaps` as `{ "partitionKey": …, "missingSeq": [...] }` — a lost event is something you can see and act on, never a silent hole.

Detection is window-local: it checks the sequence range present on the returned page, not the whole history. On a filtered read (`type` or `inboxId`), events excluded by the filter surface as gaps too — expected, not lost. Audit gaps on unfiltered pages.

## Tail a live feed

To follow events in near-real time, seed at the current head with `tail: true`, then long-poll forward with `wait`. A request with `wait` set holds until a matching event arrives or the seconds elapse (capped at 12), so a steady listener never hammers the API; if nothing arrives in time you get an empty page and poll again. When events are already waiting, they return immediately.

<CodeGroup>
  ```ts title="TypeScript" theme={null}
  // Seed at the head, then long-poll forward.
  const head = await dairo.events.list({ tail: true });
  let cursor = head.pagination.nextCursor;
  for (;;) {
    const page = await dairo.events.list({ cursor, wait: 12 });
    for (const e of page.data) handle(e);
    cursor = page.pagination.nextCursor ?? cursor;
  }
  ```

  ```python title="Python" theme={null}
  # tail/wait are query parameters on GET /v1/events — call the endpoint directly.
  import os, requests

  url = "https://api.dairo.app/v1/events"
  headers = {"Authorization": f"Bearer {os.environ['DAIRO_API_KEY']}"}

  head = requests.get(url, headers=headers, params={"tail": "true"}).json()
  cursor = head["pagination"]["nextCursor"]
  while True:
      page = requests.get(url, headers=headers, params={"cursor": cursor, "wait": 12}).json()
      for event in page["data"]:
          handle(event)
      cursor = page["pagination"]["nextCursor"] or cursor
  ```

  ```bash title="CLI" theme={null}
  # The CLI wraps tail + long-poll into one streaming command.
  dairo listen
  ```
</CodeGroup>

`tail: true` returns `data: []` plus the current head cursor (or `null` on an empty stream), so your next poll streams only events from that point on. It honors the same `inboxId` and `type` filters as a normal read. `wait: 0` — the default — returns immediately.

## Replay to your webhooks

`POST /v1/events/replay` re-sends a slice of history through your webhooks — to recover after your receiver was down, or to backfill a newly added endpoint. Replay uses the `events:write` scope. Give exactly one lower bound:

| Lower bound            | Use                                                            |
| ---------------------- | -------------------------------------------------------------- |
| `since`                | A cursor from a prior page.                                    |
| `sinceSeq` + `inboxId` | Replay one inbox partition from a sequence number (inclusive). |
| `sinceTimestamp`       | An RFC 3339 timestamp.                                         |

<CodeGroup>
  ```bash title="cURL" theme={null}
  curl -X POST https://api.dairo.app/v1/events/replay \
    -H "Authorization: Bearer $DAIRO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{ "sinceTimestamp": "2026-06-10T00:00:00Z", "maxEvents": 500 }'
  ```

  ```ts title="TypeScript" theme={null}
  const result = await dairo.events.replay({
    sinceTimestamp: "2026-06-10T00:00:00Z",
    maxEvents: 500,
  });
  console.log(result.replayed, result.skipped);
  ```

  ```python title="Python" theme={null}
  result = dairo.events.replay(since_timestamp="2026-06-10T00:00:00Z", max_events=500)
  print(result.replayed, result.skipped)
  ```

  ```text title="MCP" theme={null}
  Tool: replay_event  (scope events:write, confirm required)
  { "action": "replay", "sinceTimestamp": "2026-06-10T00:00:00Z", "maxEvents": 500, "confirm": true }
  ```
</CodeGroup>

The response counts what happened and names the first and last event of the slice:

```json theme={null}
{
  "replayed": 212,
  "skipped": 3,
  "from": {
    "eventId": "evt_2c1a8b2d3e4f5a6b7c8d9f8c2a1b4e7d",
    "seq": 201,
    "partitionKey": "inbox:7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "createdAt": "2026-06-10T00:02:11Z"
  },
  "to": {
    "eventId": "evt_9f8c2a1b4e7d4c1a8b2d3e4f5a6b7c8d",
    "seq": 412,
    "partitionKey": "inbox:7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "createdAt": "2026-06-12T10:00:01Z"
  }
}
```

Replay goes to your active webhooks, re-matched per event on their subscribed types; `skipped` counts events no active subscription matched. Narrow a slice with `until` (an upper timestamp), `types` (an event-type list), or `inboxId`. Pass `webhookId` to replay to one endpoint only — useful for backfilling a new receiver without re-firing the others; a paused or unknown `webhookId` returns a `404`. Events are re-sent in `(partitionKey, seq)` order, and every replayed delivery is recorded in that webhook's delivery log.

`maxEvents` caps the slice at 1–5,000 events (default 1,000). A slice that exceeds the cap is refused with a `400` rather than silently truncated — narrow the range or page through it with `until`.

<Tip>
  Replayed events travel the normal signing and delivery path with the same event ID, so your receiver verifies and deduplicates them exactly like live ones. Keep your handler idempotent — a replay can re-send events you already processed.
</Tip>

## Next steps

* [Webhooks](/webhooks/webhooks) — push delivery, signature verification, and the full event catalog.
* [Track delivery](/sending/outbound-tracking) — delivery, bounce, and complaint events per message.
* [CLI](/agent-first/cli) — `dairo listen`, the live tail built on this stream.
* [API reference](/api-reference) — `GET /v1/events` and `POST /v1/events/replay`.
