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 - currently a rolling ~5-year window, so today every verdict carries a short sample flagged via coverage until deeper history is ingested. 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
latnumberyesLatitude in decimal degrees, from -90 to 90
lonnumberyesLongitude in decimal degrees, from -180 to 180
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

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

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 exactly echoed from the request
lonnumberLongitude exactly echoed from the request
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 and the sample is large enough
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.

Two 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.

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": 97.1,
    "rank": 34,
    "sample_years": 35,
    "return_period_years": 35.0,
    "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 - insufficient history

When fewer than 20 years are available, the figures are still returned, but the sample size is exposed and coverage flags the limitation. The agent decides whether the ranking is strong enough to act on.

{
  "data": {
    "lat": 12.3456,
    "lon": 98.7654,
    "season": "07-01..07-31",
    "year": 2024,
    "variable": "precipitation",
    "stat": "sum",
    "unit": "millimetre",
    "value": 612.8,
    "percentile": 92.9,
    "rank": 13,
    "sample_years": 14,
    "return_period_years": 14.0,
    "method": "empirical",
    "grid": { "lat": 12.25, "lon": 98.75, "distance_km": 11.07 },
    "coverage": {
      "complete": false,
      "reason": "insufficient history: 14 years"
    }
  },
  "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 missing, non-numeric or outside valid bounds
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