VOLTSTACKBUILDER GUIDE
ENTSO-E · WEB API
BUILDER GUIDE · ENTSO-E TRANSPARENCY PLATFORM

ENTSO-E Transparency Platform API: how to get a token and query it

Reviewed , against the code that runs Voltstack's ENTSO-E feeds

THE SHORT ANSWER

To use the ENTSO-E API, register on the Transparency Platform, email transparency@entsoe.eu with “Restful API access” in the subject, generate a security token (the API key) under My Account, then send GET requests to https://web-api.tp.entsoe.eu/api with a documentType, an area code and a UTC period in yyyyMMddHHmm.

Answers come back as XML, except imbalance prices (A85) and unit outages (A77, A80), which come back as ZIP archives. ENTSO-E allows 400 requests a minute per token and bans the token for about ten minutes when you go over. The working requests, in curl and Python, are below, followed by the traps we met running these feeds every day.

BUILDER GUIDEREVIEWED: 6 NOV 2026SOURCE: ENTSO-E TRANSPARENCY PLATFORMAUTHOR: Voltstack Intelligence
ENTSO-E values Voltstack saw corrected after first publication · 23 Sep to 28 Oct 2026 · minimums
Spain imbalance price, surplus (A04)
2,423 of 3,674
quarter-hours corrected (66%), at least
Spain imbalance price, deficit (A05)
2,456 of 3,677
quarter-hours corrected (67%), at least
Austria imbalance price (A04)
1,606 of 3,274
quarter-hours corrected (49%), at least

Voltstack capture of the ENTSO-E Transparency Platform, 23 Sep 2026 00:00 UTC to 28 Oct 2026 14:02 UTC; recording of these series began on 23 Sep 2026. Each quarter-hour was re-read for about three days, so later corrections are not counted and every figure is a minimum. Method and change records: doi:10.5281/zenodo.23749147. ENTSO-E Transparency Platform, CC BY 4.0, aggregated and changed by Voltstack.

If you would rather not run the parser

Voltstack reads ENTSO-E's load, generation, outage, flow and imbalance series and does this parsing for you, naming the source on each. For imbalance prices and cross-border flows it keeps each value it reads with the time it read it, since 23 Sep 2026, so you can see what a quarter-hour said before it was corrected. The 14-day trial opens the workspace. The Voltstack API is in early access: keys are issued on request, not self-serve.

Start a 14-day free trialRequest early access →

The button opens a 14-day free trial sign-up. Sign-up needs a work email.

The button opens the early-access request on our home page.

01 · The security token

How to get an ENTSO-E API key

ENTSO-E calls the key a security token. Getting one takes an email, not a form.

  1. Register a free account on transparency.entsoe.eu and confirm your email address.
  2. Email transparency@entsoe.eu from that address with Restful API access in the subject, and name the account email in the body.
  3. Wait for ENTSO-E to switch API access on. It can take up to three working days.
  4. Sign in, open My Account, and choose to generate a new security token.

The token travels as the securityToken query parameter on every request, so it ends up in any log that records URLs. Keep it in an environment variable, keep it out of code and screenshots, and do not print request URLs in your logs. A request with no token or a wrong one is refused with HTTP 401, which is also a cheap way to check that the API is up.

The File Library, ENTSO-E's bulk CSV extracts, is separate: it takes your website login, not the token, and has its own limit of 100 requests a minute.

02 · Anatomy of a request

Document type, area, period

Every request is a GET to https://web-api.tp.entsoe.eu/api with the token, a documentType, one or two area parameters holding EIC codes, and periodStart and periodEnd in UTC as yyyyMMddHHmm. Some document types need a processType or another filter. The name of the area parameter changes with the document type, which is the first thing that trips people up.

Data (Transparency article)documentTypeArea parameterAlso needsFree re-use list
Day-ahead prices (12.1.D)A44in_Domain and out_Domain, both the zonecontract_MarketAgreement.type=A01No: the exchange's or TSO's agreement
Actual total load (6.1.A)A65outBiddingZone_DomainprocessType=A16No: the owner's agreement
Day-ahead load forecast (6.1.B)A65outBiddingZone_DomainprocessType=A01Yes, CC BY 4.0
Actual generation per type (16.1.B and C)A75in_DomainprocessType=A16No: the owner's agreement
Unit unavailability (15.1.A to D)A77 production, A80 generationbiddingZone_Domainoffset=0, 200, 400 ...No: the owner's agreement
Cross-border physical flows (12.1.G)A11out_Domain and in_Domain, the two zonesnoneYes, CC BY 4.0
Imbalance prices (17.1.G)A85controlArea_DomainnoneYes, CC BY 4.0

