GET /climate/return-period

Places a target year’s seasonal climate value in the historical distribution at one GPS point: how a chosen season (a month, or a custom month-day window) of a given year ranks against the same season across the ingested ERA5 record - a rolling 30-year window for temperature and precipitation, the WMO-standard climatological period, so a covered season is ranked against a full 30-year sample and the verdict carries coverage.complete: true. You get the season value, its empirical percentile and rank, the corresponding return period in years, and - always - the size of the sample it was ranked against. The lookup is local, so a covered point resolves in milliseconds without an account or API key.

This is the rarest and most defensible climate derivative: reinsurance, climate risk and climate finance need to answer “is this July in the top 5% over the record?” - a ranking, not a raw reading. 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 with a usable historical sample is a successful answer -> 200, even when the sample is short or the target year is not covered: those cases are served with null ranking fields and an honest coverage. Requests the service cannot answer - invalid coordinates, an invalid season or year, an unknown variable or statistic, or a point with no historical sample 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
yearnumberyesTarget year to place in the distribution, from 1940 to 2100
monthnumberseasonWhole calendar month 1..12 as the season; or use from_md/to_md
from_mdstringseasonSeason start as MM-DD; requires to_md, on or before it (no year wrap)
to_mdstringseasonSeason end as MM-DD; requires from_md
variablestringnotemperature or precipitation; defaults to temperature
statstringnoSeasonal reduction: mean, sum or max; defaults per variable

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.

Define the season either with a whole month or with both from_md and to_md - never a mix, and the custom window must not wrap across the year boundary. The statistic must be physically consistent with the variable: temperature accepts mean (default) or max; precipitation accepts sum (default) or max.

GET /climate/return-period?lat=48.8566&lon=2.3522&month=7&year=2024&variable=temperature&stat=mean
GET /climate/return-period?lat=48.8566&lon=2.3522&from_md=06-01&to_md=08-31&year=2024&variable=precipitation&stat=sum

Or locate the point by city name:

GET /climate/return-period?city=Zurich&year=2024&month=7

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
seasonstringResolved season label, for example 07-01..07-31
yearnumberTarget year echoed from the request
variablestringtemperature or precipitation
statstringSeasonal reduction applied: mean, sum or max
unitstringUnit of value: celsius or millimetre
valuenumber | nullThe target year’s seasonal value; null when the year is not covered
percentilenumber | nullEmpirical percentile (rank / sample_years, in %); null when uncovered
ranknumber | nullEmpirical rank of the value within the sample; null when uncovered
sample_yearsnumberNumber of historical years in the sample - always present
return_period_yearsnumber | nullEmpirical return period in years; null when uncovered
methodstringAlways empirical - see below
gridobjectRepresentative grid cell used for the answer; omitted if no cell served
coverageobjectCompleteness marker for the sample and the target year

coverage

FieldTypeDescription
completebooltrue when the target year is covered, the sample is large enough and no year was excluded for partial coverage
reasonstringPresent when complete: false: target year not covered, insufficient history: N years, or partial seasonal years excluded

Method honesty

The ranking is empirical: the target year’s value is placed by its rank in the observed sample, and method is always empirical. There is no GEV or other parametric fit and no confidence interval in this version - the percentile and return period are exactly the rank-based figures, nothing extrapolated.

Three honesty guarantees:

  • sample_years is always returned. A percentile over 12 years and one over 60 years are not the same claim; the sample size is exposed so the agent can judge it.
  • A short sample is not hidden. Below a floor of 20 sample years, the figures are still served but coverage.complete is false with a reason such as insufficient history: 14 years. Likewise, if the target year is not covered by the record, value, percentile, rank and return_period_years are null while sample_years and method remain present.
  • A partially covered year is excluded, and says so. A season that touches the edge of the rolling 30-year window can lose its earliest year: a partial seasonal aggregate would distort the rank, so that year is dropped from the distribution and the answer reports sample_years: 29 with coverage.complete: false and a reason such as 1 year(s) with partial seasonal coverage were excluded.

ERA5 is a gridded reanalysis with a typical global mesh of about 0.25 degrees, not a weather-station reading. A point with no historical sample at all returns 400 OUT_OF_RANGE.

Example - a hot July ranked over the record

{
  "data": {
    "lat": 48.8566,
    "lon": 2.3522,
    "season": "07-01..07-31",
    "year": 2024,
    "variable": "temperature",
    "stat": "mean",
    "unit": "celsius",
    "value": 23.4,
    "percentile": 96.7,
    "rank": 29,
    "sample_years": 30,
    "return_period_years": 15.5,
    "method": "empirical",
    "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 - season at the edge of the rolling window

The 30-year window rolls forward with the store, so a season overlapping its earliest edge can lose one year to partial coverage: that year is excluded rather than ranked on a distorted aggregate, and coverage says so. The figures are still returned - a 29-year sample is a strong claim, just not the full window - and the agent decides whether it is strong enough to act on.

{
  "data": {
    "lat": 12.3456,
    "lon": 98.7654,
    "season": "06-01..08-31",
    "year": 2024,
    "variable": "precipitation",
    "stat": "sum",
    "unit": "millimetre",
    "value": 612.8,
    "percentile": 93.1,
    "rank": 27,
    "sample_years": 29,
    "return_period_years": 10.0,
    "method": "empirical",
    "grid": { "lat": 12.25, "lon": 98.75, "distance_km": 11.07 },
    "coverage": {
      "complete": false,
      "reason": "1 year(s) with partial seasonal coverage 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 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_PERIODMissing/invalid year (or out of 1940..2100), or an invalid/mixed/wrapping season window
400INVALID_VARIABLEUnknown variable, unknown stat, or a stat inconsistent with the variable
400OUT_OF_RANGEThe request is well formed but the point has no historical sample at all
500INTERNALInternal error (detail logged, not exposed)
{ "error": "year 1899 out of range [1940, 2100]", "code": "INVALID_PERIOD" }
{ "error": "provide either 'month' or both 'from_md' and 'to_md', not a mix", "code": "INVALID_PERIOD" }
{ "error": "stat is not valid for the requested variable (temperature accepts mean/max, precipitation accepts sum/max)", "code": "INVALID_VARIABLE" }

See also