API Documentation
A stateless HTTP API over every published model and forecast cycle.
One endpoint, /v1/{model}/{cycle}/forecast, serves every
time-series shape (point, region, daily, journey) for up to eight variables at once. A second,
…/grid, serves 2-D map fields as JSON or compact binary. A few helpers describe
what exists. Alongside the forecasts,
/v1/{metar|taf|sigmet|station}/{when}/{view} serves
aviation observations from the NOAA Aviation Weather Center. Try any
of these live in the Playground.
The API is for accounts that a WeatherLabs administrator has issued a key to; customers buy applications, which use it for them. Ask about API access.
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). A cycle is one model run, named by
its initialisation time; the rest of the website calls it a run. 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°: every gefs name ends in its
statistic. <variable>_mean (temperature_2m_mean,
precipitation_mean, …) is the ensemble mean, and
<variable>_p10 / <variable>_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 has all
three, including precipitation
(precipitation_p10/precipitation_p90). Build each name from the
variable and the statistic, and request mean and bounds together in one call:
/v1/gefs/latest/forecast?vars=temperature_2m_mean,temperature_2m_p10,temperature_2m_p90&lat=…&lon=….
The gefs catalogue is surface-only (no pressure levels, no precipitation_rate),
3-hourly to +240 h. A key reads gefs when one of its account's applications grants it;
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. Keys are issued by WeatherLabs administrators, up to three
per account; your Account page lists the keys
issued to you and lets you revoke them. Each request counts against your applications' monthly
allowance, and a missing or invalid key returns 401.
curl -H "Authorization: Bearer $WEATHERLABS_API_KEY" \ "$BASE/v1/gfs/latest/forecast?lat=51.5074&lon=-0.1278&vars=temperature_2m"
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),
because a key in a URL leaks into logs, history and referrers. Always use the header.
Demo access. The public Explorer reads the API with a shared demo key, within low demo limits. It is for looking around; for your own integration, ask us for a key.
Pricing & metering
Access is sold as applications, each a monthly subscription for one user. What a key may read is the union of what the account's live applications grant: for each grant line, the models, the variables, the lead-hour horizon (hours since the model run was initialised) and the endpoints (point, daily, region, grid, journey, aviation), plus a monthly data allowance and, for gridded reads, the finest output resolution. The Applications page lists what each one grants and costs; prices are in GBP and billing is handled by Paddle (our merchant of record). The Service is in beta — there is no SLA.
-
A request may ask for several variables; it reaches the nearest of their
horizons. Window selections (
start/end,steps=A-B, or none) are clamped to it; an explicittime=orsteps=list beyond it returns403 not_entitled, as does a variable, endpoint or model no application grants. -
An account with no live application gets
403 no_applicationfrom the data endpoints; model discovery, cycle and variable metadata and/v1/limitsstill answer. - Allowances add up across an account's applications and are metered per customer, shared across its keys, per UTC calendar month. An application that lapses mid-month keeps its allowance in the sum until the month ends.
- Administrators can also grant an application to an account without a subscription, with an expiry — that is how developers and partners get API access, and how the broad Forecast API access for integrations is given.
You may hold a maximum of three API keys per account. Point, daily, journey
and region reads sample the native grid; resolution caps apply to /grid only.
How usage is metered
Usage is billed as decoded data volume: the number of forecast values a response returns × 4 bytes each, with no per-request minimum: you pay only for the data you get back. Worked examples:
| Request | Roughly |
|---|---|
Point forecast (…/forecast, one variable, full 209-step series) |
836 bytes |
| Journey, 64 waypoints, 5 variables, each with a ~27-hour window | ≈ 35 KB |
| Global grid, one step, 1.0° | ≈ 261 KB |
| Global grid, one step, 0.5° | ≈ 1.04 MB |
| Global grid, one step, native 0.25° | ≈ 4.15 MB |
| Aviation observations (METAR, TAF, SIGMET or station), per report returned | 256 bytes |
Sizes and quotas are decimal: 1 KB is 1,000 bytes and 1 MB is 1,000,000 bytes. So an
application with a 500 MB monthly allowance covers hundreds of thousands of full point
series, or a few hundred half-degree global frames. Every data response also reports the exact
bytes_read it touched, and responses carry quota headers:
| 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 (values returned × 4 bytes). |
When your quota is exhausted, further requests return 429 with
Retry-After. There are no overage charges: you add an
application 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" # → {"applications":[{"slug":"route","source":"subscription"},{"slug":"vineyard","source":"subscription"}], # "access_allowed":true,"access_status":"active","status":"active", # "quota":{"window":"month","limit_bytes":600000000,"used_bytes":12345678, # "remaining_bytes":587654322,"resets_at":"2026-11-01T00:00:00Z", # "call_equivalents":{"limit":18310,"used":376}}, # usage in 32,768-byte units # "endpoints":{"point":true,"daily":true,"region":false,"grid":false,"journey":true,"aviation":false}, # "max_lead_hours":240,"min_resolution_deg":null,"cycles":"all", # "variables":[{"name":"precipitation","max_lead_hours":120},{"name":"temperature_2m","max_lead_hours":240},…], # "grants":[{"application":"route","models":["gfs"],"variables":[…],"max_lead_hours":240,"endpoints":["point","journey"]},…], # "fair_use_rate_per_sec":10,"models":["gfs"],"max_keys":3,"applications_revision":3,"source":"record"}
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 & names
Every name is readable English built as
<quantity>[_<level>][_<period>][_<statistic>], in
lowercase: the level is part of the name (temperature_2m,
wind_u_100m, temperature_850hpa), _avg marks an
interval mean, and on the gefs ensemble the statistic comes last
(_mean, _p10, _p90). A pressure level is just another
name, so several levels are several names in one vars= list. The exact set is
whatever the served cycle contains — query
/v1/{model}/{cycle}/vars to enumerate it; each entry also
gives the name's parts (quantity, level, period, statistic), so you never need to parse names.
| Name | Units | Description | Replaces |
|---|---|---|---|
| temperature_2m | K | 2 m temperature | t2m |
| wind_u_10m wind_v_10m |
m/s | 10 m wind components | u10 / v10 |
| wind_gust_surface | m/s | Wind gust | gust |
| precipitation_rate | kg/m²/s | Precipitation rate (instantaneous) | prate |
| precipitation | kg/m² | Total precipitation (accumulated) | tp |
| cape_surface | J/kg | Convective available potential energy | cape |
| pressure_msl pressure_surface |
Pa | Mean-sea-level / surface pressure | msl / sp |
| dewpoint_2m | K | 2 m dewpoint temperature | d2m |
| cloud_cover_total | % | Total cloud cover | tcc |
| downward_shortwave_radiation_avg | W/m² | Downward shortwave radiation (6-h average; absent at f000) | dswrf |
| wind_u_100m wind_v_100m |
m/s | 100 m wind components (turbine hub height) | u100 / v100 |
| visibility | m | Surface visibility | vis |
| cin_surface | J/kg | Convective inhibition | cin |
| precipitable_water_column | kg/m² | Precipitable water | pwat |
| boundary_layer_height | m | Planetary boundary-layer height | hpbl |
|
categorical_rain categorical_snow categorical_freezing_rain categorical_ice_pellets |
0/1 | Categorical precipitation type (instantaneous) | crain, csnow, cfrzr, cicep |
|
temperature_<level>hpa wind_u_<level>hpa wind_v_<level>hpa geopotential_height_<level>hpa specific_humidity_<level>hpa relative_humidity_<level>hpa |
— |
Pressure-level fields, one name per level (1000/925/850/700/500/300/250/200 hPa),
e.g. geopotential_height_500hpa
|
t, u, v, z, q, rh + level= |
Old names. These names replaced short NCEP-style ones (the "Replaces" column)
in October 2026; values and units are unchanged. Until 30 November 2026 the
API still accepts the old names, level= (without it an old pressure name means
850 hPa) and the bare gefs mean (an old gefs name without a suffix is the
_mean). Responses are keyed by the spelling you sent and carry
Deprecation: true and Sunset: Mon, 30 Nov 2026 00:00:00 GMT headers;
/vars lists the old names too, flagged "deprecated": true with their
replacement. After that date old names return 400.
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. An application's resolution cap coarsens the sampled grid on gridded shapes only; the underlying values are untouched.
Wind speed is not a server-side variable. Request the components
wind_u_10m and wind_v_10m in one call and take
√(wind_u_10m² + wind_v_10m²) client-side — the Explorer derives it exactly this way
and shows it as wind_speed_10m.
precipitation 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
precipitation 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 or swell 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. Each model's attribution says
where its data come from: provider, product, licence,
licence_url, terms_url (null when the provider has no
separate terms), the notice to show beside the model's data (linked to
licence_url) and the modifications WeatherLabs makes. When a model
has no attribution, the API has no notice for it.
curl "$BASE/v1/models" # → [{"name":"gfs","description":"…","grid":{"nlat":721,"nlon":1440,…}, # "latest":"2026070600","cycles_count":12, # "attribution":{"provider":"NOAA National Centers for Environmental Prediction (NCEP)",…, # "licence_url":"https://www.weather.gov/disclaimer","terms_url":null, # "notice":"Source: NOAA NCEP Global Forecast System.",…}}]
GET /v1/models/{model}
One model plus its latest-cycle layout: n_steps, step_hours,
vars, the init date_range, init_time and grid, with its
description and attribution (as in
/v1/models; null when it has none).
curl "$BASE/v1/models/gfs" # → {"name":"gfs","description":"…","grid":{…},"latest":"2026070600","cycles_count":12, # "date_range":["2026062500","2026070600"],"init_time":"2026-07-06T00:00:00.000000000", # "n_steps":209,"step_hours":[0,1,…],"vars":["cape_surface","precipitation","temperature_2m",…], # "attribution":{"provider":"NOAA National Centers for Environmental Prediction (NCEP)",…}}
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 units, long name and the name's structured parts:
quantity, name_level (type, value,
units), period and statistic. Until 30 November 2026
the list also carries the old names, flagged "deprecated": true with their
replacement.
curl "$BASE/v1/gfs/latest/vars" # → [{"name":"temperature_2m","units":"K","long_name":"2 metre temperature","quantity":"temperature", # "name_level":{"type":"height_above_ground","value":2,"units":"m"},"period":null,"statistic":"deterministic",…}, # {"name":"temperature_850hpa","units":"K","quantity":"temperature", # "name_level":{"type":"isobaric","value":850,"units":"hPa"},…}, …]
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 billing counts only the values
returned, so a multi-var call costs the same as the separate calls it replaces.
| Param | Type | Description |
|---|---|---|
| vars | string |
Comma-separated variable names, e.g. temperature_2m,precipitation.
Pressure levels are names too (temperature_850hpa,temperature_500hpa).
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.
|
| days | string |
Daily shape only: local (default — calendar days at the point's timezone)
or utc. On a non-daily request days= is a 400.
|
| level | float | Deprecated — the level is part of the name now. Accepted with the old names until 30 November 2026. |
| 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=temperature_2m,precipitation&interp=bilinear&steps=0-48" # → {"model":"gfs","cycle":"2026070500","granularity":"hourly","valid_times":[…], # "timezone":"Europe/London","local_times":["2026-07-05T01:00:00+01:00",…], # "vars":{"temperature_2m":{"units":"K","values":[…]},"precipitation":{"units":"kg m**-2","values":[…]}}, # "tiles_read":2,"bytes_read":…}
Located responses carry the IANA timezone and local_times —
valid_times rendered with the location's UTC offset, per element, so a series
crossing a daylight-saving change flips offset mid-array. Journeys carry them per
waypoint; region responses use a representative point plus an other_timezones
note.
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 (precipitation_rate), and accumulation-differenced totals for
precipitation — each variable picks its own physically correct reducer in the
same request. Days are bucketed by
local calendar days at the point's timezone by default (the response reports
"days" and "timezone"; first/last days of a cycle are usually
partial locally) — pass days=utc for the previous UTC-day buckets. 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=temperature_2m&granularity=daily" # → {"model":"gfs","cycle":"…","granularity":"daily","days":"local","timezone":"America/New_York", # "vars":{"temperature_2m":{"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 — each request bills only its returned
values. 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 |
Deprecated — use the level's name (wind_u_250hpa); accepted with
old names until 30 November 2026.
|
| 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=wind_gust_surface&agg=max&steps=24" # → {"model":"gfs","cycle":"…","agg":"max","region":"uk","bbox":[50.25,60.5,-8,1.75], # "valid_times":["…T00:00:00Z"],"timezone":"Europe/London","local_times":["…T01:00:00+01:00"], # "vars":{"wind_gust_surface":{"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=wind_gust_surface&agg=max&time=2026-06-08T12:00Z" # → {"agg":"max","region":null,"bbox":[…],"vars":{"wind_gust_surface":{"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 counts
the values returned (waypoints × steps × variables × 4 bytes).
Routes with ETAs: give each waypoint its own inclusive
"start"/"end" (e.g.
{"lat":50.4,"lon":-4.1,"start":"2026-06-04T06:00Z","end":"2026-06-05T09:00Z"}) to
fetch only the hours a traveller can be there. Per-waypoint windows replace the request-level
selection (sending both is a 400); each point then carries its own
valid_times and the top-level valid_times is omitted. Widen a window
to the steps either side of an ETA to interpolate it.
curl -X POST "$BASE/v1/gfs/latest/forecast" -H "Content-Type: application/json" -d '{ "vars": "temperature_2m", "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":{"temperature_2m":{"units":"K"}}, # "points":[{"lat":…,"lon":…,"timezone":"Europe/London","local_times":[…], # "vars":{"temperature_2m":{"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, e.g. temperature_2m or
geopotential_height_500hpa. Required.
|
| step | int | Forecast step index (default 0) — not a lead hour. |
| time | string | Datetime → nearest single step. Mutually exclusive with step. |
| level | float | Deprecated — the level is part of the name; accepted with old names until 30 November 2026. |
| 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=temperature_2m&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=temperature_2m&step=8&format=bin" --output field.bin # binary → headers X-Model, X-Cycle, X-Tiles-Read, X-Bytes-Read, X-Valid-Time + float32 payload
Aviation observations — /v1/{product}/{when}/{view}
METAR observations, TAFs, SIGMETs/AIRMETs and station metadata from the NOAA Aviation Weather
Center (AWC). We read AWC's worldwide bulk feeds every minute and serve the newest snapshot
from memory, so a lookup never waits on AWC. These routes sit beside the forecast models
rather than inside them: there is no {model} or {cycle}; the path
names the product, a time and a view. They take the same API keys and count against the same
quota, and are included when one of the account's applications grants the
aviation endpoint (Aviator does; so does the Playground's demo access). Other
keys get 403 not_entitled.
| Path | Values | Description |
|---|---|---|
| product | metar | taf | sigmet | station | Which report type. Each has its own section below. |
| when | latest | time |
latest for the newest data, or a past UTC time to read the archive:
2026-10-05T12:00Z, with seconds or an RFC 3339 offset, or compact
202610051200 (2026100512 means on the hour).
|
| view | current | forecast |
current works for every product; forecast only for
taf and sigmet (metar and station
answer 404 unsupported_view).
|
curl -H "Authorization: Bearer $WEATHERLABS_API_KEY" \ "$BASE/v1/metar/latest/current?ids=EGLL,KJFK"
Location: choose exactly one
| Param | Type | Description |
|---|---|---|
| ids | string |
Comma-separated ICAO station ids, e.g. EGLL,KJFK (up to 100,
case-insensitive). Results come back in request order; ids with nothing to return are
listed in missing. For sigmet, see
SIGMET lookups by id.
|
| lat, lon | float |
Everything within radius_km of the point, nearest first, each with
distance_km. Latitude −90…90, longitude −180…180.
|
| radius_km | float |
With lat/lon: default 50, max 500. For sigmet the
default is 0, meaning SIGMETs whose polygon contains the point (or the station, with
ids); a radius widens the search.
|
| limit | int |
Maximum results. With lat/lon: default 10, max 100
(sigmet: default and max 1000). With bbox, no location, or
sigmet ids: default and max 1000. Other ids lookups
return one result per id. truncated: true in the response means more matched.
|
| bbox | string |
west,south,east,north in degrees. west > east
crosses the antimeridian. At most 40° in each direction, except for sigmet.
|
sigmet alone may omit the location to list every SIGMET and AIRMET worldwide, and
it also accepts types= (SIGMET, AIRMET) and
hazards= (CONVECTIVE, TS, TURB,
ICE, VA, TC, …) as comma-separated filters.
Time: latest or a past instant
latest reads the newest snapshot: as_of says when it was built and
at is the moment the view was resolved for (now). Any other when
reads the archive as it stood at that time; a METAR request looks back up to three hours for
each station's newest report. The archive starts when ingestion began, so earlier times return
404 no_history, and a time in the future is 400 invalid_time.
Response
Every product shares one envelope. Reports are listed under reports; the
station product lists stations instead.
{"product":"metar","view":"current","requested_time":"latest",
"at":"2026-10-05T18:56:59Z","as_of":"2026-10-05T18:50:36Z",
"snapshot":"snapshots/metar/20261005T185036Z-fd265403.wlav",
"source":"NOAA Aviation Weather Center",
"count":2,"truncated":false,"missing":[],
"reports":[{…},{…}]}
requested_time echoes latest or the parsed time;
snapshot names the data file for latest and is null for
archive reads. Units: wind in knots and degrees true
(wind_variable is true for VRB); visibility as
{statute_mi, km, at_least}, where at_least marks AWC's “10+” and
P6SM; temperatures in °C; pressures in hPa; cloud bases in feet AGL; SIGMET levels in feet
MSL; precipitation in inches, as reported. SIGMET polygons are GeoJSON-style
[lon, lat] rings. raw always carries the original report text.
Billing & caching
Each report (or station) returned bills 256 bytes; a response with no results
bills nothing. latest responses are cached briefly, like other
latest URLs; archive responses for periods that can no longer change are served
immutable.
Errors
| Code | HTTP | Meaning & fix |
|---|---|---|
| invalid_location | 400 |
No location, or more than one of ids, lat/lon and
bbox; lat without lon; coordinates or
radius_km out of range.
|
| invalid_ids | 400 |
More than 100 ids, or an id that is not 3–8 letters and digits (up to 16 for
sigmet, whose report ids are 16 characters).
|
| invalid_bbox | 400 |
Not four numbers west,south,east,north, south not below north, or wider than
40°.
|
| invalid_limit | 400 | limit outside the range for the location form. |
| invalid_parameter | 400 |
types= or hazards= on a product other than sigmet.
|
| invalid_time | 400 |
when is not latest or a time, or the time is in the future.
|
| unsupported_view | 404 |
metar and station have no forecast view; use
current.
|
| route_not_found | 404 | A view other than current or forecast. |
| no_history | 404 | The archive does not reach back to that time. |
| aviation_unavailable | 503 | No data has been published yet. Retry shortly. |
⚠ Not a flight briefing
Reports come from NOAA/NWS. WeatherLabs decodes them but does not quality-control them, and they must not be the sole basis for operational flight decisions. Use an official briefing service.
GET /v1/metar/{when}/current
Each station's newest METAR or SPECI observed at or before when, looking back up
to three hours. age_minutes is the report's age at when, and
stale (with state: "stale") is set once it is more than 90 minutes
old. Each report also carries flight_category (VFR, MVFR, IFR, LIFR), QNH,
sea-level pressure and its 3-hour tendency, decoded weather, cloud layers, and the 6- and
24-hour temperature and precipitation groups when the station sends them.
curl -H "Authorization: Bearer $WEATHERLABS_API_KEY" \ "$BASE/v1/metar/latest/current?ids=EGLL,KJFK" # → {"product":"metar","view":"current",…,"count":2,"missing":[], # "reports":[{"station":"EGLL","report_type":"METAR", # "raw":"METAR EGLL 051820Z AUTO 26009KT 240V310 9999 NCD 20/14 Q1022", # "observed_at":"2026-10-05T18:20:00Z","age_minutes":36,"stale":false,"state":"available", # "lat":51.477,"lon":-0.461,"elevation_m":26.0,"temperature_c":20.0,"dewpoint_c":14.0, # "wind_direction_deg_true":260,"wind_variable":false,"wind_kt":9,"gust_kt":null, # "visibility":{"statute_mi":6.0,"km":9.7,"at_least":true}, # "clouds":[{"cover":"CLR","base_ft_agl":null,"cloud_type":null}], # "weather":null,"qnh_hpa":1022.0,"flight_category":"VFR",…}, …]} # the five nearest stations within 40 km of Heathrow, each with distance_km curl -H "Authorization: Bearer $WEATHERLABS_API_KEY" \ "$BASE/v1/metar/latest/current?lat=51.47&lon=-0.45&radius_km=40&limit=5" # what EGLL was reporting at 12:00Z, from the archive curl -H "Authorization: Bearer $WEATHERLABS_API_KEY" \ "$BASE/v1/metar/2026-10-05T12:00Z/current?ids=EGLL"
GET /v1/taf/{when}/current | forecast
Each station's newest TAF issued at or before when that has not yet expired — the
same TAF NOAA's own API returns. forecast returns it with every group, in time
order, under groups, exactly as NOAA decodes them. Each group carries
from, to, change (PREVAILING,
FM, BECMG, TEMPO or PROB),
probability, becoming_at (when a BECMG change completes), wind,
visibility, cloud, weather, wind shear, turbulence and icing.
current resolves the TAF at when into two parts, both made of those
same NOAA groups, unaltered:
-
prevailing: the base, FM or BECMG group with the latest start at or beforewhen. NOAA ends each of these groups where the next begins, and a BECMG group already carries every element it leaves unchanged. While a BECMG change is still in progress (before itsbecoming_at), the group it changes from stays prevailing. -
active: the TEMPO and PROB groups in effect, plus any BECMG still in progress.
prevailing is null only when the TAF is not valid at
when. NOAA's feed carries just each station's newest TAF, and one issued ahead of
its validity (typically an hour before) has nothing in force yet: it comes back with
state: "upcoming" and its valid_from. Otherwise state
is available.
curl -H "Authorization: Bearer $WEATHERLABS_API_KEY" \ "$BASE/v1/taf/latest/current?ids=EGLL" # TAF EGLL 060453Z 0606/0712 VRB03KT 0500 FG OVC001 … BECMG 0607/0610 9999 NSW FEW005 # PROB30 TEMPO 0610/0612 BKN008 … resolved at 10:30Z on the 6th: # → {"product":"taf","view":"current",…,"reports":[{"station":"EGLL","state":"available", # "valid_from":"2026-10-06T06:00:00Z","valid_to":"2026-10-07T12:00:00Z","at":"…", # "prevailing":{"change":"BECMG","from":"2026-10-06T07:00:00Z","to":"2026-10-07T08:00:00Z", # "becoming_at":"2026-10-06T10:00:00Z","wind_variable":true,"wind_kt":3, # "visibility":{"statute_mi":6.0,"km":9.7,"at_least":true},"weather":"NSW", # "clouds":[{"cover":"FEW","base_ft_agl":500,…}],…}, # "active":[{"change":"TEMPO","probability":30,"from":"2026-10-06T10:00:00Z", # "to":"2026-10-06T12:00:00Z","clouds":[{"cover":"BKN","base_ft_agl":800,…}],…}]}]}
curl -H "Authorization: Bearer $WEATHERLABS_API_KEY" \ "$BASE/v1/taf/latest/forecast?lat=51.47&lon=-0.45&radius_km=60" # → {"product":"taf","view":"forecast",…,"reports":[{"station":"EGLL","distance_km":1.1,…, # "groups":[{"change":"PREVAILING","from":"…","to":"…",…}, # {"change":"TEMPO","from":"…","to":"…",…}, …]}, …]}
GET /v1/sigmet/{when}/current | forecast
US SIGMETs and AIRMETs plus international SIGMETs. current returns those valid at
when; forecast adds those starting later, and each carries
status (active or upcoming). A report has its
type, hazard and qualifier, FIR, validity, base and top
in feet MSL, movement, and its area as a polygon of [lon, lat]
points. Location is optional here: give ids (below),
lat/lon for the SIGMETs over a point, bbox for an area,
or nothing for the whole world. Like NOAA's own SIGMET feeds, a lookup returns every match:
there is no per-id cap, and limit (default and max 1000) only bounds the total.
SIGMET lookups by id. ids= takes any mix of:
-
station ids, e.g.
EGLL: the SIGMETs whose area contains the station;radius_km(max 500) widens the search around it; - FIR ids, e.g.
RJJJ: every SIGMET that FIR has issued; -
SIGMET ids: the 16-character
idof a report, to follow one SIGMET.
Each report lists the requested ids it matched in matched_ids.
missing lists ids that are none of these, or a FIR with no SIGMET on file; a
known station with no SIGMET over it is simply absent from matched_ids.
| Param | Type | Description |
|---|---|---|
| types | string | Comma-separated report types: SIGMET, AIRMET. |
| hazards | string |
Comma-separated hazards: CONVECTIVE, TS, TURB,
ICE, VA, TC, …
|
curl -H "Authorization: Bearer $WEATHERLABS_API_KEY" \ "$BASE/v1/sigmet/latest/current?bbox=-130,20,-60,55&hazards=CONVECTIVE" # → {"product":"sigmet","view":"current",…,"reports":[{"id":"7251a8240369596d","type":"SIGMET", # "origin":"domestic","hazard":"CONVECTIVE","qualifier":"SEV","status":"active", # "valid_from":"2026-10-06T09:55:00Z","valid_to":"2026-10-06T11:55:00Z","top_ft_msl":45000, # "movement":{"direction_deg_true":270,"direction":null,"kt":15}, # "polygon":[[-82.689,30.371],[-82.68,28.243],…],"raw":"…"}, …]} # the SIGMETs over Heathrow and New Orleans, and everything the Fukuoka FIR has issued curl -H "Authorization: Bearer $WEATHERLABS_API_KEY" \ "$BASE/v1/sigmet/latest/forecast?ids=EGLL,KMSY,RJJJ" # → {…,"count":4,"missing":[],"reports":[{"id":"…","fir":"RJJJ","hazard":"TC","status":"active", # "matched_ids":["RJJJ"],…}, …]} # a station id shows up in matched_ids only while a SIGMET covers that station # tropical-cyclone and volcanic-ash SIGMETs worldwide, in force or coming up curl -H "Authorization: Bearer $WEATHERLABS_API_KEY" \ "$BASE/v1/sigmet/latest/forecast?hazards=TC,VA"
GET /v1/station/{when}/current
Reporting-station metadata: ICAO id, iata, faa and
wmo ids, name, position, elevation_m,
state, country, and site_types (the reports it issues,
e.g. METAR and TAF). Use it to find the airports near a point before asking for their reports.
Results are listed under stations.
curl -H "Authorization: Bearer $WEATHERLABS_API_KEY" \ "$BASE/v1/station/latest/current?lat=40.64&lon=-73.78&radius_km=30" # → {"product":"station","view":"current",…,"stations":[{"id":"KJFK","iata":"JFK","faa":"JFK", # "wmo":"74486","name":"New York/JF Kennedy Intl","lat":40.6392,"lon":-73.7639, # "elevation_m":3.0,"distance_km":1.4,"state":"NY","country":"US", # "site_types":["METAR","TAF"]}, …]}
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": "your applications' monthly data allowance is used up — resets at 2026-11-01T00:00:00Z; add an application for a larger allowance", "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. |
| key_revoked | 401 | Key was revoked. Generate a new one and update your client. |
| not_entitled | 403 | No application on the account grants that endpoint, variable or lead hour. The message names the application that does. |
| no_application | 403 | The account holds no live application. Subscribe to one on your Account page. |
| quota_exceeded | 429 |
Data allowance used up. Wait for Retry-After/reset, or add an application. No
overage is billed.
|
| payment_past_due | 403 | A renewal payment failed and the grace period ended. Update your card under Payments & invoices on your Account page to restore access. |
| subscription_canceled | 403 | Subscription cancelled. Resubscribe from your Account page to restore access. |
| account_suspended | 403 | Account suspended (e.g. AUP breach or unpaid). Contact support. |
| model_forbidden | 403 | No application on the account grants the requested model. The message names one that does. |
| 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 (an unknown name such as
temperature_123hpa), 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. The aviation routes have their own request-shape codes, listed under
Aviation observations.
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.
Variable names. Variable names changed in October 2026 (see
Variables & names). The old names, level= and the
bare gefs mean stay accepted until 30 November 2026; every response that used
one carries Deprecation: true and
Sunset: Mon, 30 Nov 2026 00:00:00 GMT, so you can find the calls to update. After
that date they return 400.
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 on the 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.