The answer is a market document holding one or more TimeSeries. Each has one or more Period blocks with a time interval, a resolution such as PT15M or PT60M, and numbered points. A query with no data, or one the API will not run, comes back as an Acknowledgement_MarketDocument whose Reason says why. This is the shape of an actual total load answer, values left out:

<GL_MarketDocument xmlns="urn:iec62325.351:tc57wg16:451-6:generationloaddocument:3:0">
  <mRID>...</mRID>
  <revisionNumber>1</revisionNumber>
  <type>A65</type>
  <process.processType>A16</process.processType>
  <TimeSeries>
    <outBiddingZone_Domain.mRID codingScheme="A01">10YFR-RTE------C</outBiddingZone_Domain.mRID>
    <quantity_Measure_Unit.name>MAW</quantity_Measure_Unit.name>
    <curveType>A01</curveType>
    <Period>
      <timeInterval><start>2026-11-04T23:00Z</start><end>2026-11-05T23:00Z</end></timeInterval>
      <resolution>PT15M</resolution>
      <Point><position>1</position><quantity>...</quantity></Point>
      <Point><position>2</position><quantity>...</quantity></Point>
      ...
    </Period>
  </TimeSeries>
</GL_MarketDocument>

Area codes we use

AreaEIC codeNote
DE-LU (bidding zone)10Y1001A1001A82HDay-ahead, load, generation and outages. Two day-ahead auctions; see the traps.
France10YFR-RTE------CThe same code serves the A85 imbalance query.
Netherlands10YNL----------LThe same code serves the A85 imbalance query.
Belgium10YBE----------2Our A85 imbalance query uses the same code.
Austria10YAT-APG------LTwo day-ahead auctions, as DE-LU.
Spain10YES-REE------0Imbalance prices in two categories, A04 and A05.
Norway NO210YNO-2--------TBidding zone. Norway as a whole is 10YNO-0--------C for generation and outages.
Italy (national)10YIT-GRTN-----BFor generation and outages. Publishes A80 outages, not A77.

Great Britain has no day-ahead price or generation data on the platform, and an actual load query for GB answers “No matching data found”. Its cross-border flows still publish.

03 · Working requests in curl

Day-ahead, load, generation and outages from the shell

Set the token and two area codes once. Times are UTC, so a delivery day in Central European Time starts at 23:00 UTC the evening before in winter and 22:00 UTC in summer.

export ENTSOE_TOKEN="paste-your-token-here"
API="https://web-api.tp.entsoe.eu/api"
DE_LU="10Y1001A1001A82H"
FR="10YFR-RTE------C"

# Delivery day 5 Nov 2026 in CET is 4 Nov 23:00 to 5 Nov 23:00 UTC.
FROM=202611042300
TO=202611052300

Day-ahead prices (A44)

Day-ahead prices belong to the power exchanges and are not on ENTSO-E's free re-use list, so this guide shows how to request them and prints none. contract_MarketAgreement.type=A01 asks for the daily auction only; leave it out and a zone that also publishes intraday auctions mixes the two in one answer.

# Day-ahead prices (A44), DE-LU. Saved to a file, not printed.
curl -s "$API?securityToken=$ENTSOE_TOKEN&documentType=A44\
&contract_MarketAgreement.type=A01\
&in_Domain=$DE_LU&out_Domain=$DE_LU\
&periodStart=$FROM&periodEnd=$TO" -o a44-de-lu.xml

Actual total load (A65) and generation per type (A75)

# Actual total load (A65, processType A16), France
curl -s "$API?securityToken=$ENTSOE_TOKEN&documentType=A65&processType=A16\
&outBiddingZone_Domain=$FR&periodStart=$FROM&periodEnd=$TO" -o a65-fr.xml

# Actual generation per production type (A75, processType A16), France
curl -s "$API?securityToken=$ENTSOE_TOKEN&documentType=A75&processType=A16\
&in_Domain=$FR&periodStart=$FROM&periodEnd=$TO" -o a75-fr.xml

