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

# Change Feed

> Pull an ordered, resumable log of everything that changed in your organization, with a 90-day retention window

`GET /v1/events` returns your organization's changes in order. You hold the
position, so a consumer that dies mid-stream resumes exactly where it stopped.

[Webhooks](/docs/operations/webhooks) push the same events to you. The change feed
lets you pull them, which is what you want for a consumer that can be offline,
or one that needs to replay.

## Read the feed

```bash theme={null}
curl -sS "$MP_API_URL/v1/events?limit=100" \
  -H "Authorization: Bearer $MP_API_KEY"
```

```json Response theme={null}
{
  "results": [
    {
      "seq": 84213,
      "cursor": "b3BhcXVlLXN0cmluZw==",
      "namespace_id": "ns_0fda3a08ba",
      "event_type": "collection.documents.written",
      "resource_type": "collection",
      "operation": "insert",
      "payload": { "collection_id": "col_products", "document_ids": ["doc_1"] },
      "created_at": "2026-08-22T19:04:11Z"
    }
  ],
  "next_cursor": "b3BhcXVlLW5leHQ=",
  "has_more": true
}
```

Pass `next_cursor` back as `cursor` to get the next page. Omit `cursor` to start
at the oldest event still inside the retention window.

| Parameter      | Meaning                                                                                                |
| -------------- | ------------------------------------------------------------------------------------------------------ |
| `cursor`       | Opaque position from a prior `next_cursor`.                                                            |
| `namespace_id` | Restrict to one namespace.                                                                             |
| `event_type`   | Restrict to one event type, from the same vocabulary as [webhook subscriptions](/docs/operations/webhooks). |
| `limit`        | 1 to 1,000, default 100.                                                                               |

`results` and `has_more` are always present. An empty page returns the cursor you
sent, unchanged, so a poller can keep the same value and call again.

## The three things that will bite you

<Warning>
  **A cursor is only valid for the filter set that produced it.** It encodes a
  position in your organization's overall sequence, not a position within a
  `namespace_id` or `event_type` filter.

  Change either filter while reusing an old cursor and the feed silently skips
  whatever the previous combination would have matched in between. There is no
  error. Start from a fresh cursor, or none, whenever the filters change.
</Warning>

<Warning>
  **Retention is 90 days and expiry is permanent.** Events older than that are
  deleted, not archived, so there is nothing to resume from.

  A consumer down for less than 90 days resumes from its stored `next_cursor`
  and misses nothing. A consumer down longer has to resync current state
  instead. Budget for that path rather than assuming the cursor always works.
</Warning>

<Warning>
  **`payload` is not a stable contract.** Its shape is per-event-type and
  mirrors what the originating call site passes to the webhook delivery, so it
  can change when that call site changes.

  Read `event_type`, `resource_type`, `operation` and the ids you need, and
  treat everything inside `payload` as advisory. Fetch the resource if you need
  its current state.
</Warning>

## Fields on an event

| Field           | Meaning                                             |
| --------------- | --------------------------------------------------- |
| `seq`           | Monotonic position within your organization's feed. |
| `cursor`        | Opaque cursor pointing at this event.               |
| `namespace_id`  | Namespace scope, when the event has one.            |
| `event_type`    | Same vocabulary as webhook subscriptions.           |
| `resource_type` | First segment of `event_type`.                      |
| `operation`     | `insert`, `update`, `delete`, or `other`.           |
| `payload`       | Per-event-type body. See the warning above.         |
| `created_at`    | When the change happened.                           |

`seq` is monotonic per organization, so it orders events across namespaces. Use
`cursor` to resume; use `seq` when you need to compare two events' order.

## Batch ingest is covered, and how to trust it

Documents created by batch processing appear in the feed the same way direct
API writes do. After the pipeline confirms a flush of documents into storage,
one `collection.documents.written` event lands per flushed chunk, carrying
`namespace_id`, `collection_id`, `batch_id`, `document_ids` and `count`. A
large batch produces hundreds of chunk events rather than one event per
document, so project by fetching the listed ids.

When the batch finishes, the feed carries one terminal
`collection.documents.batch_completed` event per collection the batch wrote,
with a `total_documents` count taken from the store itself. If the store count
fails, `total_documents` is `null`. It is never a guess.

```json A terminal marker theme={null}
{
  "event_type": "collection.documents.batch_completed",
  "payload": {
    "batch_id": "btch_x",
    "collection_id": "col_products",
    "total_documents": 1204,
    "status": "COMPLETED"
  }
}
```

The marker exists so a consumer can verify its own completeness. Take the
union of `document_ids` across the batch's chunk events and compare its size
against `total_documents`. A match means you saw every write. A mismatch means
some chunk events were lost, and the fix is to list the collection's documents
filtered by that `batch_id` and reconcile.

<Warning>
  **Delivery semantics are at-least-once, with one narrow loss window.** A
  chunk event is emitted only after its documents are confirmed in storage, so
  the feed never reports phantom writes. A chunk's event can be lost if the
  writing process dies between the storage confirm and the emit. The terminal
  marker is how you detect that: its store-counted total does not depend on
  the chunk events having survived. Reconcile against it rather than assuming
  the chunk stream is complete.
</Warning>

## Related

* [Webhooks](/docs/operations/webhooks) for push delivery of the same events
* [Observability](/docs/operations/observability)
