# read-events

Reads the events of a **subject**, or of a subject together with **every subject below it**.

- **Route:** `QUERY /api/v2/data/read-events`
- **Category:** `data`
- **Authentication:** required, with the API token
- **Answer:** a stream of NDJSON

A read returns the events of a **[subject](/docs/eventsourcingdb/subjects)**, **oldest first** unless you ask for the reverse order, and lets you narrow them down by their IDs or start at the latest event of a type. It reads the events **up to the latest one at the moment it starts**: events written while it runs are not part of it. To receive those as well, observe the subject with **[`observe-events`](/docs/eventsourcingdb/api/observe-events)**. The tutorial **[Reading Events](/learn/tutorials/first-steps-with-eventsourcingdb/reading-events)** walks through it step by step.

## Request

The body is a JSON object, with the content type `application/json`:

| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `subject` | string | yes | The subject to read, a slash-separated path such as `/books/42`. |
| `options.recursive` | boolean | no | Whether to read the events of every subject below the given one as well. Defaults to `false`, which reads the given subject alone. |
| `options.order` | string | no | `chronological` for the oldest event first, or `antichronological` for the newest first. Defaults to `chronological`. |
| `options.lowerBound` | object | no | The lowest event ID to read. Without it, the range begins at the first event. |
| `options.upperBound` | object | no | The highest event ID to read. Without it, the range ends at the latest event. |
| `options.lowerBound.id`, `options.upperBound.id` | string | yes, in a bound | An event ID, a non-negative integer in a string, without a sign or a leading zero, such as `"23"`. |
| `options.lowerBound.type`, `options.upperBound.type` | string | yes, in a bound | `inclusive` to read the event with that ID as well, or `exclusive` to leave it out. |
| `options.fromLatestEvent` | object | no | Starts the read at the latest event of a type in a subject, that event included, instead of at `lowerBound`. |
| `options.fromLatestEvent.subject` | string | yes, in `fromLatestEvent` | The subject of that event, exactly that one and not a subject below it. |
| `options.fromLatestEvent.type` | string | yes, in `fromLatestEvent` | The event type of that event. |
| `options.fromLatestEvent.ifEventIsMissing` | string | yes, in `fromLatestEvent` | What to read if there is no such event: `read-everything` reads as if `fromLatestEvent` were left out, `read-nothing` reads no events. |