Unit outages (A77 and A80)

# Generation unit unavailability (A80), France, the next 30 days.
# The answer is a ZIP with one XML document per outage, 200 at most:
# ask again with offset=200, 400 ... until a page comes back short.
curl -s "$API?securityToken=$ENTSOE_TOKEN&documentType=A80\
&biddingZone_Domain=$FR&offset=0\
&periodStart=202611060000&periodEnd=202612060000" -o a80-fr-0.zip
unzip -l a80-fr-0.zip | tail -1

# A77 (production units) takes the same parameters.

# Older versions of one outage: ENTSO-E returns them only when the request
# names the outage's mRID (the mRID element of a document above).
curl -s "$API?securityToken=$ENTSOE_TOKEN&documentType=A80\
&biddingZone_Domain=$FR&mRID=PASTE-OUTAGE-MRID\
&periodStart=202611060000&periodEnd=202612060000" -o a80-one-outage.zip
unzip -l a80-one-outage.zip

Imbalance prices (A85)

# Imbalance prices (A85), Spain. Also a ZIP, asked for by imbalance price area.
curl -s "$API?securityToken=$ENTSOE_TOKEN&documentType=A85\
&controlArea_Domain=10YES-REE------0\
&periodStart=$FROM&periodEnd=$TO" -o a85-es.zip
unzip -l a85-es.zip
04 · The same requests in Python

ENTSO-E API in Python with requests

Two helpers do most of the work: fetch sends the request, unzips the archive types and turns an acknowledgement into an exception, and points turns one TimeSeries into a dictionary of UTC start times. Python 3.9 or later, with requests.

import io
import os
import zipfile
import xml.etree.ElementTree as ET
from datetime import datetime, timedelta

import requests

API = "https://web-api.tp.entsoe.eu/api"
TOKEN = os.environ["ENTSOE_TOKEN"]


def fetch(**params):
    """Return the XML documents in one answer, unzipping A85, A77 and A80."""
    r = requests.get(API, params={"securityToken": TOKEN, **params}, timeout=60)
    if r.status_code == 429:
        raise RuntimeError(r.text)  # "Requester banned until <instant>"
    if r.content[:2] == b"PK":
        with zipfile.ZipFile(io.BytesIO(r.content)) as z:
            return [z.read(name) for name in z.namelist()]
    if b"Acknowledgement_MarketDocument" in r.content:
        reason = ET.fromstring(r.content).findtext(".//{*}Reason/{*}text")
        raise LookupError(reason)  # no data for the query, or a refused query
    r.raise_for_status()
    return [r.content]


STEP = {"PT15M": 15, "PT30M": 30, "PT60M": 60}


def utc(text):
    return datetime.fromisoformat(text.replace("Z", "+00:00"))


def points(series, value_tag):
    """One TimeSeries as {UTC start: value}. Under curveType A03 a point that
    repeats the one before is left out, so carry it forward to the end."""
    out = {}
    fill = series.findtext("{*}curveType") == "A03"
    for period in series.iterfind("{*}Period"):
        start = utc(period.findtext("{*}timeInterval/{*}start"))
        end = utc(period.findtext("{*}timeInterval/{*}end"))
        step = timedelta(minutes=STEP[period.findtext("{*}resolution")])
        given = {
            int(p.findtext("{*}position")): float(p.findtext("{*}" + value_tag))
            for p in period.iterfind("{*}Point")
        }
        last = None
        for pos in range(1, int((end - start) / step) + 1):
            if pos in given:
                last = given[pos]
            elif not fill:
                continue
            if last is not None:
                out[start + (pos - 1) * step] = last
    return out

Day-ahead prices, keeping the SDAC series

DE_LU = "10Y1001A1001A82H"
SEQ = ".//{*}classificationSequence_AttributeInstanceComponent.position"

[doc] = fetch(
    documentType="A44",
    **{"contract_MarketAgreement.type": "A01"},
    in_Domain=DE_LU,
    out_Domain=DE_LU,
    periodStart="202611042300",
    periodEnd="202611052300",
)
day_ahead = {}
for ts in ET.fromstring(doc).iterfind(".//{*}TimeSeries"):
    if ts.findtext(SEQ) in (None, "1"):  # 1 is SDAC; 2 is the EXAA auction
        day_ahead.update(points(ts, "price.amount"))

