# Migrating to the v2 API

EventSourcingDB 1.3 introduces the **v2 API** and **deprecates the v1 API**. The v1 API is **supported until January 31, 2027**, inclusive, and a **later release removes it**. This page explains what changes, route by route, so that you can move a client from v1 to v2.

Both APIs run **side by side** in 1.3, on the same port and the same data, with the same API token. You can therefore move **one route at a time**: what one client writes through v2, another reads through v1, and the other way round. Every response of the v1 API carries a **`Deprecation`** header and a **`Sunset`** header, so a proxy or a log shows which clients still use it.

If you use an **[official client SDK](/docs/eventsourcingdb/sdks)**, update it to its version for EventSourcingDB 1.3, which speaks v2 by itself.

## What Stays the Same

The **events** do not change: a client writes and reads the same **[CloudEvents](/docs/eventsourcingdb/cloud-events)**, with the same **[subjects](/docs/eventsourcingdb/subjects)**, **[event types](/docs/eventsourcingdb/event-types)**, and **[sources](/docs/eventsourcingdb/sources)**. **[Preconditions](/docs/eventsourcingdb/preconditions)**, **[EventQL](/docs/eventsourcingdb/eventql)**, the options of reading and observing, such as bounds and `fromLatestEvent`, and the API token work as before.

## What Changes Everywhere

