Energy Data · Quickstart

Quickstart

Fetch your first Dutch imbalance prices in under two minutes. The last 31 days need no API key.

There is no signup, and the only thing you need is a terminal. Without an API key, a series or gaps read reaches back at most 31 days. Older history and the bulk files take a key; the second half shows how to use one.

Read the latest price

bash
curl https://api.tolven.eu/v1/series/imbalance-prices/NL/latest

NL is the zone, and every series and gaps path names one. Leave it out and the API answers 404 with "code": "missing_zone" and the zones it serves.

Each row is one interval, with both imbalance prices side by side:

one row
{
"ts": "2026-10-16T21:45:00.000Z",
"resolution": "PT15M",
"surplus": 153.04,
"shortage": 229.53,
"first_known_at": "2026-10-16T22:50:01.597Z",
"known_at": "2026-10-16T22:50:01.597Z",
"revised": false
}

surplus is the price for a party that was long over the interval, and shortage the price for one that was short. Both are in EUR/MWh, and the fields block of each wide response describes them. resolution is the interval’s length as an ISO 8601 duration. first_known_at is when we first fetched a value for the interval. known_at is when we first fetched the newest revision among its values. Both are our fetch times, not upstream’s publication times. revised turns true once we have fetched a later revision of any of the row’s values. A revision can change the resolution or unit alone, so true does not mean a number changed. You always know which revision you are looking at. For each price on its own, see one entry per field; it has a latest too.

Freshness of latest

In NL, imbalance settlement arrives in one batch per Dutch local day, not continuously. So the newest observation is normally between 1 and 25 hours old. It is youngest just after a batch lands and oldest just before the next one does. Plan around that shape rather than around a fixed interval. We poll far more often than upstream publishes, so our own delay is not what you are waiting on.

Measured 2026-08-16: upstream published at about 23:00 UTC on each of the previous 44 days. Before 2026-06-30 it published several hours later. The time also follows the Dutch local day rather than UTC, so expect it around 00:00 UTC while the Netherlands is on winter time. Treat that clock time as an observation of recent behaviour, not as a schedule, and build on the shape instead.

BE and FR publish each interval within the hour, and revise them often. The imbalance-prices page gives both measured.

Read a window

bash
curl https://api.tolven.eu/v1/series/imbalance-prices/NL

With no dates, you get the last 7 days. Add from and to for any other window. A window spans at most 32 days. The API rejects a wider window with 400 rather than truncating it silently, so a partial answer is never mistaken for a complete one.

Without a key, a window must start at or after anonymous_from in /v1/status: a UTC midnight, 31 days before today’s, not a rolling 31 × 24 hours. A request that starts earlier gets a 401 with "code": "requires_key", never a shorter answer. To read everything you can reach without a key, send anonymous_from as from and leave to out. With a key, NL’s series runs back to 2020-08-01.

One entry per field

A row’s known_at and revised cover both of its prices. To see which price was revised, read the tall path instead:

bash
curl https://api.tolven.eu/v1/series/imbalance-prices/NL/tall

It takes the same from, to and as_of as a wide read, with the same default window, the same window cap and the same 31 days without a key. It answers with observations: one entry per field and interval, grouped by field. This is the surplus half of the row shown earlier:

one observation
{
"field": "surplus",
"ts": "2026-10-16T21:45:00.000Z",
"resolution": "PT15M",
"value": 153.04,
"unit": "EUR/MWh",
"known_at": "2026-10-16T22:50:01.597Z",
"revision": 1
}

Each entry carries its own unit, known_at and revision, so a tall response has no fields block. Revision 1 is the first value we fetched; a value upstream has restated carries a higher one. /v1/series/imbalance-prices/NL/tall/latest gives each field’s newest entry. Those can belong to different intervals, so compare their ts before pairing them.

Check for gaps before you trust a window

The API never interpolates gaps, so ask for them explicitly:

bash
curl https://api.tolven.eu/v1/gaps/imbalance-prices/NL

It takes the same default window, the same window cap and the same 31 days without a key as a series read. You get interior gaps plus a coverage envelope: observed_from and observed_through per field. Absence outside the envelope is not a gap, so the API does not report it.

With a key

A key reaches NL’s whole series and its gaps back to 2020-08-01, as_of across all of it, and the bulk files. We issue keys by hand, and the energy-data page says how to request one. The examples below expect your key in TOLVEN_KEY and send it as an Authorization: Bearer tlv_… header. A request without the header is still served, over the last 31 days.

Ask what was known at a point in time

TenneT restates imbalance settlement after the fact. If you backtest against today’s values, you are trading on information you could not have had. Your returns will look better than they were.