print(len(day_ahead), "quarter-hours")  # 96 on a normal day, 92 or 100 on a clock change

Load and generation

FR = "10YFR-RTE------C"
window = dict(periodStart="202611042300", periodEnd="202611052300")

[doc] = fetch(documentType="A65", processType="A16", outBiddingZone_Domain=FR, **window)
load = {}
for ts in ET.fromstring(doc).iterfind(".//{*}TimeSeries"):
    load.update(points(ts, "quantity"))  # MW, a rate

[doc] = fetch(documentType="A75", processType="A16", in_Domain=FR, **window)
generation = {}
for ts in ET.fromstring(doc).iterfind(".//{*}TimeSeries"):
    if ts.find("{*}inBiddingZone_Domain.mRID") is None:
        continue  # pumping and battery charging arrive as a second series
    fuel = ts.findtext(".//{*}psrType")  # B14 nuclear, B04 fossil gas ...
    generation.setdefault(fuel, {}).update(points(ts, "quantity"))

Outages, page by page

Each outage is its own document with an mRID and a revisionNumber. The unavailable capacity is the nominal power minus the available quantity in each Available_Period.

# Without DocStatus, ENTSO-E returns active and cancelled outages only.
outages, offset = [], 0
while True:
    try:
        docs = fetch(
            documentType="A80",
            biddingZone_Domain=FR,
            offset=offset,
            periodStart="202611060000",
            periodEnd="202612060000",
        )
    except LookupError:
        break  # nothing (more) for this window
    for xml in docs:
        root = ET.fromstring(xml)
        outages.append({
            "mrid": root.findtext("{*}mRID"),
            "revision": int(root.findtext("{*}revisionNumber")),
            "status": root.findtext("{*}docStatus/{*}value") or "active",  # A09 cancelled, A13 withdrawn
            "kind": root.findtext(".//{*}businessType"),  # A53 planned, A54 forced
            "unit": root.findtext(".//{*}production_RegisteredResource.name"),
            "nominal": root.findtext(".//{*}production_RegisteredResource.pSRType.powerSystemResources.nominalP"),
        })
    if len(docs) < 200:
        break
    offset += 200


def versions(mrid, period_start, period_end):
    """The versions of one outage that ENTSO-E returns for the period you ask.
    Without mRID a query returns the current version only. Withdrawn versions
    come back only on a pass with DocStatus=A13, so ask twice, page by page,
    and make the period cover the outage's whole life. ENTSO-E caps one
    request at one year, so a longer window is refused with an
    acknowledgement, which this loop reads as "no more": split a longer
    life into yearly windows and call it once for each."""
    found = {}
    for status in (None, "A13"):  # active and cancelled, then withdrawn
        offset = 0
        while True:
            params = dict(
                documentType="A80",
                biddingZone_Domain=FR,
                mRID=mrid,
                offset=offset,
                periodStart=period_start,
                periodEnd=period_end,
            )
            if status:
                params["DocStatus"] = status
            try:
                docs = fetch(**params)
            except LookupError:
                break
            for xml in docs:
                root = ET.fromstring(xml)
                revision = int(root.findtext("{*}revisionNumber"))
                found[revision] = root.findtext("{*}docStatus/{*}value") or "active"
            if len(docs) < 200:
                break
            offset += 200
    return dict(sorted(found.items()))

Imbalance prices

[doc] = fetch(
    documentType="A85",
    controlArea_Domain="10YES-REE------0",
    periodStart="202611042300",
    periodEnd="202611052300",
)
imbalance = {}
for ts in ET.fromstring(doc).iterfind(".//{*}TimeSeries"):
    category = ts.findtext(".//{*}imbalance_Price.category") or "single"  # A04 or A05
    imbalance.setdefault(category, {}).update(points(ts, "imbalance_Price.amount"))

print({k: len(v) for k, v in imbalance.items()})  # counts, one entry per category

If you would rather not write the parsing, entsoe-py is a widely used open-source Python client that returns pandas series and data frames, and entsoeapi does the same for R. Whichever you use, check a day you know against the platform's website, a clock-change day included, and check how it handles the traps in section 06.

05 · Rate limits and outages

400 requests a minute, and a ban when you go over

