API
This reference describes the HTTP API of SynthEvents: its routes, what they expect and answer, how errors look, and which limits apply.
Base URL#
SynthEvents is public at https://api.synthevents.io. Every route lives under the base path:
/api/v1
The version is part of the path, so that a breaking change can come as a new version while existing clients keep working.
Authentication#
Reading the ready-made data sets and the domains needs no token. Generating a data set with parameters of your own takes a token, which you send in the header Authorization, after the scheme Bearer:
Authorization: Bearer <TOKEN>
A token is issued for an occasion, such as a workshop or a talk, and names the occasion, not a person. It is valid until midnight in Berlin after the last day of the occasion. To get one, ask for a tailor-made data set: we send you a token and the command that generates it.
SynthEvents checks the token before the body, and refuses a request without a valid one with 401 Unauthorized and the header WWW-Authenticate: Bearer. A token that has expired is refused with token-expired, any other problem with it with token-missing or token-invalid.
Routes#
The API has four routes:
| Method | Route | Purpose |
|---|---|---|
QUERY |
/api/v1/read-data-set |
Hands out a ready-made data set. |
POST |
/api/v1/generate-events |
Generates a data set, with a token. |
QUERY |
/api/v1/read-domains |
Lists the domains with their scenarios and their ready-made data sets. |
GET, POST |
/api/v1/ping |
Tells that the service is alive. |
Request bodies are JSON. A request for data that SynthEvents keeps uses the HTTP method QUERY, a request to produce something uses POST.
Reading Ready-Made Data Sets#
SynthEvents keeps ready-made data sets: every scenario of every domain in three sizes, of about 500, 5,000, and 20,000 events, over the year 2025, with the seed standard. They are the same for everyone, as long as SynthEvents keeps its version, and read-domains lists them by name. To fetch one, send a QUERY request to /api/v1/read-data-set:
curl \
-X QUERY \
-H "content-type: application/json" \
-d '{
"name": "library-everyday-medium",
"format": "eventsourcingdb"
}' \
-OJ \
https://api.synthevents.io/api/v1/read-data-set
The body is a JSON object with exactly these fields, both of them mandatory:
| Field | Type | Description |
|---|---|---|
name |
string | The name of a ready-made data set, made of its domain, scenario, and size, such as library-everyday-medium. |
format |
string | cloudevents for plain CloudEvents, or eventsourcingdb for a backup for EventSourcingDB. |
A ready-made data set is the data set that generating its parameters gives, so SynthEvents answers with the same body and the same headers: the same bytes, the same ETag, and the same file name. It keeps the ready-made data sets from the moment it starts, so it answers at once, and never refuses such a request as busy.
Generating Events#
To generate a data set, send a POST request to /api/v1/generate-events, with your token in place of <TOKEN>:
curl \
-X POST \
-H "authorization: Bearer <TOKEN>" \
-H "content-type: application/json" \
-d '{
"domain": "library",
"scenario": "everyday",
"range": {
"from": "2025-01-01T00:00:00Z",
"to": "2026-01-01T00:00:00Z"
},
"eventCount": 20000,
"seed": "ddd-workshop-2026",
"format": "eventsourcingdb"
}' \
-OJ \
https://api.synthevents.io/api/v1/generate-events
The body is a JSON object with exactly these fields, all of them mandatory:
| Field | Type | Description |
|---|---|---|
domain |
string | The domain, courses, library, or todo. |
scenario |
string | A scenario of the domain, such as everyday. |
range.from |
string | The start of the range, in RFC 3339 with an offset, on or after 1970-01-01T00:00:00Z. |
range.to |
string | The end of the range, in RFC 3339 with an offset, after range.from, and on or before the moment of the request. |
eventCount |
integer | The approximate number of events, from 1 to 20000. |
seed |
string | The seed, 1 to 64 characters of a to z, 0 to 9, and -. |
format |
string | cloudevents for plain CloudEvents, or eventsourcingdb for a backup for EventSourcingDB. |
The range is half-open: it includes from, but not to. Field names are case-sensitive, so a field such as eventcount is refused as unknown rather than read as eventCount. A request names no event type prefix: every event type is io.synthevents.<domain>.<event>, and a field such as eventTypePrefix is refused like any other unknown field. For what the parameters do, see Generating Data Sets.
If the request is valid, SynthEvents answers with 200 OK, and the body is the data set itself, as NDJSON, without an envelope (see Formats). SynthEvents generates the data set in full before it sends the first byte, so a failure always comes as a status code, never as a truncated file. The response has these headers:
Content-Typeisapplication/x-ndjson.Content-Lengthis the size of the data set in bytes.ETagis the SHA-256 hash of the body, in hexadecimal. It is a strong tag, since the same request always gives the same bytes.Content-Dispositionnames the file after its domain, scenario, the days of its range in UTC, the number of events asked for, the seed, and the format, such as:
Content-Disposition: attachment; filename="synthevents-library-everyday-2025-01-01-2026-01-01-20000-ddd-workshop-2026.eventsourcingdb.ndjson"
The option -OJ of curl saves the data set under that name. The name leaves out the version of SynthEvents and the times of the range, so two data sets can share a name, such as two that were generated by different versions. Their source tells them apart.
Reading Domains#
To list the domains and their scenarios, send a QUERY request to /api/v1/read-domains, with an empty JSON object as its body:
curl \
-X QUERY \
-H "content-type: application/json" \
-d '{}' \
https://api.synthevents.io/api/v1/read-domains
The response is a stream of NDJSON with the content type application/x-ndjson. It has one line of the type domain for every domain, in the order of their names, with its scenarios, also in the order of their names. A line of the type end follows the last domain, so that you can tell that the stream is complete:
{"type":"domain","data":{"name":"courses","scenarios":[{"name":"everyday","dataSets":[{"name":"courses-everyday-small","range":{"from":"2025-01-01T00:00:00Z","to":"2026-01-01T00:00:00Z"},"eventCount":500,"seed":"standard"},{"name":"courses-everyday-medium","range":{"from":"2025-01-01T00:00:00Z","to":"2026-01-01T00:00:00Z"},"eventCount":5000,"seed":"standard"},{"name":"courses-everyday-large","range":{"from":"2025-01-01T00:00:00Z","to":"2026-01-01T00:00:00Z"},"eventCount":20000,"seed":"standard"}]}]}}
{"type":"domain","data":{"name":"library","scenarios":[{"name":"everyday","dataSets":[{"name":"library-everyday-small","range":{"from":"2025-01-01T00:00:00Z","to":"2026-01-01T00:00:00Z"},"eventCount":500,"seed":"standard"},{"name":"library-everyday-medium","range":{"from":"2025-01-01T00:00:00Z","to":"2026-01-01T00:00:00Z"},"eventCount":5000,"seed":"standard"},{"name":"library-everyday-large","range":{"from":"2025-01-01T00:00:00Z","to":"2026-01-01T00:00:00Z"},"eventCount":20000,"seed":"standard"}]}]}}
{"type":"domain","data":{"name":"todo","scenarios":[{"name":"everyday","dataSets":[{"name":"todo-everyday-small","range":{"from":"2025-01-01T00:00:00Z","to":"2026-01-01T00:00:00Z"},"eventCount":500,"seed":"standard"},{"name":"todo-everyday-medium","range":{"from":"2025-01-01T00:00:00Z","to":"2026-01-01T00:00:00Z"},"eventCount":5000,"seed":"standard"},{"name":"todo-everyday-large","range":{"from":"2025-01-01T00:00:00Z","to":"2026-01-01T00:00:00Z"},"eventCount":20000,"seed":"standard"}]}]}}
{"type":"end","data":{}}
Every scenario is an object, so that it can carry more information later without a change to the shape of the response. Under dataSets, it lists its ready-made data sets in the order of their size, each with its name and the parameters it is made of, named as in a request to generate it: its range in UTC, its number of events, and its seed. The domains and their scenarios are described in Library and To-Dos.
The body must be exactly {}. A body with any field, such as {"domain": "library"}, is refused, rather than taken for a filter.
Pinging#
To check whether SynthEvents is alive, send a GET or POST request to /api/v1/ping:
curl https://api.synthevents.io/api/v1/ping
SynthEvents answers with an event of its own, wrapped in an envelope of the type event, with the content type application/json:
{"type":"event","data":{"specversion":"1.0","id":"01a1417f-457a-7af1-92eb-0d85cee3ca12","source":"tag:synthevents.io,2026:synthevents","type":"io.synthevents.api.ping-received","subject":"/api/v1/ping","time":"2026-10-15T21:37:02.330692Z","datacontenttype":"application/json","data":{"message":"My God, it's full of stars."}}}
Its id is a UUIDv7, and its time is the moment of the request.
Errors#
If SynthEvents refuses a request, it answers with a real HTTP status code and a JSON body, with the content type application/json:
{
"type": "error",
"data": {
"code": "https://docs.synthevents.io/errors/seed-malformed",
"description": "seed is malformed: expected 1 to 64 of a-z, 0-9, and -",
"details": {
"value": "DDD Workshop 2026"
}
}
}
codeis a URL that ends in the slug of the error, such asseed-malformed. It leads to the page of the error in this documentation.descriptionsays what is wrong and what is expected.detailslocates the problem, such as by the value or the field to blame. It is an empty object if there is nothing to locate.
The status codes are these:
| Status | Meaning |
|---|---|
400 Bad Request |
The body is not JSON, or does not have the expected shape, such as because a field is missing or unknown. |
401 Unauthorized |
The request to generate events has no valid token. The response has a WWW-Authenticate header. |
413 Request Entity Too Large |
The body is larger than any request needs. |
422 Unprocessable Entity |
The body has the expected shape, but a value is not valid, or the range and the number of events do not fit together. |
500 Internal Server Error |
SynthEvents failed itself. |
503 Service Unavailable |
SynthEvents is busy generating. The response has a Retry-After header. |
SynthEvents checks the token before the body, and the shape of a body before its values, and reports one error at a time. For every error, its cause, and how to fix it, see Errors.
A request to a route that does not exist is answered with 404 Not Found, and one with a method that a route does not take with 405 Method Not Allowed. Both come with a body in plain text rather than in the shape of an error, since the status code says everything there is to say.
Limits#
- A body may have at most 65,536 bytes (64 KiB), far more than any valid request needs. A larger one is refused with
body-too-large. - SynthEvents generates one data set per processor core at a time, and at least two. A request beyond that is refused with
service-busyandRetry-After: 3. A request that is refused for another reason does not count, and neither does one that is answered from the cache, nor one for a ready-made data set. - SynthEvents generates its ready-made data sets when it starts, before it answers anything, and keeps them for as long as it runs.
- SynthEvents keeps the data sets it has generated in a cache. A request for a data set that was generated recently is answered from it, and identical requests that arrive at the same time are generated only once.
- SynthEvents does not limit the rate of requests. Generating takes a token instead.
- SynthEvents sends no CORS headers, so a script on a website cannot read its responses in a browser. Call it from a server or from the command line instead.