Base URL: https://api.wxalerts.org
A model sounding for any point: temperature, dewpoint, height and wind on all 39 HRRR pressure levels, and the surface beneath them, for every forecast hour of the newest run. Raw numbers, for computing CAPE, shear and helicity yourself or drawing your own Skew-T and hodograph.
GET /v1/forecast/sounding?lat=35.2&lon=-97.4
GET /v1/forecast/sounding?q=norman+ok&hours=6
GET /v1/forecast/sounding?lat=35.2&lon=-97.4&valid_from=<now>&valid_to=<now + 6 h>&wind=uv
Needs an API key (weather:read), and is served only
under /v1; the bare path answers 401. Browsers block calls from other
websites (CORS allows only wxalerts.org), so call it from a server, script or
app. It also has its own limit, 5 calls in any 60 seconds per account,
shared by every key on the account and with
raw model data, tighter than the
rest of the API because one call can carry 49
hours of a full column. It applies on top of your key’s overall limit. Past it
you get 429 with a Retry-After. A run’s hours only ever arrive, never
change, so cache what you have and ask only for the hours you are missing
(valid_from).
| parameter | |
|---|---|
lat, lon |
the point. Or q (a place name) or location_id, as on /forecast. None of them is a 422; a name or id that matches nothing is a 404 |
hours |
1 to 49: at most this many hours, the earliest on hand first (after valid_from/valid_to). Default: every hour on hand |
valid_from, valid_to |
ISO 8601 limits on valid time, inclusive. A time without an offset is UTC. They filter the newest run, so a window outside it (in the past, or past its last hour) returns an empty hours list, not an error |
wind |
both (default), uv (only the u and v components) or speeddir (only speed and direction). The fields not asked for are left out, not sent as null |
What a response holds
{
"server_time": "2026-11-09T19:08:10Z",
"location": { "name": null, "display_name": null, "state": null, "lat": 35.2, "lon": -97.4 },
"grid_point": { "i": 902, "j": 407, "lat": 35.2076, "lon": -97.40682, "resolution_km": 3.0 },
"run": { "model": "hrrr", "cycle": "2026-11-09T18:00:00Z",
"hours_available": 9, "hours_expected": 49, "complete": false },
"levels_hpa": [1000.0, 975.0, 950.0, "...", 100.0, 75.0, 50.0],
"units": { "pressure_hpa": "hPa", "height_m": "m (geopotential)", "temperature_k": "K", "...": "..." },
"hours": [
{
"valid_time": "2026-11-09T18:00:00Z",
"forecast_hour": 0,
"surface": {
"temperature_2m_k": 292.6, "dewpoint_2m_k": 283.2, "pressure_hpa": 979.1,
"u10_ms": 1.4, "v10_ms": 3.9, "wind_speed_10m_ms": 4.1, "wind_direction_10m_deg": 200.0,
"u80_ms": 1.7, "v80_ms": 4.4, "wind_speed_80m_ms": 4.7, "wind_direction_80m_deg": 201.0,
"pbl_height_m": 785.0
},
"levels": [
{ "pressure_hpa": 1000.0, "below_ground": true, "height_m": null, "temperature_k": null,
"dewpoint_k": null, "u_ms": null, "v_ms": null, "wind_speed_ms": null, "wind_direction_deg": null },
{ "pressure_hpa": 975.0, "below_ground": false, "height_m": 403.0, "temperature_k": 291.0,
"dewpoint_k": 283.1, "u_ms": 1.8, "v_ms": 4.2, "wind_speed_ms": 4.6, "wind_direction_deg": 203.0 },
"..."
]
},
"..."
]
}
location echoes your point. It is named only when you asked by q or
location_id; given coordinates, the name fields are null. units names
the unit of every field, and is the same in every response.
- Levels, bottom up: 1000 to 125 hPa every 25, then 100, 75 and 50. That is the native HRRR column, so the lowest kilometre has its full detail.
- Units are SI: K, m, m/s. Pressure is hPa. Heights are geopotential
metres above sea level; the surface block’s
pbl_height_mis above ground. - Below ground. Every hour has all 39 levels, so position k is always the
same pressure. A level whose pressure exceeds that hour’s surface pressure is
marked
below_ground: truewith every valuenull: the model extrapolates under terrain and those numbers describe no air. - Winds are degrees from true north, the direction FROM. HRRR publishes them relative to its own grid; they are turned before they are stored.
- Precision: 0.1 K and 0.1 m/s, heights to 1 m, wind directions to 1 degree, surface pressure to 0.01 hPa.
Which run, and how much of it
The newest HRRR run with at least its first two hours on hand. Every run goes
to 18 hours; the 00, 06, 12 and 18 UTC runs go to 48. A run fills in over
roughly half an hour after it is published, so a response can be partial:
hours_available against hours_expected (19, or 49 for a long run), and
complete once every hour is in. Hours do not always arrive in order, so the
list can have a gap (f02 then f04) that fills later; read forecast_hour and
valid_time rather than assuming one step per position. Ask again later for
the rest.
The grid is the HRRR’s own 3 km CONUS grid, not averaged. grid_point is the
cell your point fell in (i and j count from the grid’s south-west
corner). A point outside the grid is a 404; no run on hand is a 503.
Responses carry Cache-Control: max-age=300.

