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>
datais about working with events: writing, reading, and observing them, running EventQL queries, and everything around them, such as subjects, event types, and event schemas.managementis about operating the database: backups and restores, verifying the store, auditing its history, and read-only mode.systemis about the server itself, without touching the data: ping, health, diagnostics, verifying a token, and the OpenAPI definition.uiserves 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:
QUERYasks 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 usesQUERY, the method that RFC 10008 defines for a safe request with a body, rather thanGET, which has none.POSTtells 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.GETreaches 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
endmeans that the stream ended as intended, not that everything succeeded: a stream that carries anerroritem before itsendreports a failure. A stream that breaks off without anendwas 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 anend, 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, orheartbeatitems, so that an import takes it as it is. - A stream sends a
heartbeatitem, with an empty object as itsdata, 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"
}
}
}
codenames 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.descriptionsays what is wrong and what is expected.detailslocates 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, orapplication/x-ndjsonfor restoring a backup or importing events.401 Unauthorized: the API token is missing or invalid. TheWWW-Authenticateheader 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. TheRetry-Afterheader 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 statusfail.
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.