Docs / EventSourcingDB / API / API Overview

API Overview

This page gives an overview of the HTTP API of EventSourcingDB: how its routes are organized, which methods they use, how authentication works, what requests, responses, and streams look like, and how errors are reported. Each route has a page of its own, linked from the list of routes, and the OpenAPI definition describes all of them for tools.

Changed in 1.3

The v1 API Is Deprecated

The v1 API under /api/v1 is deprecated and supported until January 31, 2027, inclusive. After that, there is no guarantee that it remains available, and a later release removes it. Every response of it says so in a Deprecation header and gives the end of support in a Sunset header. Use the v2 API under /api/v2 instead.

Routes#

All routes of the API live below /api/v2, in one of four categories, which form the next segment of their path:

/api/v2/<category>/<route>
  • data is about working with events: writing, reading, and observing them, running EventQL queries, and everything around them, such as subjects, event types, and event schemas.
  • management is about operating the database: backups and restores, verifying the store, auditing its history, and read-only mode.
  • system is about the server itself, without touching the data: ping, health, diagnostics, verifying a token, and the OpenAPI definition.
  • ui serves the bundled management UI only. Its routes are not part of the stable API and may change together with the UI.

The routes of the first three categories are:

Route Method Purpose
/api/v2/data/write-events POST Writes events, guarded by preconditions if you like.
/api/v2/data/read-events QUERY Reads the events of a subject.
/api/v2/data/observe-events QUERY Reads the events of a subject and keeps sending new ones as they are written.
/api/v2/data/run-eventql-query QUERY Runs an EventQL query.
/api/v2/data/register-event-schema POST Registers a schema for an event type.
/api/v2/data/read-subjects QUERY Lists a subject, or a subject and every subject below it.
/api/v2/data/read-event-types QUERY Lists the event types, each with its schema, if it has one.
/api/v2/data/read-event-type QUERY Reads a single event type.
/api/v2/management/create-backup POST Creates a backup, of the whole database or of what follows on a previous backup.
/api/v2/management/restore-backup POST Restores a backup, a full backup followed by its increments.
/api/v2/management/import-events POST Imports the events of another system, one plain CloudEvent per line, into a database without events.
/api/v2/management/export-events QUERY Exports every event as a plain CloudEvent per line, as an import takes it.
/api/v2/management/verify-store POST Verifies the integrity of the store.
/api/v2/management/audit-history POST Audits the chain of events.
/api/v2/management/enable-read-only-mode POST Enables read-only mode until the server is next started.
/api/v2/system/ping POST, GET Tells whether the server is up.
/api/v2/system/health QUERY, GET Reports the health of the server.
/api/v2/system/diagnose POST Produces a diagnosis of the server.
/api/v2/system/verify-token POST Verifies the API token.
/api/v2/system/openapi QUERY, GET Describes the API as an OpenAPI definition.

Methods#

The method of a route follows what the route does:

  • QUERY asks for data that the database already holds, without changing anything, such as reading or observing events. Since such a request carries its parameters in a body, it uses QUERY, the method that RFC 10008 defines for a safe request with a body, rather than GET, which has none.
  • POST tells the database to do something, whether that changes its state, as writing events does, or not, as creating a backup or verifying the store does.
  • GET reaches the routes that load balancers, probes, scrapers such as Prometheus, and API tools ask without a body: ping, health, and the OpenAPI definition answer it in addition to their own method.

With curl, send a QUERY request with -X QUERY. A proxy, load balancer, or firewall in front of EventSourcingDB has to let the QUERY method through, or it rejects every read, including those of the management UI.

Authentication#

Every route requires a valid API token, except for ping and the OpenAPI definition, both of the v2 API and of the deprecated v1 API, and the API catalog. The token is sent in the Authorization header with the Bearer scheme, which is taken regardless of case, followed by one or more spaces and the token:

Authorization: Bearer <API_TOKEN>

A request without a valid token is rejected with 401 Unauthorized, with the header WWW-Authenticate: Bearer, which names the scheme to send the token with.

Requests#

A request body is JSON, and the request names its content type application/json in the Content-Type header, which parameters such as charset=utf-8 may follow; a request to a route that takes a body without it, or with another one, is rejected with 400 Bad Request. Restoring a backup and importing events are the exceptions: they take the backup or the events as an NDJSON stream, with the content type application/x-ndjson. For example, this request reads the events of the subject /books/42:

curl \
  -X QUERY \
  -H "Authorization: Bearer <API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{ "subject": "/books/42" }' \
  http://localhost:3000/api/v2/data/read-events

A body has to have the shape its route expects: a field the route does not know, a value of the wrong JSON type, or null where the route takes no null, is rejected with 400 Bad Request, and the response names where in the body it lies. A body of the right shape is accepted, and what is wrong with its values – a field that is missing, a subject that is malformed, a value that is unknown – is reported as an error in the response, with status 200.

Responses#

