Docs / EventSourcingDB / API / observe-events

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 of fromLatestEvent, is not a valid subject, such as books/42 without its leading slash.
  • subject-too-long: The subject to observe, or the subject of fromLatestEvent, is longer than 1,024 characters.
  • subject-too-deep: The subject to observe, or the subject of fromLatestEvent, has more than 32 segments.
  • event-id-missing: lowerBound leaves out its id.
  • event-id-malformed: The id of lowerBound is not a non-negative integer without a sign or a leading zero, such as -1 or 007.
  • bound-type-missing: lowerBound leaves out its type.
  • bound-type-unknown: The type of lowerBound is neither inclusive nor exclusive.
  • from-latest-event-incomplete: fromLatestEvent leaves out its subject, its type, or ifEventIsMissing.
  • if-event-is-missing-unknown: ifEventIsMissing is neither read-everything nor wait-for-event.
  • event-type-malformed: The type of fromLatestEvent is not a valid event type.
  • selection-conflicting: The request combines fromLatestEvent with lowerBound.
  • range-unsatisfiable: lowerBound is 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 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:

  • 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 order or upperBound in 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.
  • 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 an error item instead.
  • 503: The server is shutting down.