observe-events
Reads the events of a subject and keeps sending new ones as they are written.
- Route:
QUERY /api/v2/data/observe-events - Category:
data - Authentication: required, with the API token
- Answer: a stream of NDJSON
An observation first sends the events that a subject already holds, oldest first, and then each new event once it is written, over the same connection. It never ends by itself, so it suits whatever has to follow the events as they happen, such as a projection or a read model. If the connection drops, observe again from the last event you received, as Resuming After Connection Loss shows. The tutorial Observing 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 observe, a slash-separated path such as /books/42. |
options.recursive |
boolean | no | Whether to observe every subject below the given one as well. Defaults to false, which observes the given subject alone. |
options.lowerBound |
object | no | The event ID to start at. Without it, the observation starts at the first event. |
options.lowerBound.id |
string | yes, in lowerBound |
An event ID, a non-negative integer in a string, without a sign or a leading zero, such as "23". |
options.lowerBound.type |
string | yes, in lowerBound |
inclusive to send the event with that ID as well, or exclusive to leave it out. |
options.fromLatestEvent |
object | no | Starts the observation 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 do if there is no such event: read-everything observes as if fromLatestEvent were left out, wait-for-event sends nothing until an event of that type is written to that subject, and every event from that one on. |
An observation always goes on in the order in which the events were written, so it takes neither an order nor an upperBound. fromLatestEvent cannot be combined with lowerBound, see Observing From the Last Event of a Given Type.
To go on observing /books/42 after the event with the ID 0:
curl \
-X QUERY \
-H "authorization: Bearer secret" \
-H "content-type: application/json" \
-d '{"subject":"/books/42","options":{"lowerBound":{"id":"0","type":"exclusive"}}}' \
http://localhost:3000/api/v2/data/observe-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 with every field it was stored with | For every event that the observation covers, first those already written, then each new one once it is written, in the order in which they were written. |
error |
code, description, and details |
When the observation cannot go on, which ends the stream, see Errors. |
heartbeat |
empty | While nothing else is sent for a while, to keep the connection alive. |
Heartbeats work as on every stream of the API, see Streams. Since an observation never ends by itself, there is no end item: the stream runs until you close the connection, or until an error ends it.
For a database in which /books/42 holds a book-acquired event with the ID 0 and a book-borrowed event with the ID 1, and into which a book-returned event is written while the observation runs, the request above is answered with these items, and the stream goes on until the connection is closed:
{"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":"heartbeat","data":{}}
{"type":"event","data":{"specversion":"1.0","id":"2","source":"https://library.eventsourcingdb.io","subject":"/books/42","type":"io.eventsourcingdb.library.book-returned","time":"2026-10-28T04:36:19.677736Z","predecessorhash":"esdb:hash:v2:f3785555ef047f8366866f6b1a9da3e1967c0a911ff030779789ae1c5ef5ac88","datacontenttype":"application/json","data":{"returnedBy":"/readers/23"},"traceparent":null,"tracestate":null,"hash":"esdb:hash:v2:071dcd6fc73535ed7caa99060d69bd6905ce5d67f7c8479b05b4fb361d5f45ea","signature":null}}
{"type":"heartbeat","data":{}}
Errors#
The observation reports these errors as an error item, which ends the stream without an end item:
subject-missing: The request leaves out the subject.subject-malformed: The subject to observe, or the subject offromLatestEvent, is not a valid subject, such asbooks/42without its leading slash.subject-too-long: The subject to observe, or the subject offromLatestEvent, is longer than 1,024 characters.subject-too-deep: The subject to observe, or the subject offromLatestEvent, has more than 32 segments.event-id-missing:lowerBoundleaves out itsid.event-id-malformed: TheidoflowerBoundis not a non-negative integer without a sign or a leading zero, such as-1or007.bound-type-missing:lowerBoundleaves out itstype.bound-type-unknown: ThetypeoflowerBoundis neitherinclusivenorexclusive.from-latest-event-incomplete:fromLatestEventleaves out itssubject, itstype, orifEventIsMissing.if-event-is-missing-unknown:ifEventIsMissingis neitherread-everythingnorwait-for-event.event-type-malformed: ThetypeoffromLatestEventis not a valid event type.selection-conflicting: The request combinesfromLatestEventwithlowerBound.range-unsatisfiable:lowerBoundis exclusive at the largest possible event ID, which leaves no event to observe.restore-in-progress: A restore is running as the observation starts, or starts while it runs, which ends the observation.file-unreadable: A file of the store that the observation needs is damaged, such as an events file whose checksum no longer matches.store-failed: The store refuses reads for good, after a restore that could not empty it again.server-shutting-down: The server began to shut down while the observation was running. Before the stream begins, the server answers with503instead.
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:
400: The content type of the body is notapplication/json, or the body is no JSON object of the shape above, for example with a field the route does not know, such asorderorupperBoundinoptions, with a value of the wrong JSON type, such as anidthat is a number rather than a string, or withnullas 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.429: Too many requests were sent, or the client already holds 256 open streams, see 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 anerroritem instead.503: The server is shutting down.