CARBON INTENSITY API

Last-hour grid carbon intensity, by country.
Free & Open Source.

Operational, lifecycle and consumption-based carbon intensity (gCO2eq/kWh) for every country with published electricity data — 213 in total. True clock hours where a live grid feed exists, a year of hourly history behind them, and the annual average on its own route everywhere else.

Quick example

Get the last completed hour for one country (ISO-3166 alpha-2):

curl https://ci-api.fabiocicerchia.it/v2/DE/past-hour
{
  "country": "Germany",
  "country_code": "DE",
  "zone": "DE",
  "unit": "gCO2eq/kWh",
  "period_start": "2026-08-27T17:00:00Z",
  "period_end": "2026-08-27T18:00:00Z",
  "resolution_sec": 900,
  "points": 4,
  "points_expected": 4,
  "complete": true,
  "direct": 280,
  "lifecycle": 330,
  "consumption_direct": 320,
  "consumption_lifecycle": 370,
  "basis": "measured",
  "data_source": { "name": "ENTSO-E", "realtime": true, "status": "operational" },
  "generated_at": "2026-08-27T19:13:36Z"
}

period_start and period_end are a true clock hour. resolution_sec is how wide the underlying provider points are — 900 for ENTSO-E's quarter-hours — and points against points_expected is how much of the hour the mean covers. complete is always true here; use /current-hour for the hour still filling in.

Reading a response

The three numbers, and how to tell whether a reading is current.

Every reading carries four intensities, all in gCO2eq/kWh. They are the corners of two independent axes: how much of the supply chain is counted, and whether the figure describes electricity made in the country or electricity used there.

Production-based counts what the country generates, exports included. Consumption-based counts what comes out of a socket there, adjusted for trade. They part company wherever a country imports heavily: Switzerland generates at 39 on hydro and nuclear but consumes at 179, because much of what it uses is bought in. Running a server in Zürich, 39 flatters you.

Production-basedConsumption-based
Combustion only direct consumption_direct
Plus upstream lifecycle consumption_lifecycle

They are not a ladder. consumption_direct is larger than lifecycle for an importing country while counting less of the supply chain — the suffixes say which corner each figure sits in.

FieldWhat it counts
direct Combustion only — what the stack emits while generating, computed from the live fuel mix. Nuclear, hydro, wind and solar count as zero. The only figure that genuinely varies hour to hour.
lifecycle Adds extraction, transport, construction and manufacturing — why wind and solar are not truly zero. IPCC AR6 factors, applied as a fixed per-country uplift. Use this one by default.
consumption_direct Adds imported electricity, so it describes what a country consumes rather than what it generates — still combustion-only, hence the suffix. A fixed annual adjustment, and absent from zone readings.
consumption_lifecycle Both adjustments at once: what is consumed here, counted across the whole supply chain. The figure to report a footprint with.
Also the most modelled of the four — the upstream uplift is derived from the domestic generation mix and applied to the consumed one, which assumes imports carry a similar upstream intensity per kWh.

All four are direct plus a per-country constant, so within one country they rank the hours of a day identically. Deciding when to run something, any of them gives the same answer.

Across countries they do not. The constants differ, so the ordering changes: by direct, Switzerland (39) looks cleaner than France (41); by consumption_direct it is France (85) against Switzerland's (179). Deciding where to run something, the figure you pick decides the answer — use consumption_lifecycle.

Is the reading current?

There is no stale flag: responses are static files, so nothing can evaluate freshness at the moment you ask. generated_at tells you when the snapshot was built, and two checks cover it:

stale = (Date.now() - Date.parse(r.generated_at)) > 3900e3
     || r.basis !== "measured";

Endpoints

Lookups are path-based — the bucket is served directly, so there is nothing in the request path to read a query string. See the API reference, rendered from openapi.json.

PathReturns
/v2/<CODE>/past-hourThe last completed clock hour: the mean of every point in it. Immutable once published.
/v2/<CODE>/current-hourThe hour in progress: the mean of the points so far.
/v2/<CODE>/latestThe newest hour the provider has published, however old. Some feeds run a day or more behind by design; for those this is the only reading there is.
/v2/<CODE>/history/<YYYY-MM-DD>One UTC day of hourly means, 365 days retained. A past day never changes again, so it is safe to cache forever.
/v2/<CODE>/yearlyThe annual average. Every country has one.
/v2/<CODE>/<ZONE>/…The same hourly routes for a bidding zone or balancing region — /v2/IT/SICI/past-hour. Measured only; 404 when the provider has nothing, so treat it as ask the country instead.
/v2/countries.jsonEvery country: metadata, zones, realtime availability and annual figures
/v2/past-hour.jsonThe measured countries' last completed hour, in one document

An UPPERCASE segment is a code and a lowercase-hyphenated one is a resource, so /v2/IT/SICI/past-hour reads as country, zone, resource. Only countries with a live grid feed answer on the hourly routes; the rest have /yearly. ISO-3166 alpha-2 only.

When a grid feed goes down, /past-hour and /current-hour 404 on the very next run — no grace period, nothing held back. Both are named for particular clock hours and mean them, so a missing hour is a missing hour. /latest is what carries a reading across the outage, for as long as the outage lasts: it has no age bound and is not rewritten while a feed is quiet, so its generated_at stands still and its age shows. period_start/period_end always say which hour a document describes.

Which countries answer on which route depends on how far behind their grid feed publishes, not on the country, so it moves. /v2/countries.json carries the live answer per country: routes (which of the three answered), data_lag_seconds (how far behind the newest hour reaches) and stale — the ⚠️ warning flag, set when the lag is past the bound the hour-named routes are held to, so only /latest answers and the figures are a recent reading rather than a current one. Two feeds sit there as a rule: EIA (US) publishes about a day behind and Eskom (ZA) several days. Check those fields rather than assuming.

Rate limit: 1 request per 10 seconds per IP, answered with 429 beyond that — and the body of a 429 is not JSON, so check the status code before parsing. From another origin a browser cannot see that status at all: the 429 carries no CORS header, so fetch rejects with a network error — back off on that too. If you want several countries at once, /v2/countries.json and /v2/past-hour.json each return the lot in a single request, which is both faster and kinder than looping over the per-country routes.
And in the spirit of the thing: every request carries a little carbon of its own. Cache what you fetch, and poll no faster than your decisions actually change — the readings only move once an hour.

Supported countries and zones

Data & methodology

Built from raw grid-operator data with lifecycle factors following IPCC AR6, WG III, Annex III and consumption-based accounting in the spirit of the ECON-PowerCI method (Scientific Data 12, 2025). Live readings compute operational intensity from each grid's generation mix; lifecycle and consumption are layered on per country.

Sources & attribution