ENTSO-E allows 400 requests a minute per token. The next one gets HTTP 429 with a body that reads “Requester banned until” and an instant about ten minutes ahead, and every request on that token fails until then. The ban covers the token, so a website and a worker that share one token share the ban.

We met it on 8 Sep 2026, and it was our own doing. After a long outage, our health checks retried every failed ENTSO-E request on each poll, and the moment the API came back the burst went over the limit: the first answer after the outage was the ban. Since then one gate sits in front of every request we send. It admits at most 80 a minute from one process, honours the instant in a 429, and stops for a minute after eight failures in a row, so a failed burst is remembered rather than repeated.

The API goes down for days

  • The Web API answered 503 from the evening of 30 Aug 2026 until 2 Sep, about 65 hours hard down.
  • Our monitoring saw it down again for about 27 hours on 7 and 8 Sep 2026, during ENTSO-E's infrastructure migration.
  • The File Library kept serving monthly extracts through the first outage. Build the fallback before you need it, and alert on stale data, not only on failed requests.
06 · Traps we met in production

What the documentation does not warn you about

A85 imbalance prices arrive as a ZIP

Imbalance prices come back as a ZIP archive even for a one-hour window, not as XML like day-ahead prices. The archive is written with the streaming flag set, so the compressed size in each local header is zero and the real sizes sit only in the central directory. Python's zipfile reads the central directory and copes; a small hand-written reader that trusts the local header gets nothing. Inside, the value tag is imbalance_Price.amount with a capital P. Areas that price both directions, Spain and France among them, publish two series for the same quarter-hour, category A04 for surplus and A05 for deficit, with different values: keep them apart. A query with no data comes back as a plain, unzipped acknowledgement. Unit outages (A77, A80) are ZIPs too, one XML document per outage.

A44 returns two day-ahead auctions for DE-LU and Austria

DE-LU and Austria have a second day-ahead auction run by EXAA, so A44 returns two TimeSeries per delivery day with the same contract type and business type. Only classificationSequence_AttributeInstanceComponent.position tells them apart: 1 is the SDAC coupling price, 2 is EXAA. Their order in the document is not stable; we saw both orders live, so code that lets the last series win picks an auction by luck. We found it on 20 Jul 2026, when our cross-check against the Bundesnetzagentur's SMARD failed for DE-LU and Austria and matched to the cent everywhere else.

A second trap sits inside the first. Day-ahead series use curveType A03, which leaves out any point whose value repeats the one before. Left unfilled, those gaps read as missing quarter-hours, and the EXAA series' explicit point at the same instant can slip into them. Carry each value forward to the end of its period, as points() above does, and keep sequence 1 and untagged series only.

DE-LU, 13 Sep 2026: a day published late

When we looked on 14 Sep 2026, the A44 answer for DE-LU delivery day 13 Sep carried only the EXAA series. The SDAC series was absent from every window we tried, including a request filtered to position 1, and the File Library's monthly extract lacked the same rows. The auction had cleared and other public sources had all 96 quarter-hours, so the gap was in the platform's publication. ENTSO-E published an SDAC series for that day later: by 23 Sep it was there, with 95 points in our record, so count it again before you call it whole.

Late publication is the trap. A parser that correctly drops the EXAA series is left with no prices for that day, and a feed that re-reads only the last few days never picks up the late series once the day has fallen out of its window, so the hole stays in your store after ENTSO-E has published the series. Count points per delivery day against what the day should hold (96 quarter-hours, 92 or 100 on a clock-change day), alert on a short day, and go back for it later rather than trusting that a 200 OK means complete.

More we met

  • Outage paging. A77 and A80 answer at most 200 documents, and a query that would return more fails with “exceeds the allowed maximum” unless you pass offset. With offset=0, 200, 400 it pages cleanly.
  • Italy and kilowatts. Italy publishes A80 outages and no A77. Some Italian units give their nominal power in kW while labelling it MW: in an Italian A80 pull on 23 Sep 2026, 191 of 327 documents carried a nominal power between 106,000 and 885,000. Read as MW, one plant showed hundreds of gigawatts unavailable.
  • Storage in the generation mix. In A75, pumped storage (B10) and batteries (B25) publish two series per fuel: one tagged inBiddingZone_Domain for generation and one tagged outBiddingZone_Domain for pumping or charging. Add them together and you blend output with consumption.
  • Resolution changes under you. Day-ahead moved to 15-minute products on 1 Oct 2025, so a long history mixes PT60M and PT15M periods. Read the resolution from each period, never assume it.
  • Imbalance prices are reported, by default, per imbalance price area. A85 takes the area in controlArea_Domain, and the area is not always the bidding zone. In July 2026 we asked for the four German control-area codes (10YDE-VE-------2, 10YDE-RWENET---I, 10YDE-EON------1, 10YDE-ENBW-----N) on a 35-day lookback, and all four returned nothing but acknowledgements. Germany settles imbalance with one price, reBAP, that its TSOs publish on netztransparenz.de.