- **Routes** live below `/api/v2/<category>/<route>`, in the categories `data`, `management`, and `system`, and some of them have new names, as the **[table below](#routes)** shows.
- **Reads use `QUERY`** rather than `POST`, since they change nothing. A proxy, load balancer, or firewall in front of EventSourcingDB has to let `QUERY` through.
- Every item of a response is an **envelope** whose content is in **`data`**, where v1 named it `payload`. A **single answer**, such as the one to reading an event type or to the diagnosis, is an envelope as well, where v1 sent it **bare**.
- **Every finite stream ends with an `end` item**, and a stream that breaks off without it was cut. **Every stream sends `heartbeat` items** while it has nothing else to send, including writing events and registering a schema, whose answers are streams now.
- **Errors are reported in the response**, with status `200` and an item of the type `error` that names its **[code](/docs/eventsourcingdb/errors)**, rather than through status codes such as `409` or `404`. See **[Errors](#errors)**.
- As in v1, a body has to have **exactly the shape a route expects**: a field the route does not know, or `null` where it takes none, is rejected with `400`, and the response names where it lies. **A missing field or a wrong value**, which v1 refused with `400` as well, is reported as an **error item** with status `200`.
- A request to a route that takes a body has to name its **content type** in the `Content-Type` header, `application/json`, or `application/x-ndjson` for restoring a backup or importing events, which parameters such as `charset=utf-8` may follow. A request without it, or with another one, is rejected with `400`; v1 does not check it.
- Answers that the server makes up itself, such as the one to ping, are **CloudEvents** in an envelope of the type `event`.

## Routes

| v1 | v2 |
| --- | --- |
| `POST /api/v1/write-events` | `POST /api/v2/data/write-events` |
| `POST /api/v1/read-events` | `QUERY /api/v2/data/read-events` |
| `POST /api/v1/observe-events` | `QUERY /api/v2/data/observe-events` |
| `POST /api/v1/run-eventql-query` | `QUERY /api/v2/data/run-eventql-query` |
| `POST /api/v1/register-event-schema` | `POST /api/v2/data/register-event-schema` |
| `POST /api/v1/read-subjects` | `QUERY /api/v2/data/read-subjects` |
| `POST /api/v1/read-event-types` | `QUERY /api/v2/data/read-event-types` |
| `POST /api/v1/read-event-type` | `QUERY /api/v2/data/read-event-type` |
| `POST /api/v1/backup` | `POST /api/v2/management/create-backup` |
| `POST /api/v1/restore` | `POST /api/v2/management/restore-backup` |
| `POST /api/v1/verify-store` | `POST /api/v2/management/verify-store` |
| `POST /api/v1/audit-history` | `POST /api/v2/management/audit-history` |
| `POST /api/v1/put-into-read-only-mode` | `POST /api/v2/management/enable-read-only-mode` |
| `GET /api/v1/ping` | `POST` or `GET /api/v2/system/ping` |
| `GET /api/v1/health` | `QUERY` or `GET /api/v2/system/health` |
| `GET /api/v1/diagnostics` | `POST /api/v2/system/diagnose` |
| `POST /api/v1/verify-api-token` | `POST /api/v2/system/verify-token` |
| `GET /.well-known/openapi` | `QUERY` or `GET /api/v2/system/openapi` |
| – | `POST /api/v2/management/import-events` |
| – | `QUERY /api/v2/management/export-events` |

**Importing** and **exporting events**, as described in **[Export and Import](/docs/eventsourcingdb/export-and-import)**, and **incremental backups** exist **only in the v2 API**; the v1 API has no counterpart for them.

## What Changes per Route

- **Writing events** answers with a **stream** of the written events, as items of the type `event`, followed by `end`, rather than with a JSON array. The preconditions on a subject take an option **`recursive`**, which applies them to everything below the subject as well, see **[Checking a Subject and Everything Below It](/docs/eventsourcingdb/preconditions#checking-a-subject-and-everything-below-it)**.
- **Listing subjects** takes the subject as **`subject`**, where v1 named it `baseSubject`, and lists it **alone** unless **`options.recursive`** is `true`. v1 always listed every subject below it as well, so set `recursive` to `true` to get what v1 returned.
- **Listing event types** and **reading an event type** name the type **`type`**, where v1 named it `eventType`, and say whether it has events as **`hasEvents`**, which is the opposite of v1's `isPhantom`. Reading an event type that the database does not know is reported as **[`event-type-unknown`](/docs/eventsourcingdb/errors/event-type-unknown)** rather than with `404`.
- **Registering an event schema** names the type **`type`**, where v1 named it `eventType`, both in the request and in the **`event-schema-registered`** event it answers with, which now comes in a stream.
- **Running an EventQL query** sends each result as an item of the type **`row`**, as v1 did, but holds the result in **`data.value`** rather than in `payload` itself. A mistake in the query names its position, where it is known, in its **`details`**, as `start` and `end`, each with a `line` and a `column`.
- **Creating a backup** takes **`lastBackedUpEvent`**: `null` for a full backup, or the `id` and `hash` of the event a previous backup ended at for an **increment** that holds only what was written since. It answers in a **format of its own**, with a `backup` item, the `eventSchema` and `event` items, the `lastBackedUpEvent` it ended at, and `end`, where v1 sent `schema` and `event` lines with nothing that marks the beginning or the end of the backup, see **[create-backup](/docs/eventsourcingdb/api/create-backup)**. **Restoring a backup** takes a full backup followed by its increments, and also a backup that v1 wrote. Like the v1 API of this version, it refuses a schema in such a backup that does not describe an object, which earlier versions accepted. See **[Backup and Restore](/docs/eventsourcingdb/backup-and-restore#compatibility)**.
- **Auditing the event store** takes **`lastAuditedEvent`**, the `id` and `hash` of the event a previous audit ended at, or `null` for a first audit, where v1 took `latestEventId` and `latestEventHash`. See **[Auditing the Event Store](/docs/eventsourcingdb/auditing-the-event-store)**.
- **Verifying the store** sends what it finds as items of the type **`finding`**.
- **Enabling read-only mode** answers with a **`read-only-mode-enabled`** event, and reports **[`read-only-mode-already-enabled`](/docs/eventsourcingdb/errors/read-only-mode-already-enabled)** if the mode is on already.
- **Ping** answers with a **`ping-received`** event, and **verifying the token** with a **`token-verified`** event, where v1 named it `api-token-verified`.
- **Health** chooses **OpenMetrics** through the **`Accept` header**, as `application/openmetrics-text`, rather than through `?format=openmetrics`, and answers a status of `fail` with `503` rather than `500`. Its metrics have **new names**, so queries and alerts that use those of v1 need rewriting. See **[Logging and Monitoring](/docs/eventsourcingdb/logging-and-monitoring)**.
- **The diagnosis** is a `POST` to `diagnose` and answers with a **`server-diagnosed`** event, which also holds the options that v1 leaves out. It gives the remaining time of the license and the heartbeat interval in **seconds** rather than nanoseconds, as **`remainingDurationInSeconds`** and **`heartbeatIntervalInSeconds`**, where v1 named them `remainingDuration` and `heartbeatInterval`, see **[Diagnostics and Troubleshooting](/docs/eventsourcingdb/diagnostics-and-troubleshooting)**.
- **The OpenAPI definition** of v2 is **JSON**, where that of v1 is YAML. The **[API catalog](/docs/eventsourcingdb/api#openapi-specification)** at `/.well-known/api-catalog` lists both.

## Errors

Where v1 answered with a status code, v2 answers with status `200` and an error item that names its code. A route that answers with a stream sends the error as an item of that stream. This section compares v2 with **the v1 API of 1.3**, which answers some cases differently than 1.2 did. The most common cases:

| v1 | v2 |
| --- | --- |
| `400` for a value that is missing or not valid, such as a malformed subject | the code of that value, such as **[`subject-malformed`](/docs/eventsourcingdb/errors/subject-malformed)** |
| `409` for a precondition that does not hold | **[`precondition-violated`](/docs/eventsourcingdb/errors/precondition-violated)** |
| `400` for an EventQL precondition that cannot be evaluated, a `500` in 1.2 | **[`query-malformed`](/docs/eventsourcingdb/errors/query-malformed)**, **[`query-unevaluable`](/docs/eventsourcingdb/errors/query-unevaluable)**, or **[`query-not-boolean`](/docs/eventsourcingdb/errors/query-not-boolean)** |
| `400` for an EventQL query, run or in a precondition, that nests more than 256 levels or holds a chain of more than 10,000 operands | **[`query-too-deep`](/docs/eventsourcingdb/errors/query-too-deep)** |
| `400` for an EventQL precondition whose query is longer than 100 KiB | **[`query-too-long`](/docs/eventsourcingdb/errors/query-too-long)** |
| `409` for an event that does not match the schema of its type | **[`event-schema-violated`](/docs/eventsourcingdb/errors/event-schema-violated)** |
| `409` for an event whose type has no schema while schemas are required | **[`event-schema-unregistered`](/docs/eventsourcingdb/errors/event-schema-unregistered)** |
| `400` for a schema that refers back to a part of itself or reaches more than 10,000 subschemas, and `409` for a write of an event type with such a stored schema | **[`event-schema-too-complex`](/docs/eventsourcingdb/errors/event-schema-too-complex)** |
| `409` for a schema that is registered already | **[`event-schema-already-registered`](/docs/eventsourcingdb/errors/event-schema-already-registered)** |
| `409` for events beyond the capacity of the license | **[`capacity-exceeded`](/docs/eventsourcingdb/errors/capacity-exceeded)** |
| `507` for a license that has expired | **[`license-invalid`](/docs/eventsourcingdb/errors/license-invalid)** |
| `404` for an event type the database does not know | **[`event-type-unknown`](/docs/eventsourcingdb/errors/event-type-unknown)** |
| `409` for a restore into a database that is not empty | **[`store-not-empty`](/docs/eventsourcingdb/errors/store-not-empty)** |
| `400` for a restore with an event that lies more than an hour beyond the clock of the server | **[`backup-ahead-of-clock`](/docs/eventsourcingdb/errors/backup-ahead-of-clock)** |
| `503` for read-only mode, a full disk, a running restore, a failed store, or indexes that are rebuilt | **[`store-read-only`](/docs/eventsourcingdb/errors/store-read-only)**, **[`disk-full`](/docs/eventsourcingdb/errors/disk-full)**, **[`restore-in-progress`](/docs/eventsourcingdb/errors/restore-in-progress)**, **[`store-failed`](/docs/eventsourcingdb/errors/store-failed)**, or **[`indexes-rebuilding`](/docs/eventsourcingdb/errors/indexes-rebuilding)** |
| an `error` line in a stream, with a message | an `error` item with a code, such as **[`event-chain-broken`](/docs/eventsourcingdb/errors/event-chain-broken)** or **[`file-unreadable`](/docs/eventsourcingdb/errors/file-unreadable)** |

Some status codes stay, since they report a problem of the request or the server rather than of what it asks for: `400` for a body of the wrong shape, `401`, `404` for a route that does not exist, `405`, `408`, `413`, `429`, `500`, and `503` for a server that shuts down before it has answered. The **[overview of the errors](/docs/eventsourcingdb/errors)** lists every code.
