Docs / EventSourcingDB / API / write-events

write-events

Writes one or more events, guarded by preconditions if you like.

  • Route: POST /api/v2/data/write-events
  • Category: data
  • Authentication: required, with the API token
  • Answer: a stream of NDJSON

You send each event as a candidate, with its source, subject, type, and data, and EventSourcingDB adds the rest, such as its ID, its time, and its hash. A write is atomic: either all of its events are written or none of them, even if they belong to different subjects, as Consistency Guarantees explains. With preconditions, the events are written only if the database is in the state you expect. The tutorial Writing Events walks through it step by step.

Request#

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

Field Type Required Meaning
events array yes The events to write, at least one, each an object as described below.
preconditions array no The preconditions that have to hold for the events to be written, each an object as described below. Without it, the write checks no precondition.

Each event in events is an object:

Field Type Required Meaning
source string yes Where the event comes from, a URI such as https://library.eventsourcingdb.io, see Sources.
subject string yes The subject that the event belongs to, a slash-separated path such as /books/42.
type string yes The event type, a reverse domain name such as io.eventsourcingdb.library.book-acquired.
data object yes The content of the event, a JSON object, which may be empty.
traceparent string no The traceparent of W3C Trace Context, which ties the event to a trace, see Integrating OpenTelemetry.
tracestate string no The tracestate of W3C Trace Context, only together with traceparent.

An event may take at most 64 KiB as it is stored, including its hash, see Limits.

Each precondition in preconditions is an object with a type and a payload, whose fields depend on the type:

Field Type Required Meaning
type string yes The type of the precondition: isSubjectPristine, isSubjectPopulated, isSubjectOnEventId, or isEventQlQueryTrue.
payload object yes The values of the precondition, as in the following rows.
payload.subject string for the first three types The subject to check, for isSubjectPristine, isSubjectPopulated, and isSubjectOnEventId.
payload.eventId string for isSubjectOnEventId The ID that the latest event of the subject has to have, a non-negative integer in a string, without a sign or a leading zero, such as "0".
payload.query string for isEventQlQueryTrue An EventQL query that has to return exactly one boolean, true for the precondition to hold. It may have at most 100 KiB.
payload.options.recursive boolean no For the first three types: whether the precondition applies to the subject together with every subject below it. Defaults to false, which checks the subject alone.

isSubjectPristine holds if the subject has no events, isSubjectPopulated if it has at least one, and isSubjectOnEventId if its latest event has the given ID. A payload takes only the fields of its type: an eventId in an isSubjectPristine precondition, for example, is answered with 400.

To acquire the book /books/42, but only if nothing has been written to its subject yet:

curl \
  -X POST \
  -H "authorization: Bearer secret" \
  -H "content-type: application/json" \
  -d '{
    "events": [
      {
        "source": "https://library.eventsourcingdb.io",
        "subject": "/books/42",
        "type": "io.eventsourcingdb.library.book-acquired",
        "data": {
          "title": "2001 – A Space Odyssey",
          "author": "Arthur C. Clarke",
          "isbn": "978-0756906788"
        }
      }
    ],
    "preconditions": [
      {
        "type": "isSubjectPristine",
        "payload": {
          "subject": "/books/42"
        }
      }
    ]
  }' \
  http://localhost:3000/api/v2/data/write-events

Answer#

The answer is a stream of items, one per line. Once the write is done, each written event is an event item:

Type Data When
event the event as it was written, a CloudEvent with the fields that EventSourcingDB adds, such as id, time, and hash For every written event, in the order of the request, once all of them are written.
error code, description, and details When the write is refused, see Errors.
heartbeat empty While the write is still running, 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. A write that ends with 500, or whose stream breaks off without its end item, leaves open whether its events were written. If you send it again, a precondition such as isSubjectOnEventId keeps it from being written twice, see Preventing Duplicate Writes.

A write runs to its end once its request has arrived, also if its client goes away meanwhile, so a client that goes away cannot tell whether its write was carried out. How long its isEventQlQueryTrue preconditions hold up the other writes is bounded, see isEventQlQueryTrue.

On an empty database, the request above is answered with:

