GET /climate/anomaly

Returns the climate anomaly for one GPS point over a date range: the departure of the observed ERA5 value from the 1991-2020 WMO climate normal, for temperature, precipitation and wind. For each family you get the observed value, the normal and their difference, so an agent can answer “this month was X degrees above the normal here” in a single call. The lookup is local, so a covered window resolves in milliseconds without an account or API key.

This is the departure signal: parametric insurance and ESG / climate reporting need the deviation from a stable baseline, not just a raw reading. The baseline is the standard 30-year WMO normal, and it is the only reference the endpoint uses. 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 where the observed value and the normal are both available is a successful answer -> 200, even when one family is null because its observation or its normal is missing: that is reported through coverage. Requests the service cannot answer - invalid coordinates, an invalid period, an unknown variable, or a window where nothing can be paired - 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.

GET /climate/anomaly?lat=48.8566&lon=2.3522&from=2024-07-01&to=2024-07-31
GET /climate/anomaly?city=Zurich&from=2024-07-01&to=2024-07-31
GET /climate/anomaly?lat=48.8566&lon=2.3522&from=2024-07-01&to=2024-07-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
reference_periodstringWMO normal baseline used, for example 1991-2020
days_countednumberDays paired (observed and normal) for at least one requested family
temperatureobject | nullTemperature anomaly; omitted when not requested, null when unpaired
precipitationobject | nullPrecipitation anomaly; omitted when not requested, null when unpaired
windobject | nullWind anomaly; omitted when not requested, null when unpaired
gridobjectRepresentative grid cell used for the answer; omitted if no cell served
coverageobjectCompleteness marker for the requested window and families

A family that was not requested via variables is omitted. A family that was requested but cannot be paired - no observation, or no normal - is serialized as null and explained through coverage.

temperature

FieldTypeDescription
observed_celsiusnumberMean observed daily temperature over the paired days
normal_celsiusnumberMean 1991-2020 normal over the same days
anomaly_celsiusnumberObserved minus normal, in Celsius

precipitation

FieldTypeDescription
observed_mmnumberObserved precipitation total over the paired days
normal_mmnumberSum of the daily normals over the same days
anomaly_mmnumberObserved minus normal, in millimetres

wind

FieldTypeDescription
observed_speed_msnumberMean observed 10 m wind speed over the paired days
normal_speed_msnumberMean normal wind speed over the same days
anomaly_speed_msnumberObserved minus normal, in metres per second

Wind speeds are magnitudes of the daily-mean wind vector (lower than the mean wind speed when direction varies during the day); observed and normal are compared on the same basis, so the anomaly is consistent.

coverage

FieldTypeDescription
completebooltrue when every requested family was paired across the window
reasonstringPresent when complete: false, distinguishing an observation gap from a climatological normal not available

Baseline honesty

The reference is always the 1991-2020 WMO climate normal (a 30-year baseline), made explicit in reference_period. The endpoint never invents an anomaly against a short window dressed up as a normal: a value is only an anomaly when it is measured against this 30-year reference.

  • The normal is a monthly climatology. Each observed day is paired with the normal for its month; over a multi-month window the normals are therefore weighted by the number of days in each month. Temperature and wind are means of the paired observed and normal values; precipitation is the observed total versus the sum of the daily normals.
  • A day counts only if it carries both an observation and a normal. A family with no observation returns null with an observation gap reason; a family with an observation but no normal returns null with a climatological normal not available reason.
  • The observed side has a finite window that differs by family: temperature and precipitation observations cover a rolling 30-year window (the WMO-standard climatological period), wind a rolling ~5-year window - a deep window can therefore pair temperature and precipitation while wind comes back null with an observation gap reason.
  • ERA5 is a gridded reanalysis with a typical global mesh of about 0.25 degrees, not a weather-station reading. A window where nothing can be paired - no observed day, or no normal for any family - returns 400 OUT_OF_RANGE.

Example - a warm month

{
  "data": {
    "lat": 48.8566,
    "lon": 2.3522,
    "from": "2024-07-01",
    "to": "2024-07-31",
    "reference_period": "1991-2020",
    "days_counted": 31,
    "temperature": {
      "observed_celsius": 22.8,
      "normal_celsius": 20.1,
      "anomaly_celsius": 2.7
    },
    "precipitation": {
      "observed_mm": 18.4,
      "normal_mm": 52.6,
      "anomaly_mm": -34.2
    },
    "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 - a family with no normal available

When a requested family has an observation but no climate normal at the grid cell, the anomaly cannot be formed: the family is null and coverage says why. The request still succeeds with 200 as long as another family was paired.

{
  "data": {
    "lat": 48.8566,
    "lon": 2.3522,
    "from": "2024-07-01",
    "to": "2024-07-31",
    "reference_period": "1991-2020",
    "days_counted": 31,
    "temperature": {
      "observed_celsius": 22.8,
      "normal_celsius": 20.1,
      "anomaly_celsius": 2.7
    },
    "wind": null,
    "grid": { "lat": 48.75, "lon": 2.25, "distance_km": 13.42 },
    "coverage": {
      "complete": false,
      "reason": "wind: climatological normal not available"
    }
  },
  "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 nothing can be paired (no observed day, or no normal for any family)
500INTERNALInternal error (detail logged, not exposed)
{ "error": "invalid from '01-07-2024', expected YYYY-MM-DD", "code": "INVALID_PERIOD" }
{ "error": "requested window has no observed day or no normal within the store", "code": "OUT_OF_RANGE" }

See also