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
| Parameter | Type | Required | Description |
|---|---|---|---|
lat | number | coordinate mode | Latitude in decimal degrees, from -90 to 90 |
lon | number | coordinate mode | Longitude in decimal degrees, from -180 to 180 |
city | string | place-name mode | Place name instead of coordinates, for example city=Zurich |
location | string | no | Strict alias of city; if both are present they must be identical |
country | string | no | ISO-3166 alpha-2 code narrowing an ambiguous place name, for example country=US |
year | number | yes | Target year to place in the distribution, from 1940 to 2100 |
month | number | season | Whole calendar month 1..12 as the season; or use from_md/to_md |
from_md | string | season | Season start as MM-DD; requires to_md, on or before it (no year wrap) |
to_md | string | season | Season end as MM-DD; requires from_md |
variable | string | no | temperature or precipitation; defaults to temperature |
stat | string | no | Seasonal 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, 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 in effect for the answer: echoed, or resolved from the place name |
lon | number | Longitude in effect for the answer: echoed, or resolved from the place name |
location | object | Resolved place echo (name, country, admin1, lat, lon, source); present only when the request used city/location - see GET /climate/point |
season | string | Resolved season label, for example 07-01..07-31 |
year | number | Target year echoed from the request |
variable | string | temperature or precipitation |
stat | string | Seasonal reduction applied: mean, sum or max |
unit | string | Unit of value: celsius or millimetre |
value | number | null | The target year’s seasonal value; null when the year is not covered |
percentile | number | null | Empirical percentile (rank / sample_years, in %); null when uncovered |
rank | number | null | Empirical rank of the value within the sample; null when uncovered |
sample_years | number | Number of historical years in the sample - always present |
return_period_years | number | null | Empirical return period in years; null when uncovered |
method | string | Always empirical - see below |
grid | object | Representative grid cell used for the answer; omitted if no cell served |
coverage | object | Completeness marker for the sample and the target year |
coverage
| Field | Type | Description |
|---|---|---|
complete | bool | true when the target year is covered, the sample is large enough and no year was excluded for partial coverage |
reason | string | Present 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_yearsis 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.completeisfalsewith a reason such asinsufficient history: 14 years. Likewise, if the target year is not covered by the record,value,percentile,rankandreturn_period_yearsarenullwhilesample_yearsandmethodremain 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: 29withcoverage.complete: falseand a reason such as1 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.
| Status | code | Case |
|---|---|---|
| 400 | INVALID_COORDS | lat/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) |
| 400 | UNKNOWN_LOCATION | city/location names a place the gazetteer cannot resolve (or place-name resolution is unavailable on the deployment) |
| 400 | INVALID_PERIOD | Missing/invalid year (or out of 1940..2100), or an invalid/mixed/wrapping season window |
| 400 | INVALID_VARIABLE | Unknown variable, unknown stat, or a stat inconsistent with the variable |
| 400 | OUT_OF_RANGE | The request is well formed but the point has no historical sample at all |
| 500 | INTERNAL | Internal 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
GET /climate/anomaly- observed value versus the 1991-2020 climate normal.GET /climate/point- single-date climate variables at a GPS point.- For agents - discovery surfaces, the live
/catalogand how settlement works.