07 · What the raw API will not give you

The first value, outage filings as of a moment, one schema

The value as first published

The API answers with values as they stand when you ask. Ask again tomorrow and a corrected value replaces the one you saw, with nothing in the answer to say it changed. Imbalance prices carry a status of intermediate or final, and on 28 Oct 2026 some Belgian prices from January 2026 and Austrian prices for whole months of 2025 were still intermediate. If a backtest, a settlement check or a trade review needs to know what the number said at the time, you have to keep that copy yourself, with the time you read it.

Series (ENTSO-E)Quarter-hours first seenCorrectedDetail
Spain imbalance price, surplus (A04)3,6742,423 (66%)Typically about 35 hours from first capture to last correction; 142 corrections changed the sign of the price. A minimum.
Spain imbalance price, deficit (A05)3,6772,456 (67%)Typically about 34.5 hours to last correction; 140 changed sign. A minimum.
Austria imbalance price (A04)3,2741,606 (49%)1,248 of the 1,606 were corrected more than once; typically 38 hours to last correction. A minimum.

These counts come from Voltstack's own capture between 23 Sep and 28 Oct 2026, read every 30 minutes with each read reaching about three days back. A correction made after those three days is not counted, and later corrections are common: for delivery on 21 Sep, 96 of the 102 Spanish quarter-hours we had recorded as corrected had changed again by 28 Oct. The window is five weeks, so do not read it as “always”. The method and the change records are published: the paper (doi:10.5281/zenodo.23749147) and its dataset (doi:10.5281/zenodo.23749094). Both are ours: the author runs Voltstack, and the capture is Voltstack's.

Outage filings as they stood at a past moment

Ask for a window of A77 or A80 and you get each outage at its current version. ENTSO-E's documentation says older versions of an outage are returned only when the request names its mRID, so the history is there one outage at a time: you need the mRID of each outage you care about and a request for each, as versions() above does. Without mRID a query returns current versions only, so no single request rebuilds the whole outage list as it stood at 08:00 on the morning you traded. The revisionNumber counts the filings, and we have seen one reach 256; a cancelled outage carries docStatus A09 and a withdrawn one A13, and a query without DocStatus returns active and cancelled outages only, so withdrawn ones need a second request with DocStatus=A13. If you need that as-of view across every unit, store each revision as you see it, keyed on mRID and revisionNumber, and never overwrite.

One schema across zones

Each document type names its area differently, resolution differs by zone and by year, A03 series leave out repeated points, some zones publish two auctions or two imbalance categories and others one, the four German control-area codes returned no imbalance prices when we tried them in July 2026, Italy files A80 and no A77, and GB is gone from prices and generation. Every consumer of the raw API ends up writing the same normalisation layer, and finds each of these differences the first time a cross-check fails.

08 · Publishing what you fetch

Free to download is not free to republish

Items on ENTSO-E's List of Data Available for Free Re-use (18 Oct 2023) are CC BY 4.0 under its Terms of Use applicable from 1 Nov 2023: load forecasts, cross-border flows, transfer capacities, redispatch, and balancing and imbalance data among them. Credit “ENTSO-E Transparency Platform”, link the licence and say what you changed. IFA and Nemo Link data are excluded even for listed items.

Items off the list, day-ahead prices, actual total load, generation and unit outages among them, need the agreement of the data's primary owner before you republish them or feed them to someone else's model, and ENTSO-E's terms say its own rights cannot be sub-licensed. Day-ahead prices belong to the exchanges whichever route they reach you by. The 2027 edition of our API ratings sets out the licence of each source, including for AI use. This is a reading of published terms, not legal advice.

