API Documentation
A stateless HTTP API over every published model + forecast cycle.
One data endpoint — /v1/{model}/{cycle}/forecast — serves every
time-series shape, dispatched on parameters (point, region, daily, journey) with multi-variable
vars=; …/grid serves 2-D map fields (JSON or compact binary); a few
helpers describe what exists. Try any of these live in the
Playground.
Overview
The production base URL is https://api.weatherlabs.io; the
examples below write it as $BASE. One service
serves every retained cycle of every published model: pick yours in the path —
{model} is e.g. gfs (discover via /v1/models)
and {cycle} is either the literal latest (tracks the newest published
cycle, re-resolved server-side every ~30 s) or a pinned compact init id
(YYYYMMDDHH, e.g. 2026070500 — immutable while retained, maximally
cacheable). The serving store keeps the newest 12 cycles per model (~12 days at
the current daily publication cadence); there is no public historical archive yet. Each cycle becomes
available from NOAA roughly 3.5–4 hours after model initialisation; our daily ingest run starts
at 05:00 UTC, so the 00z cycle is typically live by ~06:00 UTC. Coordinates
are decimal degrees: latitude -90…90, longitude accepts both -180…180 and
0…360. The forecast is a series over lead-time steps aligned to the
valid_times array returned by every series endpoint; every data response echoes
model and the resolved compact cycle.
Two models are published. gfs is the deterministic GFS at 0.25°.
gefs is the 31-member GFS ensemble at 0.25°: the plain variable name
(t2m, tp, …) is the ensemble mean, and
<var>_p10 / <var>_p90 are the per-grid-cell 10th and 90th
percentiles across the 31 members — the raw ensemble spread, not calibrated probability
intervals. Every gefs surface variable carries _p10/_p90
companions, including precipitation (tp_p10/tp_p90). Request mean and
bounds together in one call:
/v1/gefs/latest/forecast?vars=t2m,t2m_p10,t2m_p90&lat=…&lon=….
The gefs catalogue is surface-only (no pressure levels, no prate), 3-hourly to
+240 h. Ensemble access is included from the Developer tier (£3/mo) up;
model discovery stays open to every key.
⚠ Weather data — not for safety-of-life use
The data this API returns is automatically processed numerical weather-model output, provided "as is". It may be inaccurate, incomplete, delayed or unavailable, and it is not suitable as the sole basis for any decision involving risk to life or property — including aviation, marine navigation or emergency management. See the full disclaimer in our Terms.
Authentication
Requests are authenticated with an API key passed as a bearer token in the
Authorization header. Create and manage keys (up to three) on your
Account page; each request counts against your plan's
quota.
curl -H "Authorization: Bearer $WEATHERLABS_API_KEY" \ "$BASE/v1/gfs/latest/forecast?lat=51.5074&lon=-0.1278&vars=t2m"
Never put a key in the URL. The server rejects keys passed as a query parameter
(?api_key=…) with a 400 (api_key_in_query) — a key in a URL
leaks into logs, history and referrers. Always use the header.
Demo access. The public Explorer and Playground work without signing in during the beta, within low demo limits. They're for trying the API — for anything real, get your own free key from the Account page (it's £0 and takes a minute).
Rollout note (as of July 2026). Authentication is being rolled out. Until enforcement is switched on
server-side, requests without a key continue to work; once enforced, a missing or invalid key
returns 401. Add the header now and your integration keeps working through the switch.
Pricing & metering
Plans differ by data allowance, available endpoints, grid resolution, forecast horizon, the variables you can query and the model cycles you can reach. Prices are in GBP and billing is handled by Paddle (our merchant of record). The Service is in beta — there is no SLA.
| Plan | Price | Data | Endpoints | Models | Resolution | Horizon |
|---|---|---|---|---|---|---|
| Free | £0 | 10 MB / day | Point + daily | gfs | —³ | 5 days |
| Developer | £3 / mo¹ | 50 MB / day | + grid & region | gfs + gefs | 1.0° | 5 days |
| Hobby | £29 / mo | 500 MB / mo | All endpoints | gfs + gefs | 0.5° | 10 days |
| Starter | £250 / mo | 5 GB / mo | All endpoints | gfs + gefs | 0.25° (native) | 16 days |
| Growth | £750 / mo | 50 GB / mo | All endpoints | gfs + gefs | 0.25° (native) | 16 days |
| Scale | £1,500 / mo | Unlimited² | All endpoints | gfs + gefs | 0.25° (native) | 16 days |
¹ Developer includes a 30-day free
trial — try grids and regions for a month before the first charge.
² Scale is unlimited under fair use and adds bespoke perils and meteorology
consultancy — contact
sales.
³ Resolution caps apply to gridded shapes (grid and region) only — Free has no
gridded endpoints, hence "—". Point queries always sample the native 0.25° grid, on every tier.
The Free plan covers the basic perils
(t2m temperature, tp precipitation, u10/v10 wind)
on the 00z cycle only — latest always works on Free; explicitly
pinning a non-00z cycle returns 403. Every paid plan — from Developer up — includes
all variables (gust, pressure levels, and more) on every published
cycle. Cycles are currently published once daily (the 00z run).
You may hold a maximum of three API keys per account; quotas are per customer (shared across your keys). Free/Developer quotas reset daily; Hobby and above reset monthly with the billing period.
How usage is metered
Usage is billed as decoded data volume: the number of forecast values a response returns × 4 bytes each, with a 32 KiB floor per request (journey requests are the exception — the floor applies per waypoint). For convenience we express this in call-equivalents: 1 call = 32 KiB. A small point lookup is 1 call; a big global grid is many. Honest examples:
| Request | Roughly |
|---|---|
Point forecast (…/forecast, full series) | 1 call |
| Global grid, one step, 1.0° | ≈ 8 calls |
| Global grid, one step, 0.5° | ≈ 32 calls |
| Global grid, one step, native 0.25° | ≈ 127 calls |
| Journey, 20 waypoints | ≥ 20 calls (floor per waypoint) |
So a £29 Hobby month (500 MB) is roughly 15,000 native point reads, or a few hundred
half-degree global frames — region aggregates and journeys fall in between. Every data response also
reports the exact bytes_read it touched, and responses carry quota headers so you never
have to guess:
| Header | Meaning |
|---|---|
| X-Quota-Limit | Your allowance for the current window (bytes). |
| X-Quota-Remaining | Bytes left before requests are rejected. |
| X-Quota-Reset | When the window resets (epoch seconds). |
| X-Billed-Bytes | What this request counted against your quota (≥ 32 KiB). |
When your quota is exhausted, further requests return 429 with
Retry-After — there are no surprise overage charges; you upgrade or
wait for the reset. You can read your live limits without spending much quota:
curl -H "Authorization: Bearer $WEATHERLABS_API_KEY" "$BASE/v1/limits" # → {"tier":"hobby","status":"active", # "quota":{"window":"month","limit_bytes":500000000,"used_bytes":12345678, # "remaining_bytes":487654322,"resets_at":"2026-08-01T00:00:00Z", # "call_equivalents":{"limit":15258,"used":376}}, # "endpoints":{"point":true,"daily":true,"region":true,"grid":true,"journey":true}, # "max_lead_hours":240,"min_resolution_deg":0.5, # "fair_use_rate_per_sec":10,"models":["gfs","gefs"],"max_keys":3}
Fair use. Within your data allowance, requests are also subject to reasonable
rate limits to keep the Service responsive for everyone — smooth automated workloads
to a steady rate rather than bursting at the cap. Sustained load far above fair-use rates may be
throttled (429 + Retry-After); see the
Acceptable Use Policy.
Variables & levels
Surface variables are 2-D; pressure variables take a level in hPa. The exact set is
whatever the served cycle contains — query /v1/{model}/{cycle}/vars to enumerate it.
| Name | Units | Description |
|---|---|---|
| t2m | K | 2 m temperature |
| u10 / v10 | m/s | 10 m wind components |
| gust | m/s | Wind gust |
| prate | kg/m²/s | Precipitation rate (instantaneous) |
| tp | kg/m² | Total precipitation (accumulated) |
| cape | J/kg | Convective available potential energy |
| msl / sp | Pa | Mean-sea-level / surface pressure |
| d2m | K | 2 m dewpoint temperature |
| tcc | % | Total cloud cover |
| dswrf | W/m² | Downward shortwave radiation (6-h average; absent at f000) |
| u100, v100 | m/s | 100 m wind components (turbine hub height) |
| vis | m | Surface visibility |
| cin | J/kg | Convective inhibition |
| pwat | kg/m² | Precipitable water |
| hpbl | m | Planetary boundary-layer height |
| crain, csnow, cfrzr, cicep | 0/1 | Categorical precipitation type (instantaneous) |
| t, u, v, z, q, rh | — | Pressure-level fields (1000/925/850/700/500/300/250/200 hPa) |
Units are returned as is from the source (kelvin, pascals, etc.) — convert client-side. The Explorer and Playground show the conversions used.
Raw model output. Values are the raw NOAA model output on the unmodified native 0.25° (721×1440) grid — no bias correction, MOS, statistical post-processing, downscaling or resampling is applied. Paid-tier resolution caps coarsen the sampled grid on gridded shapes only; the underlying values are untouched.
Wind speed is not a server-side variable. Request the components
u10 and v10 in one call and take √(u10² + v10²) client-side —
the Explorer derives it exactly this way.
tp semantics. Total precipitation is served as
6-hour accumulation buckets derived from GFS APCP: each step's value is the
precipitation accumulated since the previous 6-hour boundary. There is no tp at
f000 (accumulation is forecast-only). Daily totals
(granularity=daily) difference the buckets, so they never
double-count.
The catalogue is atmospheric fields only (GFS atmospheric output) — there are no ocean-wave, swell or visibility fields.
Time selection
Every /forecast shape (point, region, daily, journey) accepts ONE of three
mutually exclusive selections; omitted → all steps. The selection resolves once per request —
every variable in vars= subsets by the same indices, so each response carries ONE
shared valid_times.
| Form | Meaning |
|---|---|
time=2026-06-04T12:00Z | The single valid time nearest the instant — must lie within the cycle's range (outside → 400). |
start=…&end=… | Inclusive valid-time range; either bound optional. |
steps=0,6,12 | Explicit lead hours — each must exist in the cycle (unknown hour → 400 listing the available hours). |
steps=0-48 | Every stored lead hour within the inclusive range. |
Accepted datetime forms: RFC 3339 (2026-06-04T12:00:00Z, offsets applied:
…T13:00:00+01:00), the no-seconds form 2026-06-04T12:00Z (UTC), and
date-only 2026-06-04 (as start → 00:00:00, as end →
23:59:59; ambiguous as time= → 400).
steps= are lead HOURS, never array indices. GFS steps are hourly
to h120 and 3-hourly after, so steps=123 works but steps=121 is a
400. The one index-typed parameter in the API is /grid's
step=. Daily aggregates over a partial selection produce partial days, but precip
totals stay exact — increments are computed over the full series, then subset.
GET /v1/models
Discovery: every model this deployment publishes.
curl "$BASE/v1/models" # → [{"name":"gfs","description":"…","grid":{"nlat":721,"nlon":1440,…}, # "latest":"2026070600","cycles_count":12}]
GET /v1/models/{model}
One model plus its latest-cycle layout: n_steps, step_hours,
vars, the init date_range and grid.
curl "$BASE/v1/models/gfs" # → {"name":"gfs","latest":"2026070600","cycles_count":12, # "date_range":["2026062500","2026070600"],"n_steps":209, # "step_hours":[0,1,…],"vars":["cape","gust","t2m",…],"grid":{…}}
GET /v1/models/{model}/cycles
Every retained cycle — the store keeps the newest 12 per model (~12 days at the
current daily cadence). Pin one as /v1/{model}/{compact}/… for URLs that are immutable
while retained; a pruned cycle returns 404.
curl "$BASE/v1/models/gfs/cycles" # → {"model":"gfs","latest":"2026070600", # "cycles":["2026062500","…","2026070500","2026070600"], # "date_range":["2026062500","2026070600"]}
GET /v1/{model}/{cycle}/cycle
Metadata for the scoped cycle: init time, step count, step hours and valid-times.
curl "$BASE/v1/gfs/latest/cycle" # → {"model":"gfs","cycle":"2026070500","init_time":"…","init_compact":"2026070500", # "n_steps":209,"step_hours":[0,1,2,…],"valid_times":["…Z",…],"tile":16}
GET /v1/{model}/{cycle}/vars
Every variable in the cycle, with kind, units, long name and pressure levels.
curl "$BASE/v1/gfs/latest/vars" # → [{"name":"t2m","kind":"surface","units":"K","long_name":"…","levels":[]}, # {"name":"t","kind":"pressure","units":"K","levels":[850,700,500,250]}, …]
GET /v1/{model}/{cycle}/forecast — the endpoint
One endpoint for every time-series shape, dispatched on parameters. Give it
lat+lon for a point, or region=/a bbox for an aggregate —
both or neither is a clear 400. vars= takes a comma-separated
list (up to 8 per request; duplicates or one unknown name fail the whole request) and the
response keys per-variable payloads under "vars" with ONE shared
valid_times. A point's entire series lives in a single tile, so a point call is one
object fetch and decode per variable — and the 32 KiB billing floor applies once per
request (journey excepted: per waypoint), so multi-var calls are
the cheap way to fetch several fields.
| Param | Type | Description |
|---|---|---|
| vars | string | Comma-separated variable names, e.g. t2m,tp. Required. |
| lat, lon | float | Point shape: latitude −90…90, longitude −180…180 or 0…360. Mutually exclusive with region/bbox. |
| region / lat_s…lon_e | — | Region shape — see region aggregates. |
| granularity | string | hourly (default) or daily — see daily aggregates. |
| level | float | Pressure level in hPa — request-global (applies to every pressure var in vars=; surface vars ignore it). |
| interp | string | nearest (default) or bilinear (point shape). |
| time / start+end / steps | — | Optional time selection; steps= are lead hours. |
curl "$BASE/v1/gfs/latest/forecast?lat=51.5074&lon=-0.1278&vars=t2m,tp&interp=bilinear&steps=0-48" # → {"model":"gfs","cycle":"2026070500","granularity":"hourly","valid_times":[…], # "vars":{"t2m":{"units":"K","values":[…]},"tp":{"units":"kg m**-2","values":[…]}}, # "tiles_read":2,"bytes_read":…}
Daily aggregates — granularity=daily
The same endpoint with granularity=daily: per-variable daily aggregates derived
from the native step series — min/max/mean for instantaneous fields, trapezoidal integrals for
rates (prate), and accumulation-differenced totals for tp — each
variable picks its own physically correct reducer in the same request. With a time selection,
only the selected steps contribute (partial days are partial — but precip totals are always
computed on the full series first, so they never inflate). Daily is point-only for now: daily
region/journey returns 400 unsupported.
curl "$BASE/v1/gfs/latest/forecast?lat=40.71&lon=-74.01&vars=t2m&granularity=daily" # → {"model":"gfs","cycle":"…","granularity":"daily", # "vars":{"t2m":{"units":"K","daily":[{"date":"2026-07-05","min":…,"max":…,"mean":…}, …]}}}
GET /v1/{model}/{cycle}/regions
The named regions this service can aggregate over — continents, countries, and sub-national areas like Greater London. Each is a true polygon boundary (Natural Earth), precomputed onto the forecast grid: "mean over the UK" aggregates only grid cells whose centres fall inside the UK polygon, not a bounding box that would also cover Ireland and northern France.
curl "$BASE/v1/gfs/latest/regions" # → [{"name":"africa","display_name":"Africa","kind":"continent", # "bbox":[-34.75,37.25,-25.25,51.25],"n_cells":40926}, …]
bbox is
[lat_s, lat_n, lon_w, lon_e]; lon_w > lon_e
means the region crosses the antimeridian (e.g. usa, russia).
n_cells is the number of grid cells inside the polygon.
Region aggregates — region= or a bbox (single step)
The same endpoint with a spatial selection instead of lat+lon:
aggregate each variable over a named region polygon
(region=) or a raw bounding box, at exactly one forecast step.
Region requests are single-step by design — each one is served from a single
step-major frame object (cold in tens of milliseconds) and is independently cacheable at the
edge. Select the step with steps= (one lead hour) or time=; a request
resolving to more than one step (including no selector at all) returns
400 region_single_step. For a series over a region, issue one request per
step — note each request bills the 32 KiB minimum. Named-region means are cos-latitude
area-weighted (a 0.25° cell at 70°N covers ~⅓ the area of an equatorial one); bbox means remain
plain unweighted cell means. Large areas auto-coarsen (cell striding) — the response reports
stride and coarsened.
| Param | Type | Description |
|---|---|---|
| region | string | Named region (see …/regions). Mutually exclusive with the bbox params (and with lat+lon). |
| lat_s, lat_n | float | South / north latitude bounds. Required without region. |
| lon_w, lon_e | float | West / east longitude bounds. Required without region. |
| vars | string | Comma-separated variable names. Required. |
| level | float | Pressure level (pressure vars). |
| agg | string | mean (default), min, or max. |
| steps / time | — | Required: a time selection resolving to exactly one step — steps=24 (one lead hour) or time= (nearest datetime). Anything else is 400 region_single_step. |
curl "$BASE/v1/gfs/latest/forecast?region=uk&vars=gust&agg=max&steps=24" # → {"model":"gfs","cycle":"…","agg":"max","region":"uk","bbox":[50.25,60.5,-8,1.75], # "valid_times":["…T00:00:00Z"],"vars":{"gust":{"units":"m s**-1","values":[18.7]}},"n_cells":535,…} curl "$BASE/v1/gfs/latest/forecast?lat_s=49&lat_n=59&lon_w=-8&lon_e=2&vars=gust&agg=max&time=2026-06-08T12:00Z" # → {"agg":"max","region":null,"bbox":[…],"vars":{"gust":{"values":[…1 value…]}},"n_cells":…,"stride":1,"coarsened":false}
POST /v1/{model}/{cycle}/forecast — journey
The same endpoint, POSTed with a waypoint body: one forecast series per waypoint per variable,
fetched concurrently — waypoints that share a tile coalesce. Useful for routes, fleets, or a set
of stations (max 512 waypoints). Optional "time"/"start"/
"end"/"steps" body fields apply a time selection,
resolved once for all waypoints and variables. Units are hoisted to a top-level
"vars" map; points carry values only. Billing: the 32 KiB floor applies
per waypoint (512 waypoints ⇒ at least ~16 MB billed).
Routes with ETAs: there is no per-waypoint time parameter — fetch the whole trip
window ("start"/"end") and match each waypoint to its nearest
valid_time client-side.
curl -X POST "$BASE/v1/gfs/latest/forecast" -H "Content-Type: application/json" -d '{ "vars": "t2m", "interp": "bilinear", "steps": "0-48", "points": [{"lat":51.51,"lon":-0.13},{"lat":53.48,"lon":-2.24},{"lat":55.95,"lon":-3.19}] }' # → {"model":"gfs","cycle":"…","valid_times":[…],"vars":{"t2m":{"units":"K"}}, # "points":[{"lat":…,"lon":…,"vars":{"t2m":{"values":[…]}}}, …]}
GET /v1/{model}/{cycle}/grid
A full lat/lon field for one forecast step — the data the
Explorer renders. Returns JSON by default, or a compact
little-endian binary with format=bin (a 72-byte header followed by row-major
float32 values, north row first; X-Model/X-Cycle response
headers carry the scope). Defaults to native resolution; pass max_cells to coarsen the
output (note: a global slice reads the same tiles either way, so coarsening reduces transfer/CPU,
not I/O).
step= is an ARRAY INDEX into the cycle's steps — NOT a lead hour.
For all-hourly products index == hour, which hides the difference until the 3-hourly tail: index
130 is lead-hour 150 on a 209-step GFS cycle. To pick a datetime use time= instead
(nearest single step; mutually exclusive with step=). Multi-step selections
(steps=, start=, end=) are a 400 here.
| Param | Type | Description |
|---|---|---|
| var | string | Variable name. Required. |
| step | int | Forecast step index (default 0) — not a lead hour. |
| time | string | Datetime → nearest single step. Mutually exclusive with step. |
| level | float | Pressure level (pressure vars). |
| region | string | Named region (see …/regions): crops to the region's bbox and returns NaN outside the polygon. Mutually exclusive with the bbox params. |
| lat_s, lat_n, lon_w, lon_e | float | Bounding box (default global). |
| max_cells | int | Coarsen so output cells ≤ this. |
| format | string | json (default) or bin. |
curl "$BASE/v1/gfs/latest/grid?var=t2m&step=8" # JSON → {"model":"gfs","cycle":"…","nlat":721,"nlon":1440,"lat0":90,"dlat":-0.25, # "lon0":0,"dlon":0.25,"vmin":…,"vmax":…,"valid_time":"…Z","values":[…]} curl "$BASE/v1/gfs/latest/grid?var=t2m&step=8&format=bin" --output field.bin # binary → headers X-Model, X-Cycle, X-Tiles-Read, X-Bytes-Read, X-Valid-Time + float32 payload
Removed routes & parameters
The pre-consolidation routes are gone (404): the unscoped legacy tree
(/v1/cycle, /v1/vars, /v1/regions,
/v1/forecast[…], /v1/region, /v1/grid,
/v1/journey) and the per-shape scoped routes
(…/forecast/hourly, …/forecast/daily, …/region,
…/journey). Their shapes all live on the unified …/forecast via
parameters. Two retired parameters return a hard 400 naming the
replacement: var= (singular) → vars=, and the old cycle=
query parameter → pin cycles in the path instead (/v1/gfs/2026070500/…).
Caching
Responses carry Cache-Control chosen by scope: pinned-compact URLs are
public, max-age=31536000, immutable (a published cycle never changes under its id,
and is served while retained — the newest 12 cycles per model);
latest-scoped URLs are public, max-age=30
(matching the server's own latest-resolution interval); discovery endpoints are
public, max-age=60. Pin the compact id whenever you fan many requests over one
cycle.
Response format
Every /forecast response shares a shape: model and the
resolved compact cycle, granularity, ONE
valid_times array (ISO-8601), and a "vars" object keyed by your
requested names — each carrying units plus values (aligned to
valid_times, subset together under a time selection) or
daily. Key order inside "vars" is not significant — index by name.
Responses also carry init_time and the tiles_read /
bytes_read the request actually touched. Missing data is
null/NaN, never silently zero.
Errors
Errors return the appropriate HTTP status with a JSON envelope:
# 429 example { "error": "hobby plan quota exhausted for this month — resets at 2026-08-01T00:00:00Z; upgrade for a larger quota", "code": "quota_exceeded", "request_id": "8f2c…", "quota": {"limit": 500000000, "remaining": 0, "resets_at": "2026-08-01T00:00:00Z", "window": "month", "unit": "bytes"} }
error is a human-readable message; code is a stable machine token you can
branch on; request_id identifies the request in our logs (quote it in support
requests); quota is included on quota/billing errors. Auth, quota and billing codes:
| Code | HTTP | Meaning & fix |
|---|---|---|
| no_api_key | 401 | No key sent. Add Authorization: Bearer <key>. |
| invalid_key | 401 | Key not recognised. Check it was copied in full from the Account page. |
| key_revoked | 401 | Key was revoked. Generate a new one and update your client. |
| tier_forbidden | 403 | Endpoint/resolution/horizon not in your plan. Upgrade to unlock it. |
| quota_exceeded | 429 | Data allowance used up. Wait for Retry-After/reset, or upgrade. No overage is billed. |
| payment_past_due | 403 | A renewal payment failed and the grace period ended. Update billing from the dashboard to restore access. |
| subscription_canceled | 403 | Subscription cancelled. Resubscribe from the dashboard to restore access. |
| account_suspended | 403 | Account suspended (e.g. AUP breach or unpaid). Contact support. |
| model_forbidden | 403 | The requested model is not in your tier. gefs is included from Developer up — upgrade to access it. |
| demo_disabled | 403 | The shared demo key is unavailable or rate-limited. Use your own key. |
| api_key_in_query | 400 | Key was passed as ?api_key=. Send it in the Authorization header instead. |
| origin_forbidden | 403 | The request reached the origin without going through the public API endpoint. Call the API via the base URL above. |
Request-shape errors keep the same envelope and the existing 400/404
semantics: 400 for a missing/empty vars=, more than 8 names
(too_many_vars), a duplicate or unknown variable (one bad name fails the whole
request — no partial responses), both a point AND a region selection (or neither),
granularity=daily on a region/journey shape (unsupported), an
out-of-range step index, a pressure level the cycle doesn't carry, a malformed or out-of-range
time selection, or the retired var=/cycle=
parameters (each 400 names its replacement); 404 for an
unknown model (the message lists the available models), an unknown or pruned cycle (the message lists
the retained cycles and the latest), or an unknown route — including every pre-consolidation
route.
API stability & deprecation
The API is versioned under /v1. Backwards-compatible additions — new
endpoints, new optional parameters, new fields in a response — can happen at any time, so write
clients that tolerate unknown fields. Breaking changes get either a new version
prefix or at least 90 days' notice via email and the changelog before they take
effect.
The legacy unscoped aliases (/v1/forecast, /v1/grid, etc.) have been
removed — see Removed routes. Pinning a model and a compact
cycle in the path (/v1/gfs/2026070500/…) is the stable, maximally cacheable form and
is recommended for production. gfs and gefs are both generally
available; further models are roadmap and are not part of the API until generally available.
Performance
- Point/journey: one tile fetch per coordinate — a point's full series is a single object.
- Grid: a global slice reads every tile once (the store is time-major), warming the in-process
tile cache; subsequent steps/variables are then served from RAM. Use
format=binfor the fast path into a typed array. - Caching: each cycle is immutable, so responses are CDN-cacheable by URL.