The bounds select **a range of event IDs**, whichever order you read them in; see **[Reading a Specific Range](/learn/tutorials/first-steps-with-eventsourcingdb/reading-events#reading-a-specific-range)**. **`fromLatestEvent` cannot be combined** with `lowerBound` or with the order `antichronological`. It lets a read start at the latest **[snapshot](/docs/eventsourcingdb/snapshots)** of a subject, see **[Reading From the Last Event of a Given Type](/learn/tutorials/first-steps-with-eventsourcingdb/reading-events#reading-from-the-last-event-of-a-given-type)**.

To read the events of `/books/42`:

```shell
curl \
  -X QUERY \
  -H "authorization: Bearer secret" \
  -H "content-type: application/json" \
  -d '{"subject":"/books/42"}' \
  http://localhost:3000/api/v2/data/read-events
```

## Answer

The answer is a **stream of items**, one per line. Each event is an `event` item:

| Type | Data | When |
| --- | --- | --- |
| `event` | the event, a **[CloudEvent](/docs/eventsourcingdb/cloud-events)** with every field it was stored with | For every event that the read finds, in the order that `options.order` asks for. |
| `error` | `code`, `description`, and `details` | When the read cannot go on, see **[Errors](#errors)**. |
| `heartbeat` | empty | While nothing else is sent for a while, to keep the connection alive. |
| `end` | empty | Once, as the last item, when the stream ends cleanly. |

**Heartbeats and the `end` item** work as on every stream of the API, see **[Streams](/docs/eventsourcingdb/api#streams)**. A subject without events, a range beyond the latest event, and a `fromLatestEvent` with `read-nothing` that finds no event are answered with the `end` item alone.

For a database in which `/books/42` holds a `book-acquired` and a `book-borrowed` event, the request above is answered with:

```json
{"type":"event","data":{"specversion":"1.0","id":"0","source":"https://library.eventsourcingdb.io","subject":"/books/42","type":"io.eventsourcingdb.library.book-acquired","time":"2026-10-28T04:36:03.031127Z","predecessorhash":"0000000000000000000000000000000000000000000000000000000000000000","datacontenttype":"application/json","data":{"author":"Arthur C. Clarke","isbn":"978-0756906788","title":"2001 – A Space Odyssey"},"traceparent":null,"tracestate":null,"hash":"esdb:hash:v2:0c93c8369291dbce3e786d0427a0b5e94a7be9867d5d7e7357f5b2557ec049cc","signature":null}}
{"type":"event","data":{"specversion":"1.0","id":"1","source":"https://library.eventsourcingdb.io","subject":"/books/42","type":"io.eventsourcingdb.library.book-borrowed","time":"2026-10-28T04:36:07.365416Z","predecessorhash":"esdb:hash:v2:0c93c8369291dbce3e786d0427a0b5e94a7be9867d5d7e7357f5b2557ec049cc","datacontenttype":"application/json","data":{"borrowedBy":"/readers/23","borrowedUntil":"2026-11-22"},"traceparent":null,"tracestate":null,"hash":"esdb:hash:v2:f3785555ef047f8366866f6b1a9da3e1967c0a911ff030779789ae1c5ef5ac88","signature":null}}
{"type":"end","data":{}}
```

## Errors

The read reports these errors as an `error` item, followed by the `end` item:

- **[`subject-missing`](/docs/eventsourcingdb/errors/subject-missing)**: The request leaves out the subject.
- **[`subject-malformed`](/docs/eventsourcingdb/errors/subject-malformed)**: The subject to read, or the subject of `fromLatestEvent`, is not a valid subject, such as `books/42` without its leading slash.
- **[`subject-too-long`](/docs/eventsourcingdb/errors/subject-too-long)**: The subject to read, or the subject of `fromLatestEvent`, is longer than 1,024 characters.
- **[`subject-too-deep`](/docs/eventsourcingdb/errors/subject-too-deep)**: The subject to read, or the subject of `fromLatestEvent`, has more than 32 segments.
- **[`order-unknown`](/docs/eventsourcingdb/errors/order-unknown)**: `options.order` is neither `chronological` nor `antichronological`.
- **[`event-id-missing`](/docs/eventsourcingdb/errors/event-id-missing)**: A bound leaves out its `id`.
- **[`event-id-malformed`](/docs/eventsourcingdb/errors/event-id-malformed)**: The `id` of a bound is not a non-negative integer without a sign or a leading zero, such as `-1` or `007`.
- **[`bound-type-missing`](/docs/eventsourcingdb/errors/bound-type-missing)**: A bound leaves out its `type`.
- **[`bound-type-unknown`](/docs/eventsourcingdb/errors/bound-type-unknown)**: The `type` of a bound is neither `inclusive` nor `exclusive`.
- **[`from-latest-event-incomplete`](/docs/eventsourcingdb/errors/from-latest-event-incomplete)**: `fromLatestEvent` leaves out its `subject`, its `type`, or `ifEventIsMissing`.
- **[`if-event-is-missing-unknown`](/docs/eventsourcingdb/errors/if-event-is-missing-unknown)**: `ifEventIsMissing` is neither `read-everything` nor `read-nothing`.
- **[`event-type-malformed`](/docs/eventsourcingdb/errors/event-type-malformed)**: The `type` of `fromLatestEvent` is not a valid event type.
- **[`selection-conflicting`](/docs/eventsourcingdb/errors/selection-conflicting)**: The request combines `fromLatestEvent` with `lowerBound`, or with the order `antichronological`.
- **[`range-unsatisfiable`](/docs/eventsourcingdb/errors/range-unsatisfiable)**: The bounds leave no event that could match, such as a `lowerBound` above the `upperBound`, or the event that `fromLatestEvent` starts at lies above the `upperBound`.
- **[`restore-in-progress`](/docs/eventsourcingdb/errors/restore-in-progress)**: A restore is running, which refuses reads until it has finished.
- **[`file-unreadable`](/docs/eventsourcingdb/errors/file-unreadable)**: A file of the store that the read needs is damaged, such as an events file whose checksum no longer matches.
- **[`store-failed`](/docs/eventsourcingdb/errors/store-failed)**: The store refuses reads for good, after a restore that could not empty it again.
- **[`server-shutting-down`](/docs/eventsourcingdb/errors/server-shutting-down)**: The server began to shut down while the read was running. Before the stream begins, the server answers with `503` instead.

## Status Codes

Before the stream begins, the server answers with a status code other than `200` where it cannot take the request at all, as described in **[Errors](/docs/eventsourcingdb/api#errors)**:

- **`400`**: The content type of the body is not `application/json`, or the body is no JSON object of the shape above, for example with a field the route does not know, such as `recursive` outside of `options`, with a value of the wrong JSON type, such as an `id` that is a number rather than a string, or with `null` as a value.
- **`401`**: The API token is missing or wrong.
- **`408`**: The body did not arrive in time.
- **`413`**: The body is larger than its **[limit](/docs/eventsourcingdb/limits)**.
- **`429`**: Too many requests were sent, or the client already holds 256 open streams, see **[Rate Limiting](/docs/eventsourcingdb/rate-limiting)**; the answer says when to try again.
- **`500`**: The server failed unexpectedly before the stream began. Once it has begun, the stream breaks off without its `end` item instead.
- **`503`**: The server is shutting down.