09 · Quick answers

ENTSO-E API questions

How do I get an ENTSO-E API key?

Register a free account on transparency.entsoe.eu, then email transparency@entsoe.eu from the same address with “Restful API access” in the subject. Once ENTSO-E grants access, which can take up to three working days, sign in, open My Account and generate a security token. The token is the API key: it goes on every request as the securityToken parameter.

Is the ENTSO-E API free?

Reading is free. Republishing is a separate question: items on ENTSO-E's List of Data Available for Free Re-use, such as cross-border flows, load forecasts and imbalance prices, are CC BY 4.0, while day-ahead prices, actual total load, generation and outage data need the owner's agreement before you republish them.

What is the ENTSO-E API rate limit?

ENTSO-E sets 400 requests a minute per token. Go over it and the API answers HTTP 429 with “Requester banned until” and an instant about ten minutes ahead, and every request on that token fails until then. Keep well under the limit when a worker and a website share one token.

Why does the ENTSO-E imbalance price request return a ZIP file?

Imbalance prices (documentType A85) and unit outages (A77 and A80) come back as ZIP archives, not plain XML. The archive is written with the streaming flag, so the sizes sit in its central directory, not in the local header. Python's zipfile reads it; a hand-written reader that trusts the local header reads nothing.

Why does ENTSO-E return two day-ahead prices for Germany?

DE-LU and Austria have a second day-ahead auction, EXAA, so A44 returns two TimeSeries per delivery day. They differ only in classificationSequence_AttributeInstanceComponent.position: 1 is the SDAC coupling price, 2 is EXAA. The order in the document is not stable, so filter on the position instead of taking the last series.

Does ENTSO-E data change after it is published?

Yes. In Voltstack's capture from 23 Sep to 28 Oct 2026, 2,423 of 3,674 quarter-hours of Spain's surplus imbalance price, 2,456 of 3,677 of its deficit price and 1,606 of 3,274 of Austria's imbalance price were corrected after we first saw them. All are minimums. A repeat request returns the corrected value, so keep the first one yourself if you need it.

Is there a Python library for the ENTSO-E API?

Yes: entsoe-py is a widely used open-source client that returns pandas series and data frames. The plain requests code on this page shows what it does underneath. Whichever you use, check a delivery day you know, a clock-change day included, against the platform's own website.
Voltstack Analytics

Keep the value you saw, not only the value that stands now

Voltstack keeps each value it reads with the time it read it: ENTSO-E imbalance prices and cross-border flows since 23 Sep 2026, so a desk can see what a quarter-hour said at a past moment and what it says now. Other series are served as currently published. The 14-day trial opens the workspace with these series in it; the Voltstack API is in early access, with keys issued on request.

Start a 14-day free trialRequest early access →or see the live demo

The button opens a 14-day free trial sign-up. Sign-up needs a work email.

The button opens the early-access request on our home page.

Related Voltstack Intelligence

More for builders

RATINGS · 2027 EDITION
Best European Energy Data APIs (2027)
APIs rated for builders, with the licence for AI use and how often values are corrected.
RATINGS · 2026 EDITION
Best European Energy Data APIs & Terminals (2026)
The previous edition, with integration notes as of 24 Sep 2026.
STRUCTURAL REFERENCE
2026 European Gas & Power Data Provider Ratings
Data providers rated across price reporters, analytics platforms and alternative data.

Sources: ENTSO-E Transparency Platform (https://transparency.entsoe.eu/), its Web API and File Library, its Terms of Use (1 Nov 2023) and List of Data Available for Free Re-use (18 Oct 2023). Request parameters, area codes and the traps come from the code and notes behind Voltstack's own ENTSO-E feeds, as of 6 Nov 2026. Correction counts: Voltstack capture of the ENTSO-E Transparency Platform, 23 Sep to 28 Oct 2026, CC BY 4.0, aggregated and changed by Voltstack (doi:10.5281/zenodo.23749147). This page prints no price from ENTSO-E or any exchange.

Disclaimer: Produced by Voltstack Intelligence for information only. ENTSO-E changes its API and terms from time to time; check the platform's own documentation before you rely on a parameter here. Corrections to research@voltstack.energy.

VOLTSTACK INTELLIGENCE · BUILDER GUIDE · REVIEWED 6 NOV 2026voltstack.energy