{"openapi":"3.1.0","info":{"title":"Tolven Energy Data API","description":"Public, no-auth JSON API for gap-aware, revision-tracked NL energy data. Gaps are preserved, never silently interpolated; every returned observation is redistributable under CC-BY 4.0, and the response carries the required attribution. Bulk historical data is available under `/v0/bulk` — the whole history as a single Parquet, or per-month partitions for staying current — both point-in-time-capable via the `publication_ts`/`revision` columns.\n\n## Freshness\n\nImbalance settlement arrives as **one batch per Dutch local day**, not continuously. The newest observation is therefore between roughly **1 and 25 hours old**, sawtoothing: youngest just after a batch lands, oldest just before the next one does. Plan around that shape rather than around a fixed interval — this service polls far more often than upstream publishes, so our own delay is not the constraint.\n\nMeasured 2026-08-16: upstream published at ~23:00 UTC on each of the preceding 44 days, and several hours later than that through late June. That clock time is an observation of upstream's recent behaviour, not a schedule it is held to and not a commitment made here. It also follows the Dutch local day rather than UTC, so expect it an hour later — around 00:00 UTC — while the Netherlands is on winter time. The shape above is the part to build on.\n\n`GET /v0/status` says which side of that shape each dataset is on right now: a `verdict` of `current`, `late` or `upstream_unreachable`, beside the timestamps it is derived from. `late` means upstream was reached within the last 3 hours but the newest observation is more than 27 hours old, or there is none; `upstream_unreachable` means no fetch has succeeded in the last 3 hours. `latest` and the bulk manifest carry `upstream_checked_at` for the same reason: old data with a recent check is a batch that has not landed, old data with an old check is a source that cannot be reached.\n\n## Revision coverage\n\nThe series and bulk responses carry `revisions_observed_from` — the instant from which observed revision history exists. (`/v0/gaps` is live-only in v0 and omits it.) Points at or after it were published while this service was already watching, so `?as_of=` over them is a leakage-free point-in-time read. Points before it were seen only retrospectively: their `revision: 1` is the value as it already stood at first fetch, not first publication, so what you get is the **final settled value**. Upstream serves no revision archive, so that earlier history cannot be reconstructed. An `as_of` before the boundary is rejected with `400` rather than returning an empty result that would read as \"nothing was known\".\n\n## Query windows\n\n`/v0/series/{dataset}` and `/v0/gaps/{dataset}` span at most 31 days per request — a whole calendar month, matching the monthly `/v0/bulk` partitions. A wider window is rejected with `400` rather than silently truncated, so a partial answer is never mistaken for a complete one. For longer spans, including the full history, use `/v0/bulk`.\n\n## Rate limits\n\nPer client IP: 120 requests/minute sustained on the JSON endpoints with bursts up to 60, and 60 requests/minute on `/v0/bulk` with bursts up to 20. Requests over the limit receive `429 Too Many Requests` carrying a `Retry-After` header and the same JSON error envelope as every other failure; back off before retrying.\n\nThe limit counts requests that reach this service. A response served from the cache in front of it never arrives here and is not counted.\n\nNo workflow here needs many requests. A first-time load is a single object — `/v0/bulk/{dataset}/full.parquet` — rather than one request per month, a count that would grow for as long as the history does. To stay current afterwards, read the manifest and re-download only the months whose `ETag` moved, normally just the settlement tail.\n\n## Zones\n\nServed free of charge: NL. Other ENTSO-E bidding zones are available on request: hello@tolven.eu.","contact":{"email":"hello@tolven.eu"},"license":{"name":"MIT"},"version":"0"},"servers":[{"url":"/"}],"paths":{"/health":{"get":{"tags":[],"operationId":"health","responses":{"200":{"description":"Service is up","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/v0/bulk/{dataset}":{"get":{"tags":["bulk"],"operationId":"bulk_manifest","parameters":[{"name":"dataset","in":"path","description":"Public series id","required":true,"schema":{"type":"string"},"example":"imbalance-prices"}],"responses":{"200":{"description":"Available monthly partitions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkManifestResponse"}}}},"404":{"description":"Unknown series, or nothing published yet","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorBody"}}}},"429":{"description":"Rate limit exceeded. Retry after the interval in the `Retry-After` header.","headers":{"retry-after":{"schema":{"type":"string"},"description":"Whole seconds to wait before retrying"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorBody"}}}}},"x-badges":[{"name":"60 req/min"}]}},"/v0/bulk/{dataset}/full.parquet":{"get":{"tags":["bulk"],"summary":"The whole redistributable history in one object.","description":"This is the door that makes a first-time load cost one request instead of\none per month — a count that grows every month and that no rate limit could\nhave accommodated. The monthly partitions remain the incremental door. See\nADR 0020.","operationId":"full_download","parameters":[{"name":"dataset","in":"path","description":"Public series id","required":true,"schema":{"type":"string"},"example":"imbalance-prices"}],"responses":{"200":{"description":"The whole redistributable history as one Parquet","content":{"application/vnd.apache.parquet":{}}},"404":{"description":"Unknown series, or nothing published yet","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorBody"}}}},"429":{"description":"Rate limit exceeded. Retry after the interval in the `Retry-After` header.","headers":{"retry-after":{"schema":{"type":"string"},"description":"Whole seconds to wait before retrying"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorBody"}}}}},"x-badges":[{"name":"60 req/min"}]}},"/v0/bulk/{dataset}/{year}/{month}.parquet":{"get":{"tags":["bulk"],"operationId":"bulk_download","parameters":[{"name":"dataset","in":"path","description":"Public series id","required":true,"schema":{"type":"string"},"example":"imbalance-prices"},{"name":"year","in":"path","description":"Partition year","required":true,"schema":{"type":"integer","format":"int32"},"example":2025},{"name":"month","in":"path","description":"Zero-padded month; the `.parquet` suffix is part of the path","required":true,"schema":{"type":"string"},"example":"10"}],"responses":{"200":{"description":"Parquet for one month","content":{"application/vnd.apache.parquet":{}}},"404":{"description":"Unknown series, or no such month","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorBody"}}}},"429":{"description":"Rate limit exceeded. Retry after the interval in the `Retry-After` header.","headers":{"retry-after":{"schema":{"type":"string"},"description":"Whole seconds to wait before retrying"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorBody"}}}}},"x-badges":[{"name":"60 req/min"}]}},"/v0/gaps/{dataset}":{"get":{"tags":["series"],"operationId":"gaps_handler","parameters":[{"name":"dataset","in":"path","description":"Public series id","required":true,"schema":{"type":"string"},"example":"imbalance-prices"},{"name":"zone","in":"query","description":"Bidding-zone code, matched case-insensitively and echoed back canonical.\n`NL` is currently the only zone served, and the default.","required":false,"schema":{"type":"string"},"example":"NL"},{"name":"from","in":"query","description":"Window start, inclusive. RFC3339, a bare date, or a naive datetime (all\nUTC). Defaults to `to` minus 7 days. The window may span at most 31 days\n— a whole calendar month; for longer spans use `/v0/bulk`.","required":false,"schema":{"type":"string"},"example":"2025-10-01"},{"name":"to","in":"query","description":"Window end, exclusive. Same formats as `from`. Defaults to now.","required":false,"schema":{"type":"string"},"example":"2025-10-02"}],"responses":{"200":{"description":"Interior gaps + coverage envelope per field","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GapsResponse"}}}},"400":{"description":"Malformed parameter, or a window longer than 31 days","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorBody"}}}},"404":{"description":"Unknown series, or not normalized yet","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorBody"}}}},"429":{"description":"Rate limit exceeded. Retry after the interval in the `Retry-After` header.","headers":{"retry-after":{"schema":{"type":"string"},"description":"Whole seconds to wait before retrying"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorBody"}}}}},"x-badges":[{"name":"120 req/min"},{"name":"max window 31 days"}]}},"/v0/series/{dataset}":{"get":{"tags":["series"],"operationId":"window","parameters":[{"name":"dataset","in":"path","description":"Public series id","required":true,"schema":{"type":"string"},"example":"imbalance-prices"},{"name":"zone","in":"query","description":"Bidding-zone code, matched case-insensitively and echoed back canonical.\n`NL` is currently the only zone served, and the default.","required":false,"schema":{"type":"string"},"example":"NL"},{"name":"from","in":"query","description":"Window start, inclusive. RFC3339, a bare date, or a naive datetime (all\nUTC). Defaults to `to` minus 7 days. The window may span at most 31 days\n— a whole calendar month; for longer spans use `/v0/bulk`.","required":false,"schema":{"type":"string"},"example":"2025-10-01"},{"name":"to","in":"query","description":"Window end, exclusive. Same formats as `from`. Defaults to now.","required":false,"schema":{"type":"string"},"example":"2025-10-02"},{"name":"as_of","in":"query","description":"Point-in-time cutoff: return each point as it was known at this instant,\nhiding revisions published later. Same formats as `from`.\n\nMust be at or after the `revisions_observed_from` this response\ncarries. Earlier instants are rejected with `400`: no revision history\nwas observed before then, so the honest answer is a refusal rather than\nan empty result that would read as \"nothing was known\".","required":false,"schema":{"type":"string"},"example":"2026-07-01T12:30:00Z"}],"responses":{"200":{"description":"Observations in the window","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeriesResponse"}}}},"400":{"description":"Malformed parameter, a window longer than 31 days, or an `as_of` before the revision-coverage boundary","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorBody"}}}},"404":{"description":"Unknown series, or not normalized yet","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorBody"}}}},"429":{"description":"Rate limit exceeded. Retry after the interval in the `Retry-After` header.","headers":{"retry-after":{"schema":{"type":"string"},"description":"Whole seconds to wait before retrying"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorBody"}}}}},"x-badges":[{"name":"120 req/min"},{"name":"max window 31 days"}]}},"/v0/series/{dataset}/latest":{"get":{"tags":["series"],"operationId":"latest","parameters":[{"name":"dataset","in":"path","description":"Public series id","required":true,"schema":{"type":"string"},"example":"imbalance-prices"},{"name":"zone","in":"query","description":"Bidding-zone code, matched case-insensitively and echoed back canonical.\n`NL` is currently the only zone served, and the default.","required":false,"schema":{"type":"string"},"example":"NL"},{"name":"as_of","in":"query","description":"Point-in-time cutoff: the latest revision known at this instant.\n\nMust be at or after the `revisions_observed_from` this response\ncarries; earlier instants are rejected with `400`.","required":false,"schema":{"type":"string"},"example":"2026-07-01T12:30:00Z"}],"responses":{"200":{"description":"The most recent observation per field","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LatestResponse"}}}},"400":{"description":"Malformed parameter, or an `as_of` before the revision-coverage boundary","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorBody"}}}},"404":{"description":"Unknown series, or not normalized yet","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorBody"}}}},"429":{"description":"Rate limit exceeded. Retry after the interval in the `Retry-After` header.","headers":{"retry-after":{"schema":{"type":"string"},"description":"Whole seconds to wait before retrying"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorBody"}}}}},"x-badges":[{"name":"120 req/min"}]}},"/v0/status":{"get":{"tags":["status"],"operationId":"status_handler","responses":{"200":{"description":"Freshness of every public dataset","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"429":{"description":"Rate limit exceeded. Retry after the interval in the `Retry-After` header.","headers":{"retry-after":{"schema":{"type":"string"},"description":"Whole seconds to wait before retrying"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorBody"}}}}},"x-badges":[{"name":"120 req/min"}]}}},"components":{"schemas":{"ApiErrorBody":{"type":"object","description":"The JSON error envelope returned for every 4xx. Documented as a schema and\nused as the actual response body, so the two cannot diverge.","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable description of what was wrong with the request.","example":"unknown zone: ZZ"}}},"BulkManifestResponse":{"type":"object","required":["dataset","licence","attribution","from","to","months","zones","contact"],"properties":{"attribution":{"type":"string","description":"Attribution the source legally requires.","example":"ENTSO-E Transparency Platform, CC-BY 4.0"},"contact":{"$ref":"#/components/schemas/ManifestContact","description":"Where to ask for zones not listed in `zones`."},"dataset":{"type":"string","description":"Series id."},"from":{"type":"string","format":"date-time","description":"Earliest observation timestamp available (UTC)."},"full":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/FullFile","description":"The whole history as one object — the door for a first-time load, where\nfetching the months would cost one request each. `null` on a manifest\npublished before this existed; the next publish fills it in."}]},"licence":{"type":"string","description":"Licence door of every partition (CC-BY by construction).","example":"cc-by-4.0"},"months":{"type":"array","items":{"$ref":"#/components/schemas/BulkMonth"}},"revisions_observed_from":{"type":["string","null"],"format":"date-time","description":"Instant from which observed revision history exists: the earliest\n`publication_ts` in these partitions, over every zone they contain —\ndataset-wide like `from`/`to`, since the partitions are not\nzone-scoped. The windowed endpoints report a per-zone boundary.\n\nApplying `as_of` to a point older than this does not give a\npoint-in-time read. Those rows were seen only retrospectively, so their\n`revision: 1` is the value as it already stood at first fetch — the\nfinal settled number. `null` on a manifest published before this field\nexisted; the next publish fills it in.","example":"2026-06-15T09:00:00Z"},"to":{"type":"string","format":"date-time","description":"Latest observation timestamp available (UTC)."},"upstream_checked_at":{"type":["string","null"],"format":"date-time","description":"Last successful fetch from the source (UTC), whether or not it changed\nanything. `null` until a refresh has recorded one. Distinct from `to`:\n`to` is how far the published partitions reach, this is when upstream\nwas last reached. The partitions publish on their own cadence, so `to`\ncan lag the newest observation `/v0/status` reports.","example":"2026-09-08T10:30:02Z"},"zones":{"type":"array","items":{"type":"string"},"description":"Bidding zones these partitions contain, and the only ones the JSON\nendpoints serve. Other zones are available on request: see `contact`.","example":["NL"]}}},"BulkMonth":{"type":"object","required":["year","month","rows","bytes","url","gaps"],"properties":{"bytes":{"type":"integer","description":"Partition size in bytes.","minimum":0},"etag":{"type":["string","null"],"description":"Object ETag — the change token to decide whether to re-download."},"gaps":{"type":"array","items":{"$ref":"#/components/schemas/FieldGapsDto"},"description":"Per-field gap summary for this month: each observed field's coverage\nenvelope plus any interior holes. A field present this month always has\nan entry; its inner `gaps` is empty when the field is complete."},"month":{"type":"integer","format":"int32","minimum":0},"rows":{"type":"integer","description":"Rows (revisions) in the partition.","minimum":0},"url":{"type":"string","description":"Absolute download URL for this month's Parquet."},"year":{"type":"integer","format":"int32"}}},"DatasetStatusDto":{"type":"object","required":["dataset","zones","verdict","missing_intervals_30d"],"properties":{"dataset":{"type":"string","description":"The public series id.","example":"imbalance-prices"},"missing_intervals_30d":{"type":"integer","format":"int64","description":"Absent intervals inside interior holes over the last 30 days, summed\nover `zones` and every field. Moves down as well as up: a hole between\ntwo batches counts until a refresh fills it. Absence after\n`observed_through` is never counted.","example":0},"observed_through":{"type":["string","null"],"format":"date-time","description":"Newest observation timestamp over `zones` (UTC). `null` when nothing is\ncached.","example":"2026-09-07T21:45:00Z"},"upstream_checked_at":{"type":["string","null"],"format":"date-time","description":"Last successful fetch from the source, whether or not it changed\nanything (UTC). `null` when no refresh has recorded one.","example":"2026-09-08T10:30:02Z"},"upstream_last_error":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/UpstreamErrorDto","description":"Last failed fetch, if any has been recorded. Not cleared by a later\nsuccess: read it against `upstream_checked_at`."}]},"verdict":{"$ref":"#/components/schemas/VerdictDto","description":"Whether the data is current — see [`VerdictDto`] for the ceilings."},"zones":{"type":"array","items":{"type":"string"},"description":"The zones this status aggregates over — the public ones.","example":["NL"]}}},"FieldGapsDto":{"type":"object","required":["field","gaps"],"properties":{"field":{"type":"string","description":"Source field code, e.g. `A04` / `A05`.","example":"A04"},"first_observed":{"type":["string","null"],"format":"date-time","description":"First observed timestamp in the window (coverage envelope; not a gap).","example":"2025-10-01T00:00:00Z"},"gaps":{"type":"array","items":{"$ref":"#/components/schemas/GapDto"}},"last_observed":{"type":["string","null"],"format":"date-time","description":"Last observed timestamp in the window.","example":"2025-10-01T23:45:00Z"}}},"FullFile":{"type":"object","description":"The whole redistributable history, published as a single Parquet.","required":["rows","bytes","url"],"properties":{"bytes":{"type":"integer","description":"Size in bytes.","minimum":0},"etag":{"type":["string","null"],"description":"Object ETag — the change token to decide whether to re-download. It\nmoves whenever any month does, so it churns while the settlement tail\ndoes; a client tracking only what changed should use the months."},"rows":{"type":"integer","description":"Rows (revisions) — the sum over every published month.","minimum":0},"url":{"type":"string","description":"Absolute download URL for the whole history."}}},"GapDto":{"type":"object","required":["from","until","resolution_min","missing"],"properties":{"from":{"type":"string","format":"date-time","description":"Start of the missing span (UTC) — the first absent slot.","example":"2025-10-01T01:00:00Z"},"missing":{"type":"integer","format":"int64","description":"Count of absent intervals in `[from, until)`.","example":4},"resolution_min":{"type":"integer","format":"int32","description":"Interval length (minutes) of the run the hole sits in.","example":15},"until":{"type":"string","format":"date-time","description":"Next present timestamp (UTC, exclusive).","example":"2025-10-01T03:00:00Z"}}},"GapsResponse":{"type":"object","required":["dataset","zone","licence","attribution","from","to","fields"],"properties":{"attribution":{"type":"string","description":"Attribution the source legally requires.","example":"ENTSO-E Transparency Platform, CC-BY 4.0"},"dataset":{"type":"string","description":"Series id.","example":"imbalance-prices"},"fields":{"type":"array","items":{"$ref":"#/components/schemas/FieldGapsDto"}},"from":{"type":"string","format":"date-time","description":"Window start, inclusive (UTC)."},"licence":{"type":"string","description":"Licence door of the series (CC-BY by construction).","example":"cc-by-4.0"},"to":{"type":"string","format":"date-time","description":"Window end, exclusive (UTC)."},"zone":{"type":"string","description":"Bidding-zone code, canonical whatever spelling was requested.","example":"NL"}}},"LatestResponse":{"type":"object","description":"The most recent observation per field.\n\nIts own type rather than a [`SeriesResponse`] with two null fields: `latest`\nanswers about a point, not a range, and a schema that says so is a better\ncontract than one documenting when its own fields are absent.","required":["dataset","zone","licence","attribution","observations"],"properties":{"attribution":{"type":"string","description":"Attribution string the source legally requires.","example":"ENTSO-E Transparency Platform, CC-BY 4.0"},"dataset":{"type":"string","description":"Series id these observations belong to.","example":"imbalance-prices"},"licence":{"type":"string","description":"Licence door of every returned observation (always CC-BY by construction).","example":"cc-by-4.0"},"observations":{"type":"array","items":{"$ref":"#/components/schemas/ObservationDto"}},"observed_through":{"type":["string","null"],"format":"date-time","description":"Newest observation timestamp among the observations returned, for this\nzone (UTC). `null` when there are none. `/v0/status` reports the same\nquantity dataset-wide, with a verdict.","example":"2026-09-07T21:45:00Z"},"revisions_observed_from":{"type":["string","null"],"format":"date-time","description":"Instant from which observed revision history exists for this zone. See\nthe \"Revision coverage\" section of the API description, and\n[`SeriesResponse::revisions_observed_from`], which carries the same\nvalue and the same meaning.\n\n`null` only when the series holds no rows.","example":"2026-06-15T09:00:00Z"},"upstream_checked_at":{"type":["string","null"],"format":"date-time","description":"Last successful fetch from the source (UTC), whether or not it changed\nanything. `null` until a refresh has recorded one. Old data with a\nrecent check means the batch has not landed; old data with an old\ncheck means the source has not been reached.","example":"2026-09-08T10:30:02Z"},"zone":{"type":"string","description":"Bidding-zone code, canonical whatever spelling was requested.","example":"NL"}}},"ManifestContact":{"type":"object","description":"Where a bulk consumer asks for more than the free tier serves. The manifest\nis the one object every bulk client polls, so this is where the offer has\nto be stated for them to see it.","required":["email","note"],"properties":{"email":{"type":"string","example":"hello@tolven.eu"},"note":{"type":"string","description":"What is served free and what is available on request."}}},"ObservationDto":{"type":"object","required":["field","ts","resolution_min","value","unit","publication_ts","revision"],"properties":{"field":{"type":"string","description":"Source field code, e.g. `A04` (surplus) / `A05` (shortage).","example":"A04"},"publication_ts":{"type":"string","format":"date-time","description":"When this value was published — `?as_of=` reads against this.","example":"2025-10-01T23:00:28Z"},"resolution_min":{"type":"integer","format":"int32","description":"Interval length in minutes, a per-observation property so a resolution\nchange mid-series stays representable rather than being resampled. NL\nimbalance prices are 15-minute for their whole history, back to\n2014-12-05 — the 2025-10-01 hourly→15-min MTU change affects other\nseries, not this one.","example":15},"revision":{"type":"integer","format":"int32","description":"Revision number; bumps each time the upstream value changed.","example":1},"ts":{"type":"string","format":"date-time","description":"Interval start (UTC).","example":"2025-10-01T12:00:00Z"},"unit":{"type":"string","example":"EUR/MWH"},"value":{"type":"number","format":"double","example":113.49}}},"SeriesResponse":{"type":"object","description":"A windowed read: every observation in the resolved `[from, to)`.\n\nCarries the request it answers — `dataset`, `zone` and the resolved window —\nso a stored response can be interpreted without the URL that produced it.","required":["dataset","zone","from","to","licence","attribution","observations"],"properties":{"attribution":{"type":"string","description":"Attribution string the source legally requires.","example":"ENTSO-E Transparency Platform, CC-BY 4.0"},"dataset":{"type":"string","description":"Series id these observations belong to.","example":"imbalance-prices"},"from":{"type":"string","format":"date-time","description":"Window start, inclusive — the value actually used, after defaulting.","example":"2025-10-01T00:00:00Z"},"licence":{"type":"string","description":"Licence door of every returned observation (always CC-BY by construction).","example":"cc-by-4.0"},"observations":{"type":"array","items":{"$ref":"#/components/schemas/ObservationDto"}},"revisions_observed_from":{"type":["string","null"],"format":"date-time","description":"Instant from which observed revision history exists **for this zone**:\nthe earliest `publication_ts` among the rows this door can serve.\n\nRead it on the observation axis. A point whose `ts` is at or after this\nwas published while the refresh was already running, so its revision\nhistory is genuinely ours and `?as_of=` over it is leakage-free. A point\nbefore it was only ever seen retrospectively — its `revision: 1` is the\nvalue as it already stood at first fetch, not first publication — so\nwhat it carries is the final settled value, not a point-in-time read.\n\n`null` only when the series holds no rows.","example":"2026-06-15T09:00:00Z"},"to":{"type":"string","format":"date-time","description":"Window end, exclusive — the value actually used, after defaulting.","example":"2025-10-02T00:00:00Z"},"zone":{"type":"string","description":"Bidding-zone code, canonical whatever spelling was requested.","example":"NL"}}},"StatusResponse":{"type":"object","description":"Per-dataset freshness. The one place that turns \"the API is up\" into\n\"the data is current\".","required":["generated_at","datasets"],"properties":{"datasets":{"type":"array","items":{"$ref":"#/components/schemas/DatasetStatusDto"},"description":"One entry per public series, always, in allow-list order."},"generated_at":{"type":"string","format":"date-time","description":"When this status was evaluated (UTC)."}}},"UpstreamErrorDto":{"type":"object","description":"The last failed fetch from the source. A class, never a message.","required":["at","class"],"properties":{"at":{"type":"string","format":"date-time","description":"When the fetch failed (UTC).","example":"2026-09-02T14:30:00Z"},"class":{"type":"string","description":"`upstream_unavailable` (5xx, maintenance page, platform switched off),\n`upstream_rejected` (4xx: credentials, request, rate limit) or\n`transport` (no answer at all).","example":"upstream_unavailable"}}},"VerdictDto":{"type":"string","description":"Whether a dataset's data is current. A closed set a script can switch on;\nthe timestamps beside it let a consumer apply its own thresholds instead.\n`current`: upstream was reached within the last 3 hours and the newest\nobservation is at most 27 hours old. `late`: upstream was reached within\nthe last 3 hours, but the newest observation is more than 27 hours old —\nthe daily batch is late, or this service's own pipeline is, or there is\nno observation at all.\n`upstream_unreachable`: no successful fetch within the last 3 hours, or\nnone ever; data may still be served, it is not being refreshed.\n\nThe numbers live in this type-level doc because utoipa renders a unit\nenum's description from here and not from its variants. They are the\nenforced ceilings: `the_documented_ceilings_are_the_enforced_ceilings`\npins them to the constants in the status use case.","enum":["current","late","upstream_unreachable"],"example":"current"}}},"tags":[{"name":"series","description":"Windowed and `latest` time-series reads"},{"name":"bulk","description":"Bulk historical Parquet partitions + manifest discovery"},{"name":"status","description":"Per-dataset freshness: is the data current, and when was upstream last reached"}]}