# import-events

Imports the **events of another system** into a **database without events**, with their IDs and their times, one plain CloudEvent per line.

- **Route:** `POST /api/v2/management/import-events`
- **Category:** `management`
- **Authentication:** required, with the API token
- **Answer:** a stream of NDJSON

An import brings an existing stock of events into EventSourcingDB, such as when you move to it from another system, or after you removed personal data from an export, as **[GDPR Compliance](/docs/eventsourcingdb/gdpr-compliance)** describes. Unlike **[`write-events`](/docs/eventsourcingdb/api/write-events)**, which gives every event the time of the write, it **keeps the ID and the time of every event**. You send the events without hashes: EventSourcingDB **computes the hash of every event** and the chain of hashes itself, as a write does, so a converter from another system needs to know nothing about them. Each line is exactly what **[`export-events`](/docs/eventsourcingdb/api/export-events)** writes, so an export imports without conversion.

An import goes **only into a database that holds no events**. Schemas are welcome: an import brings none, so **register the schemas of your event types beforehand**, with **[`register-event-schema`](/docs/eventsourcingdb/api/register-event-schema)**, and the import checks every event against the schema of its type. It **checks every line** as it reads it, and until it is over, the database refuses reads and writes. It is **all or nothing**: if it fails, it **removes what it imported so far**, so that the database holds no events again, and **keeps the schemas** that were registered before it.

## Request

