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
| Parameter | Type | Required | Description |
|---|---|---|---|
lat | number | yes | Latitude in decimal degrees, from -90 to 90 |
lon | number | yes | Longitude in decimal degrees, from -180 to 180 |
from | string | yes | Start date YYYY-MM-DD, inclusive |
to | string | yes | End date YYYY-MM-DD, inclusive; the window spans at most 366 days |
kind | string | no | Comma-separated subset of hdd, cdd, gdd; all three by default |
base | number | no | Base 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, typicallyera5-copernicus.freshness.kind:snapshotfor ERA5 reanalysis;as_ofis the store production date that backed the answer.- ERA5 values are derived from Copernicus Climate Change Service information.
Fields of data
| Field | Type | Description |
|---|---|---|
lat | number | Latitude exactly echoed from the request |
lon | number | Longitude exactly echoed from the request |
from | string | Start date echoed in YYYY-MM-DD form |
to | string | End date echoed in YYYY-MM-DD form |
base | number | Base temperature in Celsius actually applied (echoed or defaulted) |
days_counted | number | Number of covered days that contributed to the sums |
hdd | number | Heating degree days total; present only when hdd was requested |
cdd | number | Cooling degree days total; present only when cdd was requested |
gdd | number | Growing degree days total; present only when gdd was requested |
method | string | Always mean_temperature - the computation method (see below) |
coverage | object | Completeness 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
| Field | Type | Description |
|---|---|---|
complete | bool | true when every day in the window had a usable grid value |
reason | string | Present 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.
| Status | code | Case |
|---|---|---|
| 400 | INVALID_COORDS | lat/lon missing, non-numeric or outside valid bounds |
| 400 | INVALID_PERIOD | from/to missing or malformed, to before from, or window over 366 days |
| 400 | INVALID_BASE | base is non-numeric or outside the supported range |
| 400 | INVALID_VARIABLE | kind contains a value outside hdd, cdd, gdd |
| 400 | OUT_OF_RANGE | The window is well formed but no day in it is covered by the store |
| 500 | INTERNAL | Internal 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
/catalogand how settlement works.