GET /climate/aggregate

Returns period aggregates for one GPS point over a date range: mean temperature with its daily extremes, total precipitation and mean wind, computed from ERA5 reanalysis daily values decoded from GRIB into a local grid store. Ask for a month or a season and get the summary in a single call, instead of assembling 30 to 90 individual point lookups yourself. The lookup is local, so a covered window resolves in milliseconds without an account or API key.

This is the period-summary signal: the climate balance of a month or a season at a point, for analytics and energy operations. 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 or a requested family has no data: those cases are reported through days_counted, a null family and coverage. Requests the service cannot answer - invalid coordinates, an invalid period, an unknown variable, or a window with no covered day at all - leave the 200 range.

Parameters

ParameterTypeRequiredDescription
latnumbercoordinate modeLatitude in decimal degrees, from -90 to 90
lonnumbercoordinate modeLongitude in decimal degrees, from -180 to 180
citystringplace-name modePlace name instead of coordinates, for example city=Zurich
locationstringnoStrict alias of city; if both are present they must be identical
countrystringnoISO-3166 alpha-2 code narrowing an ambiguous place name, for example country=US
fromstringyesStart date YYYY-MM-DD, inclusive
tostringyesEnd date YYYY-MM-DD, inclusive; the window spans at most 366 days
variablesstringnoComma-separated subset of temperature, precipitation, wind; all by default

Locate the point either with lat/lon coordinates or with a place name via city (alias location) - never both. Place names resolve against an embedded GeoNames cities500 gazetteer (exact match, case- and accent-insensitive, no fuzzy matching; ambiguity resolved by population, narrowed by country=), and the resolved place is echoed back in data.location - see GET /climate/point for the full contract.

The window [from, to] is the unit of the product and is capped at 366 days per call. Use variables to reduce the payload when the agent only needs one family.

GET /climate/aggregate?lat=48.8566&lon=2.3522&from=2024-06-01&to=2024-08-31
GET /climate/aggregate?city=Zurich&from=2024-06-01&to=2024-08-31
GET /climate/aggregate?lat=48.8566&lon=2.3522&from=2024-06-01&to=2024-08-31&variables=temperature

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 in effect for the answer: echoed, or resolved from the place name
lonnumberLongitude in effect for the answer: echoed, or resolved from the place name
locationobjectResolved place echo (name, country, admin1, lat, lon, source); present only when the request used city/location - see GET /climate/point
fromstringStart date echoed in YYYY-MM-DD form
tostringEnd date echoed in YYYY-MM-DD form
days_countednumberDays covered by at least one requested family
temperatureobject | nullTemperature aggregate; omitted when not requested, null when no data
precipitationobject | nullPrecipitation aggregate; omitted when not requested, null when no data
windobject | nullWind aggregate; omitted when not requested, null when no data
gridobjectRepresentative grid cell used for the answer; omitted if no cell served
coverageobjectCompleteness marker for the requested window

A family that was not requested via variables is omitted. A family that was requested but has no value over the window is serialized as null - the same honesty convention as GET /climate/point.

temperature

FieldTypeDescription
mean_celsiusnumberMean of the daily mean temperatures over the covered days
min_celsiusnumberLowest daily-mean temperature over the covered days
max_celsiusnumberHighest daily-mean temperature over the covered days

precipitation

FieldTypeDescription
total_mmnumberSum of daily precipitation over the covered days

wind

FieldTypeDescription
mean_speed_msnumberMean 10 m wind speed in metres per second
mean_direction_degnumberMean meteorological direction in degrees: 0 = north, 90 = east

Wind is the daily-mean wind vector: mean_speed_ms is the magnitude of the mean vector (lower than the mean wind speed when direction varies during the day), and mean_direction_deg is the resultant direction.

grid

FieldTypeDescription
latnumberLatitude of the representative grid cell
lonnumberLongitude of the representative grid cell
distance_kmnumberDistance from the requested point to that cell

coverage

FieldTypeDescription
completebooltrue when every day in the window had a usable value for each family
reasonstringPresent when complete: false, explaining the gap

Aggregation honesty

Each family is aggregated with the convention that fits it, and these conventions matter:

  • Precipitation is a total (sum), not a mean: total_mm adds the daily totals over the covered days.
  • Temperature and wind are means over the covered days.
  • min_celsius / max_celsius are extremes of the daily-mean values, not hourly Tmin/Tmax. They tell you the coldest and warmest days in the window, not the lowest and highest instantaneous readings. For true daily Tmin/Tmax extremes, use GET /climate/indices.
  • ERA5 is a gridded reanalysis with a typical global mesh of about 0.25 degrees, not a weather-station reading.

A window that is only partly covered is reported honestly: uncovered days are dropped, days_counted reflects the covered days and a sum like total_mm is the total over those days only, with coverage.complete: false. The historical window is finite, moves with the ingested store (provenance.freshness.as_of names the dump), and differs by family: temperature and precipitation cover a rolling 30-year window (the WMO-standard climatological period), wind a rolling ~5-year window - so a deep window is answerable for temperature and precipitation while wind over the same window is served null with an honest coverage reason. A window with no covered day at all returns 400 OUT_OF_RANGE.

Example - summer aggregate, all families

{
  "data": {
    "lat": 48.8566,
    "lon": 2.3522,
    "from": "2024-06-01",
    "to": "2024-08-31",
    "days_counted": 92,
    "temperature": { "mean_celsius": 21.3, "min_celsius": 13.8, "max_celsius": 29.6 },
    "precipitation": { "total_mm": 148.7 },
    "wind": { "mean_speed_ms": 3.4, "mean_direction_deg": 241.5 },
    "grid": { "lat": 48.75, "lon": 2.25, "distance_km": 13.42 },
    "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 - partial window, one family with no data

A window covered only in part still returns 200: days_counted reflects the covered days, the precipitation sum is the total over those days, and a requested family with no value comes back null with coverage.complete: false.

{
  "data": {
    "lat": 48.8566,
    "lon": 2.3522,
    "from": "2024-06-01",
    "to": "2024-06-30",
    "days_counted": 28,
    "temperature": { "mean_celsius": 19.7, "min_celsius": 12.1, "max_celsius": 27.0 },
    "precipitation": { "total_mm": 41.2 },
    "wind": null,
    "grid": { "lat": 48.75, "lon": 2.25, "distance_km": 13.42 },
    "coverage": {
      "complete": false,
      "reason": "no wind data at this grid cell; 2 days in the window have no value"
    }
  },
  "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 non-numeric or outside valid bounds, or a point-mode conflict (coordinates and a place name together, city differing from location, or no point at all)
400UNKNOWN_LOCATIONcity/location names a place the gazetteer cannot resolve (or place-name resolution is unavailable on the deployment)
400INVALID_PERIODfrom/to missing or malformed, to before from, or window over 366 days
400INVALID_VARIABLEvariables contains a family outside temperature, precipitation, wind
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-06-2024', expected YYYY-MM-DD", "code": "INVALID_PERIOD" }
{ "error": "requested window has no covered day within 1996-06-15..2026-06-14", "code": "OUT_OF_RANGE" }

See also