The body is a **stream of NDJSON**, with the content type `application/x-ndjson`, and has **no limit on its size**, while each of its lines may hold **at most 390 KiB**, or 399,360 bytes, not counting its newline, which leaves room for every event that the database stores, however its characters are escaped, see **[Limits](/docs/eventsourcingdb/limits#size-limits)**. Each line is **one event as a plain CloudEvent**, with exactly these fields:

| Field | Value |
| --- | --- |
| `specversion` | `1.0` |
| `id` | The ID of the event. The IDs begin at `0` and follow each other **without gaps**, written without a sign or a leading zero, so that a line that went missing or was sent twice is found. |
| `source` | The **[source](/docs/eventsourcingdb/sources)** of the event, a URI as RFC 3986 defines it. |
| `subject` | The **[subject](/docs/eventsourcingdb/subjects)** of the event. |
| `type` | The **[type](/docs/eventsourcingdb/event-types)** of the event. |
| `time` | The time of the event, a timestamp as RFC 3339 defines it, with any offset, such as `2026-11-05T11:00:00+02:00`. It is **stored in UTC**. No time may lie before that of the event before it, nor before `1970-01-01T00:00:00Z`. |
| `datacontenttype` | `application/json` |
| `data` | The data of the event, a JSON object. |
| `traceparent` | Optional: the traceparent of the event, as W3C Trace Context defines it. Leave the field out where the event has none; `null` is refused. |
| `tracestate` | Optional: the tracestate of the event, as W3C Trace Context defines it. Leave the field out where the event has none; `null` is refused. A tracestate requires a `traceparent`, as W3C Trace Context does, and as writing events requires it. |

Any other field is refused, among them `hash`, `predecessorhash`, and `signature`, which the import computes itself, or has no key to, and extensions of CloudEvents. An **empty body** imports no events, so that the export of a database without events imports as well.

For example, a body of two events, each on a line of its own:

```json
{"specversion":"1.0","id":"0","source":"https://library.eventsourcingdb.io","subject":"/books/42","type":"io.eventsourcingdb.library.book-acquired","time":"2026-11-05T09:00:00Z","datacontenttype":"application/json","data":{"title":"2001 – A Space Odyssey","author":"Arthur C. Clarke"}}
{"specversion":"1.0","id":"1","source":"https://library.eventsourcingdb.io","subject":"/books/42","type":"io.eventsourcingdb.library.book-borrowed","time":"2026-11-05T11:00:00+02:00","datacontenttype":"application/json","data":{"borrowedBy":"/readers/23"}}
```

Send the file with `--data-binary`, which keeps its line breaks: `-d` removes them, and the import then refuses the body as malformed.

```shell
curl \
  -X POST \
  -H "authorization: Bearer secret" \
  -H "content-type: application/x-ndjson" \
  --data-binary @events.ndjson \
  http://localhost:3000/api/v2/management/import-events
```

### Checking That Nothing Is Missing

An import **does not check that the stock is complete**: a file that was cut at the end of a line imports as a smaller stock, since nothing in it says how many events it should hold. Before you import, compare the stock with its source, such as the number of its lines with the number of events in the other system, and that the ID of its last line is that number less one. After the import, read the latest event, for example with **[`read-events`](/docs/eventsourcingdb/api/read-events)** on the subject `/` with the options `recursive` set to `true` and `order` set to `antichronological`, taking only its first event, and compare its ID with that of the last line of the stock. **[`export-events`](/docs/eventsourcingdb/api/export-events)** describes how to check an export.

## Answer

The answer is a **stream of items**, one per line, which tells the outcome of the import:

| Type | Data | When |
| --- | --- | --- |
| `error` | `code`, `description`, and `details` | When the events cannot be imported, see **[Errors](#errors)**. |
| `heartbeat` | empty | Once all of the events have been read, while nothing else is sent for a while and the import still runs. |
| `end` | empty | Once, as the last item, when the stream ends cleanly. Without an `error` item before it, the events were imported. |

**Heartbeats and the `end` item** work as on every stream of the API, see **[Streams](/docs/eventsourcingdb/api#streams)**, with one difference: an import sends **heartbeats only once it has read all of the events**. While the events are still being sent, the data of the client keeps the connection alive.

A client that goes away **while it sends the events** breaks off reading them, which fails the import, as **[`import-unreadable`](/docs/eventsourcingdb/errors/import-unreadable)** describes. **Once all of the events have been read**, though, the import **runs to its end** even if its client is gone.

An import that succeeds is answered with the `end` item alone:

```json
{"type":"end","data":{}}
```

If the server crashes while it imports, the import leaves a **mark** in the data directory, and EventSourcingDB **refuses to start** on it with `the data directory holds an unfinished import; empty it, register the event schemas again, and import the events again`, since what is in the directory then is partly imported. Empty the data directory, start EventSourcingDB, register the schemas, and import the events again.

## Errors

The import reports these errors as an `error` item, followed by the `end` item. After any of them, the database holds **no events**, as before the import, and **keeps its schemas**, since an import that has begun removes what it imported so far:

- **[`license-invalid`](/docs/eventsourcingdb/errors/license-invalid)**: The license expired while the server ran. It is checked before any line of the events.
- **[`import-unreadable`](/docs/eventsourcingdb/errors/import-unreadable)**: Reading the body broke off, because the connection dropped or the client sent nothing for more than five seconds.
- **[`import-malformed`](/docs/eventsourcingdb/errors/import-malformed)**: A line is not an event that an import takes, such as an empty line, a line with a field that a line does not have, a line with `null` for a field, an event without an ID, a time, or a subject, or an event with a `tracestate` but no `traceparent`. This includes a line that is longer than any event of an import can be, and an event dated before 1970.
- **[`store-not-empty`](/docs/eventsourcingdb/errors/store-not-empty)**: The database holds events. This is checked before the first line is read, also for an empty body.
- **[`event-chain-broken`](/docs/eventsourcingdb/errors/event-chain-broken)**: The ID of an event does not follow on the one before it, as where the first ID is not `0`, a line went missing, or a line is there twice, or the time of an event lies before that of the event before it.
- **[`import-ahead-of-clock`](/docs/eventsourcingdb/errors/import-ahead-of-clock)**: An event lies more than an hour beyond the clock of the server, such as one dated `9999-12-31`. The same events import once the clock has caught up.
- **[`event-too-large`](/docs/eventsourcingdb/errors/event-too-large)**: An event is larger than the maximum event size, its hash included, measured as it is stored rather than as its line writes it.
- **[`subject-too-long`](/docs/eventsourcingdb/errors/subject-too-long)**: The subject of an event is longer than 1,024 characters.
- **[`subject-too-deep`](/docs/eventsourcingdb/errors/subject-too-deep)**: The subject of an event has more than 32 segments.
- **[`event-schema-violated`](/docs/eventsourcingdb/errors/event-schema-violated)**: An event does not match the schema that was registered for its type.
- **[`event-schema-unregistered`](/docs/eventsourcingdb/errors/event-schema-unregistered)**: The server runs with `--require-event-schemas`, and an event has a type for which no schema was registered.
- **[`event-schema-malformed`](/docs/eventsourcingdb/errors/event-schema-malformed)**: The schema of 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 holds.
- **[`event-schema-too-complex`](/docs/eventsourcingdb/errors/event-schema-too-complex)**: The schema of the type of an event refers back to a part of itself, or reaches more than 10,000 subschemas, as one that an earlier version registered can.
- **[`capacity-exceeded`](/docs/eventsourcingdb/errors/capacity-exceeded)**: The import holds more events than the license permits.
- **[`indexes-rebuilding`](/docs/eventsourcingdb/errors/indexes-rebuilding)**: The indexes are still being rebuilt after an upgrade, which an import needs complete to tell whether the database holds events.
- **[`store-read-only`](/docs/eventsourcingdb/errors/store-read-only)**: The database is in read-only mode.
- **[`disk-full`](/docs/eventsourcingdb/errors/disk-full)**: The disk is almost full, so the database refuses writes.
- **[`restore-in-progress`](/docs/eventsourcingdb/errors/restore-in-progress)**: A restore or another import is running.
- **[`file-unreadable`](/docs/eventsourcingdb/errors/file-unreadable)**: A file of the store that the import needs is damaged, such as the schema file of the type of an event, which was registered before the import, or an events file whose checksum no longer matches.
- **[`store-failed`](/docs/eventsourcingdb/errors/store-failed)**: The store refuses writes after a failure to store one, or reads and writes after a restore or an import that could not empty it again.
- **[`server-shutting-down`](/docs/eventsourcingdb/errors/server-shutting-down)**: The server began to shut down after the import had sent a heartbeat. Before that, the server answers with `503` instead.

The errors of an event name it by its ID, which is the ID that its line gives, and the errors of a line name it by its number.

## 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/x-ndjson`.
- **`401`**: The API token is missing or wrong.
- **`429`**: Too many requests were sent; the answer says when to try again. An import counts as a read, as a restore does.
- **`500`**: The server failed unexpectedly before the stream began, for example because an import that failed could not empty the database again. Once the stream has begun, it breaks off without its `end` item instead.
- **`503`**: The server is shutting down, or began to shut down before the stream began, which includes the whole time the events are being sent.