In NL, revisions are rare, which is what makes them dangerous. Between 2026-06-18 and 2026-10-19 we observed revisions to 4 of 23,438 observations in NL. /v1/status carries the current count. The largest moved a settlement price from €500.00/MWh to €113.49/MWh, thirteen days after the interval had closed. A backtest run against live data used the wrong value for that period, and nothing flagged it. Three of the others arrived together, within hours of an upstream outage ending: what a source shows just after it recovers may not be settled yet either.

BE and FR revise far more often. Of the intervals we watched arrive up to 2026-11-01, 23% moved afterwards in BE and 18% in FR.

Add as_of and you get the latest revision known at that instant, with anything we learned later hidden. This is the interval from that example:

bash
curl -H "Authorization: Bearer $TOLVEN_KEY" \
"https://api.tolven.eu/v1/series/imbalance-prices/NL?from=2026-07-15T07:00:00Z&to=2026-07-15T07:15:00Z&as_of=2026-07-16T00:00:00Z"

shortage comes back as 500.0, the value as it stood the day after the interval. Drop as_of and you get 113.49, with revised set and a known_at on 2026-07-28. Without a key, as_of works the same way inside the last 31 days.

One boundary to know about. Every series and bulk response carries revisions_observed_from: the instant the API started watching upstream. Points after it have true revision history. The API saw earlier points only retrospectively, so their first recorded value is the final settled one, not what upstream first published. Their first_known_at is null, and their known_at is when we loaded them. Upstream serves no revision archive, so that earlier history cannot be reconstructed. The API rejects an as_of before the boundary with 400 rather than returning an empty result that would read as “nothing was known”. The gaps endpoint is live-only, so it carries no boundary and takes no as_of.

Pull history in bulk

For anything longer than a few days, skip JSON. The whole history is one object:

bash
curl -H "Authorization: Bearer $TOLVEN_KEY" -O https://api.tolven.eu/v1/bulk/imbalance-prices/full.parquet

That is every redistributable interval at its current value, in a single request: NL back to 2020-08-01, BE and FR back to December 2014. Beside it, full.revisions.parquet holds every revision we have observed, one row per field and revision. Take them once, then stay current from the monthly partitions:

bash
curl https://api.tolven.eu/v1/bulk/imbalance-prices

The manifest itself needs no key, so you can see what the files hold before you request one. Each month in the manifest lists two files: its current values and its revisions. A month is a UTC month. Each file carries a row count, byte size, ETag and absolute URL. Frozen months keep stable URLs and ETags, so they cache indefinitely. Re-download only the months whose ETag moved, which is normally just the settlement tail.

python
import os
import pandas as pd
key = {"Authorization": f"Bearer {os.environ['TOLVEN_KEY']}"}
revisions = pd.read_parquet(
"https://api.tolven.eu/v1/bulk/imbalance-prices/full.revisions.parquet",
storage_options=key,
)
# Every revision carries its known_at, so point-in-time filtering is
# yours to do; no query engine required.
cutoff = pd.Timestamp("2026-08-01", tz="UTC")
known_at_cutoff = (
revisions[revisions["known_at"] <= cutoff]
.sort_values("revision")
.drop_duplicates(["zone", "field", "ts"], keep="last")
)

For current values only, read full.parquet the same way.

Rate limits

The JSON endpoints allow 120 requests a minute per IP. Bulk downloads allow 60 a minute. With a key, both are five times higher and counted per key. Past a limit the API answers 429 with a Retry-After header; the limit resets within a minute. With a key, no workflow here needs many requests: the whole history is one download. Staying current after it costs one manifest read plus the months that moved.

Where to go next

MethodPathWhat it doesWithout a key
GET/v1/series/{dataset}/{zone}Windowed reads, optionally as_ofLast 31 days
GET/v1/series/{dataset}/{zone}/latestThe newest intervalYes
GET/v1/series/{dataset}/{zone}/tallWindowed reads, one entry per fieldLast 31 days
GET/v1/series/{dataset}/{zone}/tall/latestThe newest entry per fieldYes
GET/v1/gaps/{dataset}/{zone}Interior gaps plus a coverage envelopeLast 31 days
GET/v1/bulk/{dataset}/full.parquetCurrent values for the whole historyNo
GET/v1/bulk/{dataset}/full.revisions.parquetEvery observed revisionNo
GET/v1/bulk/{dataset}Monthly manifest and its filesManifest only

Full parameters, response shapes and error codes live in the API reference. The running API generates it, so it cannot drift. Before you build on any of this in production, read attribution and licensing; it says what you may redistribute.