GET /weather/forecast
Returns forecast weather variables for one point and one forecast target -
the point given as GPS coordinates or as a city name (city=Zurich), the
target as a lead time, an instant or a calendar date (date=2026-07-05):
2 m temperature, precipitation and 10 m wind, served from NOAA data
decoded from GRIB into a local grid store. The runtime lookup is local, so a
covered target resolves in milliseconds without an account or API key.
The horizon runs up to 35 days ahead in two honestly distinguished
registers: the deterministic GFS model within about 16 days, then the
GEFS ensemble mean beyond it. provenance.source and data.grid name the
model and mesh actually served for each answer, and provenance.dataset gives
the machine code (gfs or gefs) to branch on, so an agent never mistakes a
statistical smoothing for a deterministic run. The ensemble segment smooths
extremes by construction: it is a trend signal, never a threshold trigger. See
Two forecast registers below.
This is the operational counterpart to
GET /climate/point: use it when an agent needs a
future weather signal for energy dispatch, logistics routing, agricultural
planning or short-term risk checks. 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 and covered request is a successful answer -> 200, even when a
requested variable is null because the grid cell has a data gap. Requests the
service cannot answer - invalid coordinates, invalid target time, an uncovered
forecast horizon, an unknown variable or a missing forecast cycle - leave the
200 range.
Two forecast registers
The served window is composite. A single request is answered from whichever register covers the resolved target, and the response always names which one:
| Register | Horizon from cycle | Model | Mesh (data.grid) | provenance.source |
|---|---|---|---|---|
| Deterministic | up to 384 h (about 16 days) | NOAA GFS | ~0.25 degrees | gfs-noaa / GFS |
| Ensemble mean | 384 h to 840 h (about 35 days) | NOAA GEFS | ~0.5 degrees | NOAA GEFS ensemble mean (35-day, 0.5°) |
The extended register is the average of the 31-member GEFS ensemble, not a
single deterministic scenario: it is a statistical smoothing of the plausible
outcomes, so day-to-day variability is damped and the effective mesh is coarser
(0.5 degrees). The provenance.source string makes this explicit on every
extended answer, provenance.dataset carries the machine code (gfs or gefs)
to branch on, and data.grid reports the coarser cell that was used. What that
smoothing does to the numbers is spelled out in
The ensemble tail smooths extremes.
The deterministic register comes from the current GFS cycle; the ensemble
register comes from the latest GEFS 00Z cycle, which can be from the day
before. Because the two cycles differ, the real composite window depends on
their alignment — the ends are not a fixed clock offset. A request outside every
covered segment is a 400 OUT_OF_RANGE whose message quotes the exact
windows actually covered at the time of the call (see Errors).
The ensemble tail smooths extremes
Past the end of the deterministic window (around 384 h — a boundary carried by the data, not by a fixed constant: the service serves whichever dataset covers the target), every base variable is the average of ~31 ensemble members. Averaging removes exactly what a threshold rule looks for: an extreme that only part of the members produce survives the mean as a fraction of itself. This is not a defect, it is what an ensemble mean is — but it changes what the value can be used for.
Measured in production on 2026-07-25 for Paris, on the same store and the
same call (/weather/forecast/daily?lat=48.85&lon=2.35&days=35):
| Segment | Register | Daily precipitation | Daily max temperature |
|---|---|---|---|
| J+0 → J+15 (16 days) | deterministic GFS | 9 days at 0.0 mm, peak 13.12 mm | 21.2 → 40.5 °C, a 19.3 °C spread |
| J+16 → J+34 (19 days) | GEFS ensemble mean | no day below 0.40 mm, none above 1.82 mm | 25.3 → 27.9 °C, a 2.6 °C spread |
The consequence, stated plainly: a threshold trigger placed on the ensemble
segment — “more than 30 mm in a day”, “a dry day”, a wind cut-off — will
essentially never fire, whatever weather actually occurs. Read that segment
as a trend (is the period leaning warm, wet, windy?), and keep parametric or
threshold decisions inside the deterministic segment, which is the one that
resolves extremes. Neither the 35-day window nor those values are wrong: they
answer a different question. For a threshold inside the ensemble tail, count
the members instead of averaging them with
GET /weather/forecast/exceedance,
which serves the share of members crossing your threshold on each day.
Branch on provenance.dataset (gfs or gefs) — on
GET /weather/forecast/daily
each row carries its own days[].dataset — rather than parsing the
human-readable provenance.source label.
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 |
horizon_h | number | one of target fields | Forecast lead in hours from the current GFS cycle, integer 0 to 840 (35 days) |
valid_time | string | one of target fields | Forecast target instant in RFC 3339 UTC form, for example 2026-06-20T12:00:00Z |
date | string | one of target fields | Forecast day as YYYY-MM-DD, served at 12:00 UTC of that day |
variables | string | no | Comma-separated family selector; defines the exact set served (omit for the default temperature, precipitation, wind). Opt-in families convective, humidity, clouds, snow come from the deterministic GFS only (≤ 384 h) — see Optional variable families |
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: the match is exact but case- and
accent-insensitive (no fuzzy matching), an ambiguous name resolves to the most
populous candidate, and country= narrows it. The resolved place is echoed
back in data.location, so the agent sees exactly which city answered.
Provide exactly one of horizon_h, valid_time or date. date is a
calendar-day shortcut: date=2026-07-05 is served at
valid_time=2026-07-05T12:00:00Z - noon UTC, not local noon, and a single
instant, not a date range or a daily summary. The lead block in the response
states the instant actually served.
GET /weather/forecast?lat=48.8566&lon=2.3522&horizon_h=24
GET /weather/forecast?lat=48.8566&lon=2.3522&valid_time=2026-06-20T12:00:00Z
GET /weather/forecast?city=Zurich&date=2026-07-05
Use variables to reduce the payload when the agent only needs one family:
GET /weather/forecast?lat=48.8566&lon=2.3522&horizon_h=24&variables=temperature,wind
200 response - UnifiedResponse
{
"data": { ... },
"provenance": {
"source": "gfs-noaa",
"dataset": "gfs",
"fetched_at": "2026-06-20T12:00:00Z",
"freshness": { "kind": "live" }
}
}
provenance.source: names the model actually served for this answer -gfs-noaa/GFSin the deterministic register, orNOAA GEFS ensemble mean (35-day, 0.5°)in the extended ensemble register.provenance.dataset: the machine code of that same model,gfsorgefs- the field to branch on, sincesourceis a human label.gefsmeans the smoothed ensemble mean, read as a trend.freshness.kind:livewhen the cycle is fresh, orcachedwithage_secswhen a stale but still covered cycle is served.- GFS and GEFS data both come from NOAA and are public-domain U.S. government data.
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; present only when the request used city/location |
horizon_h | number | Echoed when the request used horizon_h; omitted otherwise |
valid_time | string | Echoed when the request used valid_time; omitted otherwise |
date | string | Echoed when the request used date; omitted otherwise |
temperature | object | Requested 2 m temperature; omitted when not requested |
precipitation | object | Requested precipitation; omitted when not requested |
wind | object | Requested 10 m wind; omitted when not requested |
convective | object | Opt-in convective instability (CAPE/CIN + convective_risk); present only when variables requested convective |
humidity | object | Opt-in 2 m relative humidity; present only when variables requested humidity |
clouds | object | Opt-in total cloud cover; present only when variables requested clouds |
snow | object | Opt-in snow depth on the ground and snowfall flag; present only when variables requested snow |
grid | object | Effective grid cell used for the answer; omitted if no cell served |
lead | object | Resolved cycle, target and served model step |
coverage | object | Completeness marker for the requested variables |
Exactly one of horizon_h, valid_time and date is present, mirroring the
target form the caller used.
location
Present only in place-name mode - when the request located the point with
city/location. Calls made with explicit lat/lon never carry this block,
and their responses are unchanged.
| Field | Type | Description |
|---|---|---|
name | string | Canonical gazetteer name of the resolved place, e.g. Zürich |
country | string | ISO-3166 alpha-2 country code of the resolved place |
admin1 | string | First-level administrative division; can be empty |
lat | number | Latitude the point was resolved to |
lon | number | Longitude the point was resolved to |
source | string | Always GeoNames |
temperature
| Field | Type | Description |
|---|---|---|
celsius | number | null | 2 m temperature in Celsius |
precipitation
| Field | Type | Description |
|---|---|---|
total_mm | number | null | Forecast precipitation total in millimetres |
wind
| Field | Type | Description |
|---|---|---|
speed_ms | number | null | 10 m wind speed in metres per second |
direction_deg | number | null | Meteorological direction in degrees: 0 = north, 90 = east |
Optional variable families
Beyond the three base families, variables can opt into four more —
convective, humidity, clouds and snow — all decoded from the same
deterministic GFS GRIB (public-domain NOAA). They are served in the
deterministic register only (horizon_h ≤ 384): requested beyond it, or
missing at the served grid cell, each comes back as a null block with
coverage.complete: false naming it — never extrapolated. Because variables
defines the exact set returned, the base families appear only if you list them
too; omit variables entirely for the default temperature/precipitation/wind
response, which is unchanged.
GET /weather/forecast?city=Nice&horizon_h=24&variables=convective,humidity,clouds,snow
convective
Surface-based convective instability. convective_risk is an instability
proxy for thunderstorm/hail risk — not a calibrated hail probability: high CAPE
is a necessary ingredient, but hail also needs wind shear, freezing level and
more, which this endpoint does not model. The raw CAPE/CIN are served alongside
so the agent can apply its own thresholds.
| Field | Type | Description |
|---|---|---|
cape_jkg | number | null | Surface Convective Available Potential Energy, J/kg (raw) |
cin_jkg | number | null | Convective Inhibition, J/kg (raw, ≤ 0); a strong cap suppresses convection |
risk | string | null | Qualitative instability from CAPE in J/kg (NWS/SPC conventions): none [0,100), low [100,1000), moderate [1000,2500), high [2500,4000), extreme [4000,∞) |
inhibited | bool | null | true when cin_jkg ≤ -100 (a strong cap likely to suppress convection); reported separately, it never changes risk |
A missing CIN alone does not degrade coverage: risk is still served from CAPE,
with cin_jkg and inhibited null. A missing CAPE degrades the block to all
null and sets coverage.complete: false.
humidity
| Field | Type | Description |
|---|---|---|
relative_pct | number | null | 2 m relative humidity, percent |
clouds
| Field | Type | Description |
|---|---|---|
total_cover_pct | number | null | Total (whole-column) cloud cover, percent (TCDC) |
snow
Two distinct questions: how deep the snowpack on the ground is (a state, SNOD), and whether precipitation at the served step is falling as snow (a categorical flag, CSNOW). A snow-free ground under a snowfall — and the reverse — are both possible.
| Field | Type | Description |
|---|---|---|
depth_m | number | null | Snow depth on the ground in metres (state) |
is_snow | bool | null | Whether precipitation at the served step is falling as snow |
grid
| Field | Type | Description |
|---|---|---|
lat | number | Latitude of the representative grid cell |
lon | number | Longitude of the representative grid cell |
distance_km | number | Distance from the requested point to that cell |
lead
lead makes the resolved forecast target explicit, whether the caller supplied
horizon_h or valid_time.
| Field | Type | Description |
|---|---|---|
cycle | string | Analysis cycle of the served model, RFC 3339 UTC (GFS cycle, or the GEFS 00Z cycle in the ensemble register) |
valid_time | string | Resolved target instant, RFC 3339 UTC |
horizon_h | number | Lead time in hours from cycle to valid_time |
step_time | string | null | Actual model step served; omitted if no step was served |
coverage
coverage tells the agent whether every requested variable was actually
available at the served grid cell.
| Field | Type | Description |
|---|---|---|
complete | bool | true when all requested variables have values |
reason | string | Present when complete: false, naming the missing family data |
Example - horizon target
{
"data": {
"lat": 48.8566,
"lon": 2.3522,
"horizon_h": 24,
"temperature": { "celsius": 18.6 },
"precipitation": { "total_mm": 0.4 },
"wind": { "speed_ms": 6.1, "direction_deg": 242.3 },
"grid": { "lat": 48.75, "lon": 2.25, "distance_km": 13.42 },
"lead": {
"cycle": "2026-06-20T00:00:00Z",
"valid_time": "2026-06-21T00:00:00Z",
"horizon_h": 24,
"step_time": "2026-06-21T00:00:00Z"
},
"coverage": { "complete": true }
},
"provenance": {
"source": "gfs-noaa",
"dataset": "gfs",
"fetched_at": "2026-06-20T01:20:00Z",
"freshness": { "kind": "live" }
}
}
Example - optional variable families
A request opting into convective,humidity,clouds,snow at a deterministic
horizon. Each family is its own block; convective_risk is the qualitative
instability proxy, with raw CAPE/CIN alongside.
GET /weather/forecast?city=Nice&horizon_h=24&variables=convective,humidity,clouds,snow
{
"data": {
"lat": 43.70313,
"lon": 7.26608,
"location": {
"name": "Nice",
"country": "FR",
"admin1": "Provence-Alpes-Côte d'Azur",
"lat": 43.70313,
"lon": 7.26608,
"source": "GeoNames"
},
"horizon_h": 24,
"convective": {
"cape_jkg": 640.0,
"cin_jkg": -18.0,
"risk": "low",
"inhibited": false
},
"humidity": { "relative_pct": 58.0 },
"clouds": { "total_cover_pct": 12.0 },
"snow": { "depth_m": 0.0, "is_snow": false },
"grid": { "lat": 43.75, "lon": 7.25, "distance_km": 5.61 },
"lead": {
"cycle": "2026-07-05T00:00:00Z",
"valid_time": "2026-07-06T00:00:00Z",
"horizon_h": 24,
"step_time": "2026-07-06T00:00:00Z"
},
"coverage": { "complete": true }
},
"provenance": {
"source": "gfs-noaa",
"dataset": "gfs",
"fetched_at": "2026-07-05T04:35:00Z",
"freshness": { "kind": "live" }
}
}
The same families requested beyond 384 h (the ensemble register) come back as
null blocks with coverage.complete: false — the GEFS ensemble mean does not
carry them, and the service never extrapolates.
Example - explicit valid_time
When the request uses valid_time, that field is echoed and horizon_h is
omitted from the top-level data. The resolved lead still carries the computed
horizon.
{
"data": {
"lat": 48.8566,
"lon": 2.3522,
"valid_time": "2026-06-21T00:00:00Z",
"temperature": { "celsius": 18.6 },
"precipitation": { "total_mm": 0.4 },
"wind": { "speed_ms": 6.1, "direction_deg": 242.3 },
"grid": { "lat": 48.75, "lon": 2.25, "distance_km": 13.42 },
"lead": {
"cycle": "2026-06-20T00:00:00Z",
"valid_time": "2026-06-21T00:00:00Z",
"horizon_h": 24,
"step_time": "2026-06-21T00:00:00Z"
},
"coverage": { "complete": true }
},
"provenance": {
"source": "gfs-noaa",
"dataset": "gfs",
"fetched_at": "2026-06-20T01:20:00Z",
"freshness": { "kind": "live" }
}
}
Example - place name and calendar date
A request made with city= and date= answers with the resolved place echoed
in location and the day served at noon UTC: data.date is echoed,
lead.valid_time is 12:00:00Z of that day, and data.lat/data.lon carry
the resolved coordinates.
GET /weather/forecast?city=Zurich&date=2026-06-21&variables=temperature
{
"data": {
"lat": 47.36667,
"lon": 8.55,
"location": {
"name": "Zürich",
"country": "CH",
"admin1": "Zurich",
"lat": 47.36667,
"lon": 8.55,
"source": "GeoNames"
},
"date": "2026-06-21",
"temperature": { "celsius": 20.3 },
"grid": { "lat": 47.25, "lon": 8.5, "distance_km": 13.55 },
"lead": {
"cycle": "2026-06-20T00:00:00Z",
"valid_time": "2026-06-21T12:00:00Z",
"horizon_h": 36,
"step_time": "2026-06-21T12:00:00Z"
},
"coverage": { "complete": true }
},
"provenance": {
"source": "gfs-noaa",
"dataset": "gfs",
"fetched_at": "2026-06-20T01:20:00Z",
"freshness": { "kind": "live" }
}
}
Example - stale but covered cycle
A stale cycle can still answer a covered target. It remains a 200, with
freshness.kind = "cached" and an age in seconds so the agent can decide whether
to trust or retry later.
{
"data": {
"lat": 48.8566,
"lon": 2.3522,
"horizon_h": 3,
"temperature": { "celsius": 17.1 },
"precipitation": { "total_mm": 0.0 },
"wind": { "speed_ms": 4.2, "direction_deg": 190.0 },
"grid": { "lat": 48.75, "lon": 2.25, "distance_km": 13.42 },
"lead": {
"cycle": "2026-06-20T00:00:00Z",
"valid_time": "2026-06-20T03:00:00Z",
"horizon_h": 3,
"step_time": "2026-06-20T03:00:00Z"
},
"coverage": { "complete": true }
},
"provenance": {
"source": "gfs-noaa",
"dataset": "gfs",
"fetched_at": "2026-06-20T09:30:00Z",
"freshness": { "kind": "cached", "age_secs": 34200 }
}
}
Example - long horizon, ensemble register
A target beyond the deterministic window (here about 30 days out) is answered
from the GEFS ensemble mean. provenance.source names the ensemble,
provenance.dataset is gefs, data.grid reports the coarser 0.5-degree cell,
and lead.cycle is the GEFS 00Z cycle. Treat the values as a smoothed central
estimate, not a deterministic run: the 1.8 mm below is a member average, and
the tail never crosses a threshold the way
a real rainy day would.
{
"data": {
"lat": 48.8566,
"lon": 2.3522,
"horizon_h": 720,
"temperature": { "celsius": 16.9 },
"precipitation": { "total_mm": 1.8 },
"wind": { "speed_ms": 4.7, "direction_deg": 231.0 },
"grid": { "lat": 49.0, "lon": 2.5, "distance_km": 20.34 },
"lead": {
"cycle": "2026-06-19T00:00:00Z",
"valid_time": "2026-07-19T00:00:00Z",
"horizon_h": 720,
"step_time": "2026-07-19T00:00:00Z"
},
"coverage": { "complete": true }
},
"provenance": {
"source": "NOAA GEFS ensemble mean (35-day, 0.5°)",
"dataset": "gefs",
"fetched_at": "2026-06-20T01:20:00Z",
"freshness": { "kind": "live" }
}
}
Coverage honesty
Both registers are gridded forecast models, not weather station readings. The
response is the nearest or interpolated grid value for the served cell: about
0.25 degrees in the deterministic GFS register and about 0.5 degrees in
the GEFS ensemble register. Forecast steps are discrete (commonly around 3 hours
for GFS, coarser for the extended ensemble), and the public horizon reaches
up to 35 days (horizon_h 0 to 840). The real composite window depends
on how the GFS and GEFS 00Z cycles align, so the far bound moves with each
ingest rather than sitting at a fixed offset. Within the ensemble segment the
values are member averages with damped extremes: usable as a trend, not as a
parametric trigger — see
The ensemble tail smooths extremes.
A target inside the available forecast window can return 200 with null
values and coverage.complete: false when the grid has a local gap. A target
outside every covered segment - before the cycle, past the end, or in a gap
between the two registers - returns 400 OUT_OF_RANGE, because the service
cannot answer the request; its message quotes the windows actually covered. If
no forecast cycle has been ingested yet, the service returns 503
NO_FORECAST_AVAILABLE; that is a transient no-charge state, not a paid partial
answer.
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_TIME | Not exactly one of valid_time/horizon_h/date, malformed RFC 3339 or YYYY-MM-DD, or horizon_h outside 0..840 |
| 400 | OUT_OF_RANGE | Target is outside every covered forecast window |
| 400 | INVALID_VARIABLE | variables contains a family outside the supported set |
| 503 | NO_FORECAST_AVAILABLE | No forecast cycle is currently available in the store |
| 500 | INTERNAL | Internal error (detail logged, not exposed) |
Error messages are written to guide the retry - they state the accepted forms with a copyable example, and a 4xx is never charged. A weekday name is refused explicitly because it is ambiguous:
{ "error": "invalid date 'Wednesday': weekday names are ambiguous — provide a calendar date in YYYY-MM-DD format, e.g. 2026-07-05", "code": "INVALID_TIME" }
{ "error": "unknown place 'Xyzzy'; check the spelling, narrow with 'country' (ISO-3166 alpha-2), or provide lat/lon instead", "code": "UNKNOWN_LOCATION" }
The OUT_OF_RANGE message quotes the composite window actually covered. When
the deterministic and ensemble segments are contiguous it reads as one range;
when a gap separates them, both segments are cited so the caller sees exactly
what is answerable.
{ "error": "requested lead time is outside the available forecast window 2026-06-20T00:00:00+00:00..2026-07-05T06:00:00+00:00", "code": "OUT_OF_RANGE" }
{ "error": "no forecast cycle currently available", "code": "NO_FORECAST_AVAILABLE" }
See also
GET /weather/forecast/exceedance- counted member fraction crossing a threshold, over the ensemble tail.GET /climate/point- historical ERA5 reanalysis for dated climate evidence.- For agents - discovery surfaces, the live
/catalogand how settlement works.