Every response with status 200, apart from those of health and the OpenAPI definition, has the same shape, called an envelope:

{
  "type": "...",
  "data": { ... }
}

The type says what the envelope holds, such as event, subject, eventType, row, or error, and the data holds it. data is always there: where there is nothing to hold, it is an empty object.

A route answers either with a single envelope, with the content type application/json, or with a stream of envelopes, with the content type application/x-ndjson. Synthetic answers, such as the one to ping, are CloudEvents in an envelope of the type event.

Understanding NDJSON#

NDJSON stands for newline-delimited JSON: each line is a JSON value of its own, here an envelope, and lines are separated by a single newline. A stream lets a client handle each item as soon as it arrives, rather than waiting for the whole result, which keeps both sides responsive and their memory small, however many events a read returns.

The health route and the OpenAPI definition are the exceptions, and neither is wrapped in an envelope: health follows the health-check format, or OpenMetrics if you ask for it in the Accept header, and the OpenAPI definition is an OpenAPI document.

Streams#

A stream that comes to an end, such as reading events, ends with exactly one end item:

{"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-25T09:00:00Z","predecessorhash":"0000000000000000000000000000000000000000000000000000000000000000","datacontenttype":"application/json","data":{"title":"2001 – A Space Odyssey"},"traceparent":null,"tracestate":null,"hash":"esdb:hash:v2:bf3598c5414095c18051bfa74fa3246f4c1e891488a202e9b80f5ffc68ac9456","signature":null}}
{"type":"end","data":{}}
  • An end means that the stream ended as intended, not that everything succeeded: a stream that carries an error item before its end reports a failure. A stream that breaks off without an end was cut, for example by a dropped connection or a proxy, so its result is incomplete. Over HTTP/1.1, a client that half-closes its connection once it has sent its request ends that request: a stream that reads, such as reading events or running a query, then breaks off without an end, while a write is answered all the same.
  • Exporting events is the exception to the items of a stream: it sends every event as a plain CloudEvent, without envelopes and without end, error, or heartbeat items, so that an import takes it as it is.
  • A stream sends a heartbeat item, with an empty object as its data, whenever it has sent nothing for a second, so that a proxy does not cut a stream that is only waiting. Skip it.
  • The server waits up to 60 seconds for a client to take in each item, and up to 10 seconds for it to take in the end of the stream after its last item. A client that stops reading for longer, for example while it handles an event, loses the connection, so its stream breaks off without an end.
  • Observing events never ends by itself, so it has no end: it runs until the connection closes.

Errors#

When the database refuses what a request asks for, it says so in the response, with status 200 and an envelope of the type error. A single response is that envelope; a stream carries it as an item.

{
  "type": "error",
  "data": {
    "code": "https://docs.eventsourcingdb.io/errors/subject-malformed",
    "description": "subject is malformed: expected a slash-separated path such as '/books/42'",
    "details": {
      "value": "books/42"
    }
  }
}
  • code names the error. It is a URL that leads to the page of the error in this documentation, and it stays the same, so tell errors apart by their code.
  • description says what is wrong and what is expected.
  • details locates the problem, for example by naming the event or the precondition it concerns, and repeats the value that caused it. If there is nothing to locate, it is an empty object.

The overview of the errors lists every code. A status other than 200 reports a problem of the request or the server rather than of what it asks for:

  • 400 Bad Request: the body is not JSON of the shape the route expects, or the request does not name its content type, application/json, or application/x-ndjson for restoring a backup or importing events.
  • 401 Unauthorized: the API token is missing or invalid. The WWW-Authenticate header names the scheme to send it with, Bearer.
  • 404 Not Found: the route does not exist.
  • 405 Method Not Allowed: the route exists, but not with this method.
  • 408 Request Timeout: the body did not arrive in time.
  • 413 Content Too Large: the body exceeds its limit.
  • 429 Too Many Requests: the client sent too many requests, see Rate Limiting. The Retry-After header says how many seconds to wait.
  • 500 Internal Server Error: the server failed unexpectedly.
  • 503 Service Unavailable: the server is shutting down, or, for the health route, its health reports the status fail.

OpenAPI Specification#

The OpenAPI definition of the v2 API is available as JSON at:

http://localhost:3000/api/v2/system/openapi

The definition of the v1 API, which is deprecated and supported until January 31, 2027, inclusive, remains available as YAML at:

http://localhost:3000/.well-known/openapi

Both are listed in the API catalog at /.well-known/api-catalog, as defined by RFC 9727, which lets tools discover the APIs of a server and their descriptions.

Both definitions declare OpenAPI 3.2 and describe each line of a stream under itemSchema, as that version of OpenAPI does. The definition of the v1 API declared OpenAPI 3.1 up to version 1.2, so a tool that reads only OpenAPI 3.1 can no longer read it.

Use these definitions to generate client code, explore the API, or integrate with tools like Postman, Insomnia, or Swagger UI.