{"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":"end","data":{}}

Errors#

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

  • license-invalid: The license of the database has expired, so no events can be written. This is reported before any value of the request is checked.
  • events-missing: The request leaves out events, or sends an empty list.
  • source-missing: An event leaves out its source.
  • source-malformed: The source of an event is not a URI as RFC 3986 defines it.
  • subject-missing: An event, or a precondition on a subject, leaves out its subject.
  • subject-malformed: The subject of an event or of a precondition is not a valid subject, such as books/42 without its leading slash.
  • subject-too-long: The subject of an event or of a precondition is longer than 1,024 characters.
  • subject-too-deep: The subject of an event or of a precondition has more than 32 segments.
  • event-type-missing: An event leaves out its type.
  • event-type-malformed: The type of an event is not a valid event type.
  • data-missing: An event leaves out its data.
  • data-malformed: The data of an event holds a number beyond the range of a 64-bit floating-point number, such as 1e400, or one with more than 1000 digits.
  • traceparent-malformed: The traceparent of an event is not valid under W3C Trace Context.
  • tracestate-malformed: The tracestate of an event is not valid under W3C Trace Context.
  • tracestate-without-traceparent: An event has a tracestate, but no traceparent.
  • precondition-type-missing: A precondition leaves out its type.
  • precondition-unknown: A precondition has a type that does not exist.
  • payload-missing: A precondition leaves out its payload.
  • event-id-missing: An isSubjectOnEventId precondition leaves out its event ID.
  • event-id-malformed: The event ID of an isSubjectOnEventId precondition is not a non-negative integer without a sign or a leading zero, such as -1 or 007.
  • query-missing: An isEventQlQueryTrue precondition leaves out its query.
  • query-too-long: The query of an isEventQlQueryTrue precondition is longer than 100 KiB. This is checked before the events of the write are validated and before the write waits for the other writes, so it is reported ahead of a violated event schema, a precondition that does not hold, read-only mode, and a full disk.
  • precondition-violated: A precondition does not hold, so none of the events were written.
  • query-malformed: The query of an isEventQlQueryTrue precondition does not parse.
  • query-too-deep: The query of an isEventQlQueryTrue precondition nests more than 256 levels, or holds a chain of more than 10,000 operands.
  • query-unevaluable: The query of an isEventQlQueryTrue precondition fails while it is evaluated, such as on a division by zero.
  • query-not-boolean: The query of an isEventQlQueryTrue precondition does not return exactly one boolean.
  • event-schema-violated: An event does not match the schema that is registered for its event type.
  • event-schema-unregistered: The type of an event has no schema, while the database requires one for every event type.
  • event-schema-malformed: The schema that is stored for the type of an event holds a number with more than 1,000 digits before or after its decimal point, which only a data directory changed by hand can hold.
  • event-schema-too-complex: The schema that is stored for the type of an event refers back to a part of itself, or reaches more than 10,000 subschemas, which only an earlier version registered, so every write of the type is refused rather than validated.
  • event-too-large: An event takes more than 64 KiB as it is stored, including its hash. This is checked before the event is checked against the schema of its type, so it is reported even where the event does not match that schema.
  • capacity-exceeded: The events would take the database beyond the number of events that its license permits.
  • store-read-only: The database is in read-only mode, which refuses every write.
  • disk-full: The disk is almost full, so the database refuses writes until there is room again.
  • restore-in-progress: A restore is running, which refuses writes until it has finished.
  • file-unreadable: A file of the store that the write needs is damaged, such as an events file whose checksum no longer matches.
  • store-failed: The database has failed, after storing an earlier write or a restore failed, and refuses writes.
  • indexes-rebuilding: The write has an isEventQlQueryTrue precondition, while the indexes that its query needs are still being rebuilt after an upgrade.
  • server-shutting-down: The server began to shut down while the write was running, after the stream had sent a heartbeat. Before that, 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 id in an event, with a field that the type of a precondition does not take, with a value of the wrong JSON type, such as data that is no object, or with null as a value, such as "traceparent": null.
  • 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, where a write counts as several, 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 its end item instead.
  • 503: The server is shutting down.