GET /climate/degree-days

Returns heating (HDD), cooling (CDD) and growing (GDD) degree days for one GPS point over a date range, summed from ERA5 reanalysis daily mean temperatures decoded from GRIB into a local grid store. Pick any window within the ingested ERA5 range (currently a rolling ~5-year window) - a month, a season or several years - and get the totals in a single call: the kind of figure a human otherwise assembles with hours of scripting over raw climate files. The lookup is local, so a covered window resolves in milliseconds without an account or API key.

This is the demand-and-growth signal: energy teams use HDD/CDD for demand forecasting and weather hedging; agriculture uses GDD for phenology and crop staging. See the live /catalog for the authoritative endpoint listing and price.

x402 golden rule: the agent pays for the answer to its question. A well-formed request over a covered window is a successful answer -> 200, even when some days inside the window have a grid gap: those days are excluded from the sum and reported through coverage and days_counted. Requests the service cannot answer - invalid coordinates, an invalid or empty period, an out-of-bounds base, or a window with no covered day at all - leave the 200 range.

Parameters

ParameterTypeRequiredDescription
latnumberyesLatitude in decimal degrees, from -90 to 90
lonnumberyesLongitude in decimal degrees, from -180 to 180
fromstringyesStart date YYYY-MM-DD, inclusive
tostringyesEnd date YYYY-MM-DD, inclusive; the window spans at most 366 days
kindstringnoComma-separated subset of hdd, cdd, gdd; all three by default
basenumbernoBase temperature in Celsius; defaults to 18 for HDD/CDD, 10 for a GDD-only request

The window [from, to] is the unit of the product and is capped at 366 days per call. base defaults depend on the requested kinds: an energy base of 18 C when any of HDD/CDD is requested, a growth base of 10 C for a GDD-only request. Supply base explicitly to override it; it must lie within a sane range.

GET /climate/degree-days?lat=48.8566&lon=2.3522&from=2024-01-01&to=2024-12-31&kind=hdd

Request a single family, or several, and set an explicit base:

GET /climate/degree-days?lat=48.8566&lon=2.3522&from=2024-04-01&to=2024-09-30&kind=gdd&base=10

200 response - UnifiedResponse

{
  "data": { ... },
  "provenance": {
    "source": "era5-copernicus",
    "fetched_at": "2026-06-20T12:00:00Z",
    "freshness": { "kind": "snapshot", "as_of": "2026-06-19T00:00:00Z" }
  }
}
  • provenance.source: stable identifier of the served store source, typically era5-copernicus.
  • freshness.kind: snapshot for ERA5 reanalysis; as_of is the store production date that backed the answer.
  • ERA5 values are derived from Copernicus Climate Change Service information.

Fields of data

FieldTypeDescription
latnumberLatitude exactly echoed from the request
lonnumberLongitude exactly echoed from the request
fromstringStart date echoed in YYYY-MM-DD form
tostringEnd date echoed in YYYY-MM-DD form
basenumberBase temperature in Celsius actually applied (echoed or defaulted)
days_countednumberNumber of covered days that contributed to the sums
hddnumberHeating degree days total; present only when hdd was requested
cddnumberCooling degree days total; present only when cdd was requested
gddnumberGrowing degree days total; present only when gdd was requested
methodstringAlways mean_temperature - the computation method (see below)
coverageobjectCompleteness marker for the requested window

Each requested family is summed over the covered days in [from, to]. A family that was not requested via kind is omitted from data.

coverage

FieldTypeDescription
completebooltrue when every day in the window had a usable grid value
reasonstringPresent when complete: false, explaining which days were uncovered

days_counted reflects only the days that contributed to the totals; when some days inside the window have a grid gap they are dropped from the sum and coverage.complete is false.

Method honesty

Degree days here use the mean-temperature method: for each day, HDD = max(0, base - Tmean), CDD = max(0, Tmean - base) and GDD = max(0, Tmean - base), summed over the window. This is the standard NWS / EIA / WMO convention and is reported as method: "mean_temperature".

Two honesty notes:

  • GDD is not capped. This is the plain mean-temperature GDD with no Tmin/Tmax clamping or upper cutoff; some agronomic models cap daily contributions, and this endpoint deliberately does not.
  • Grid, not station. ERA5 is a gridded reanalysis with a typical global mesh of about 0.25 degrees, not a weather-station reading. Daily means are aggregate store values, and uncovered days are excluded from the sum rather than interpolated.

The historical window is finite and moves with the ingested store (provenance.freshness.as_of tells you which dump backed the answer). A window with no covered day at all returns 400 OUT_OF_RANGE; a window with some covered days returns 200 with coverage.complete: false.

Example - heating degree days over a year

{
  "data": {
    "lat": 48.8566,
    "lon": 2.3522,
    "from": "2024-01-01",
    "to": "2024-12-31",
    "base": 18,
    "days_counted": 366,
    "hdd": 2412.7,
    "method": "mean_temperature",
    "coverage": { "complete": true }
  },
  "provenance": {
    "source": "era5-copernicus",
    "fetched_at": "2026-06-20T12:00:00Z",
    "freshness": { "kind": "snapshot", "as_of": "2026-06-19T00:00:00Z" }
  }
}

Example - all three families with a partial-coverage window

When some days inside the window have a grid gap, they are dropped from the sums. The response is still a 200: days_counted reflects the covered days and coverage explains the gap.

{
  "data": {
    "lat": 48.8566,
    "lon": 2.3522,
    "from": "2024-06-01",
    "to": "2024-06-30",
    "base": 18,
    "days_counted": 28,
    "hdd": 12.4,
    "cdd": 41.9,
    "gdd": 233.6,
    "method": "mean_temperature",
    "coverage": {
      "complete": false,
      "reason": "2 days in the window have no grid value and were excluded"
    }
  },
  "provenance": {
    "source": "era5-copernicus",
    "fetched_at": "2026-06-20T12:00:00Z",
    "freshness": { "kind": "snapshot", "as_of": "2026-06-19T00:00:00Z" }
  }
}

Errors

Only requests the service cannot answer leave the 200 range.

StatuscodeCase
400INVALID_COORDSlat/lon missing, non-numeric or outside valid bounds
400INVALID_PERIODfrom/to missing or malformed, to before from, or window over 366 days
400INVALID_BASEbase is non-numeric or outside the supported range
400INVALID_VARIABLEkind contains a value outside hdd, cdd, gdd
400OUT_OF_RANGEThe window is well formed but no day in it is covered by the store
500INTERNALInternal error (detail logged, not exposed)
{ "error": "invalid from '01-01-2024', expected YYYY-MM-DD", "code": "INVALID_PERIOD" }
{ "error": "unknown kind 'xdd' (expected: hdd, cdd, gdd)", "code": "INVALID_VARIABLE" }
{ "error": "requested window has no covered day within 2021-01-01..2026-06-20", "code": "OUT_OF_RANGE" }

See also

  • GET /climate/point - single-date climate variables (temperature, precipitation, wind) at a GPS point.
  • GET /climate/aggregate - monthly and seasonal aggregates over a period at a GPS point.
  • For agents - discovery surfaces, the live /catalog and how settlement works.