# 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](/docs/eventsourcingdb/consistency-guarantees#atomic-writes)** explains. With **[preconditions](/docs/eventsourcingdb/preconditions)**, the events are written only if the database is in the state you expect. The tutorial **[Writing Events](/learn/tutorials/first-steps-with-eventsourcingdb/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](/docs/eventsourcingdb/sources)**. |
| `subject` | string | yes | The **[subject](/docs/eventsourcingdb/subjects)** that the event belongs to, a slash-separated path such as `/books/42`. |
| `type` | string | yes | The **[event type](/docs/eventsourcingdb/event-types)**, 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](/docs/eventsourcingdb/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](/docs/eventsourcingdb/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](/docs/eventsourcingdb/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:

```shell
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](/docs/eventsourcingdb/cloud-events)** 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](#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](/docs/eventsourcingdb/api#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](/docs/eventsourcingdb/consistency-guarantees#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`](/docs/eventsourcingdb/preconditions#iseventqlquerytrue)**.

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

```json
{"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`](/docs/eventsourcingdb/errors/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`](/docs/eventsourcingdb/errors/events-missing)**: The request leaves out `events`, or sends an empty list.
- **[`source-missing`](/docs/eventsourcingdb/errors/source-missing)**: An event leaves out its source.
- **[`source-malformed`](/docs/eventsourcingdb/errors/source-malformed)**: The source of an event is not a URI as RFC 3986 defines it.
- **[`subject-missing`](/docs/eventsourcingdb/errors/subject-missing)**: An event, or a precondition on a subject, leaves out its subject.
- **[`subject-malformed`](/docs/eventsourcingdb/errors/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`](/docs/eventsourcingdb/errors/subject-too-long)**: The subject of an event or of a precondition is longer than 1,024 characters.
- **[`subject-too-deep`](/docs/eventsourcingdb/errors/subject-too-deep)**: The subject of an event or of a precondition has more than 32 segments.
- **[`event-type-missing`](/docs/eventsourcingdb/errors/event-type-missing)**: An event leaves out its type.
- **[`event-type-malformed`](/docs/eventsourcingdb/errors/event-type-malformed)**: The type of an event is not a valid event type.
- **[`data-missing`](/docs/eventsourcingdb/errors/data-missing)**: An event leaves out its data.
- **[`data-malformed`](/docs/eventsourcingdb/errors/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`](/docs/eventsourcingdb/errors/traceparent-malformed)**: The `traceparent` of an event is not valid under W3C Trace Context.
- **[`tracestate-malformed`](/docs/eventsourcingdb/errors/tracestate-malformed)**: The `tracestate` of an event is not valid under W3C Trace Context.
- **[`tracestate-without-traceparent`](/docs/eventsourcingdb/errors/tracestate-without-traceparent)**: An event has a `tracestate`, but no `traceparent`.
- **[`precondition-type-missing`](/docs/eventsourcingdb/errors/precondition-type-missing)**: A precondition leaves out its type.
- **[`precondition-unknown`](/docs/eventsourcingdb/errors/precondition-unknown)**: A precondition has a type that does not exist.
- **[`payload-missing`](/docs/eventsourcingdb/errors/payload-missing)**: A precondition leaves out its payload.
- **[`event-id-missing`](/docs/eventsourcingdb/errors/event-id-missing)**: An `isSubjectOnEventId` precondition leaves out its event ID.
- **[`event-id-malformed`](/docs/eventsourcingdb/errors/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`](/docs/eventsourcingdb/errors/query-missing)**: An `isEventQlQueryTrue` precondition leaves out its query.
- **[`query-too-long`](/docs/eventsourcingdb/errors/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`](/docs/eventsourcingdb/errors/precondition-violated)**: A precondition does not hold, so none of the events were written.
- **[`query-malformed`](/docs/eventsourcingdb/errors/query-malformed)**: The query of an `isEventQlQueryTrue` precondition does not parse.
- **[`query-too-deep`](/docs/eventsourcingdb/errors/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`](/docs/eventsourcingdb/errors/query-unevaluable)**: The query of an `isEventQlQueryTrue` precondition fails while it is evaluated, such as on a division by zero.
- **[`query-not-boolean`](/docs/eventsourcingdb/errors/query-not-boolean)**: The query of an `isEventQlQueryTrue` precondition does not return exactly one boolean.
- **[`event-schema-violated`](/docs/eventsourcingdb/errors/event-schema-violated)**: An event does not match the schema that is registered for its event type.
- **[`event-schema-unregistered`](/docs/eventsourcingdb/errors/event-schema-unregistered)**: The type of an event has no schema, while the database requires one for every event type.
- **[`event-schema-malformed`](/docs/eventsourcingdb/errors/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`](/docs/eventsourcingdb/errors/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`](/docs/eventsourcingdb/errors/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`](/docs/eventsourcingdb/errors/capacity-exceeded)**: The events would take the database beyond the number of events that its license permits.
- **[`store-read-only`](/docs/eventsourcingdb/errors/store-read-only)**: The database is in **[read-only mode](/docs/eventsourcingdb/read-only-mode)**, which refuses every write.
- **[`disk-full`](/docs/eventsourcingdb/errors/disk-full)**: The disk is almost full, so the database refuses writes until there is room again.
- **[`restore-in-progress`](/docs/eventsourcingdb/errors/restore-in-progress)**: A restore is running, which refuses writes until it has finished.
- **[`file-unreadable`](/docs/eventsourcingdb/errors/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`](/docs/eventsourcingdb/errors/store-failed)**: The database has failed, after storing an earlier write or a restore failed, and refuses writes.
- **[`indexes-rebuilding`](/docs/eventsourcingdb/errors/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`](/docs/eventsourcingdb/errors/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](/docs/eventsourcingdb/api#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](/docs/eventsourcingdb/limits)**.
- **`429`**: Too many requests were sent, where a write counts as several, see **[Rate Limiting](/docs/eventsourcingdb/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.
