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:

RegisterHorizon from cycleModelMesh (data.grid)provenance.source
Deterministicup to 384 h (about 16 days)NOAA GFS~0.25 degreesgfs-noaa / GFS
Ensemble mean384 h to 840 h (about 35 days)NOAA GEFS~0.5 degreesNOAA 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):

SegmentRegisterDaily precipitationDaily max temperature
J+0 → J+15 (16 days)deterministic GFS9 days at 0.0 mm, peak 13.12 mm21.2 → 40.5 °C, a 19.3 °C spread
J+16 → J+34 (19 days)GEFS ensemble meanno day below 0.40 mm, none above 1.82 mm25.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

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
horizon_hnumberone of target fieldsForecast lead in hours from the current GFS cycle, integer 0 to 840 (35 days)
valid_timestringone of target fieldsForecast target instant in RFC 3339 UTC form, for example 2026-06-20T12:00:00Z
datestringone of target fieldsForecast day as YYYY-MM-DD, served at 12:00 UTC of that day
variablesstringnoComma-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 / GFS in the deterministic register, or NOAA GEFS ensemble mean (35-day, 0.5°) in the extended ensemble register.
  • provenance.dataset: the machine code of that same model, gfs or gefs - the field to branch on, since source is a human label. gefs means the smoothed ensemble mean, read as a trend.
  • freshness.kind: live when the cycle is fresh, or cached with age_secs when 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

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; present only when the request used city/location
horizon_hnumberEchoed when the request used horizon_h; omitted otherwise
valid_timestringEchoed when the request used valid_time; omitted otherwise
datestringEchoed when the request used date; omitted otherwise
temperatureobjectRequested 2 m temperature; omitted when not requested
precipitationobjectRequested precipitation; omitted when not requested
windobjectRequested 10 m wind; omitted when not requested
convectiveobjectOpt-in convective instability (CAPE/CIN + convective_risk); present only when variables requested convective
humidityobjectOpt-in 2 m relative humidity; present only when variables requested humidity
cloudsobjectOpt-in total cloud cover; present only when variables requested clouds
snowobjectOpt-in snow depth on the ground and snowfall flag; present only when variables requested snow
gridobjectEffective grid cell used for the answer; omitted if no cell served
leadobjectResolved cycle, target and served model step
coverageobjectCompleteness 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.

FieldTypeDescription
namestringCanonical gazetteer name of the resolved place, e.g. Zürich
countrystringISO-3166 alpha-2 country code of the resolved place
admin1stringFirst-level administrative division; can be empty
latnumberLatitude the point was resolved to
lonnumberLongitude the point was resolved to
sourcestringAlways GeoNames

temperature

FieldTypeDescription
celsiusnumber | null2 m temperature in Celsius

precipitation

FieldTypeDescription
total_mmnumber | nullForecast precipitation total in millimetres

wind

FieldTypeDescription
speed_msnumber | null10 m wind speed in metres per second
direction_degnumber | nullMeteorological 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.

FieldTypeDescription
cape_jkgnumber | nullSurface Convective Available Potential Energy, J/kg (raw)
cin_jkgnumber | nullConvective Inhibition, J/kg (raw, ≤ 0); a strong cap suppresses convection
riskstring | nullQualitative instability from CAPE in J/kg (NWS/SPC conventions): none [0,100), low [100,1000), moderate [1000,2500), high [2500,4000), extreme [4000,∞)
inhibitedbool | nulltrue 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

FieldTypeDescription
relative_pctnumber | null2 m relative humidity, percent

clouds

FieldTypeDescription
total_cover_pctnumber | nullTotal (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.

FieldTypeDescription
depth_mnumber | nullSnow depth on the ground in metres (state)
is_snowbool | nullWhether precipitation at the served step is falling as snow

grid

FieldTypeDescription
latnumberLatitude of the representative grid cell
lonnumberLongitude of the representative grid cell
distance_kmnumberDistance from the requested point to that cell

lead

lead makes the resolved forecast target explicit, whether the caller supplied horizon_h or valid_time.

FieldTypeDescription
cyclestringAnalysis cycle of the served model, RFC 3339 UTC (GFS cycle, or the GEFS 00Z cycle in the ensemble register)
valid_timestringResolved target instant, RFC 3339 UTC
horizon_hnumberLead time in hours from cycle to valid_time
step_timestring | nullActual 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.

FieldTypeDescription
completebooltrue when all requested variables have values
reasonstringPresent 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.

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_TIMENot exactly one of valid_time/horizon_h/date, malformed RFC 3339 or YYYY-MM-DD, or horizon_h outside 0..840
400OUT_OF_RANGETarget is outside every covered forecast window
400INVALID_VARIABLEvariables contains a family outside the supported set
503NO_FORECAST_AVAILABLENo forecast cycle is currently available in the store
500INTERNALInternal 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