Base URL: https://api.wxalerts.org
Every storm cell NOAA’s ProbSevere is tracking over the CONUS, the rotations found inside them, where each has been, and where its fitted motion puts it next. This is the data behind the storm layer on the live map, which is just a client of these routes.
GET /storms/active/geojson
GET /storms/active/geojson?lead=45&profile=tight&west=-103&south=33&east=-94&north=40
GET /storms/{storm_id}/track?minutes=60
GET /storms/{storm_id}/forecast-track?lead=30
GET /storms/{storm_id}/cone?lead=30&profile=wide
No key needed for these four. They share the per address limit. Browsers block calls from other websites (CORS allows only wxalerts.org), so call them from a server, script or app.
GET /v1/storms/active?min_prob_tor=10
GET /v1/storms/{storm_id}
GET /v1/storms/couplets/{couplet_id}/cone?lead=15
Needs an API key (storms:read) for these three, and
they are served only under /v1. Without the prefix they answer 401
api_key_required.
This page is the wire format. What the shapes mean, how far to trust a cone, and why the tornado and severe numbers are on different scales is in Storm analysis, starting with Reading the map.
Ids
storm_id is WxAlerts’ own id for a storm. ProbSevere renames a cell when it
merges or splits, and the ingest stitches those back into one storm; every
ProbSevere ID it has worn is in feed_ids. Use storm_id for anything you keep.
couplet_id is the id of a tracked rotation, which is linked to a cell
(storm_id, possibly null) but tracked on its own.
The per storm routes take any id ever issued, not only active ones, so a client holding an id from earlier still gets an answer.
Freshness
Every collection response carries feed_valid_time, the newest ProbSevere scan
ingested, and feed_age_seconds. The feed publishes about every two minutes and
lands two to three and a half minutes after its valid time, so an age of 100 to
220 seconds is normal. Check it: an empty response looks the same whether the
sky is quiet or the feed stopped. A storm drops out of the active set once it
has not been seen for 15 minutes. Responses are cached for 30 seconds
(Cache-Control: public, max-age=30); polling faster gains nothing.
GET /storms/active/geojson
Everything a map layer needs in one FeatureCollection: cells, their tracks, forecast lines and cones, and the rotations with theirs. One request rather than several, so a cone never belongs to a cell that has since moved.
| parameter | |
|---|---|
west, south, east, north |
a viewport, in degrees. All four or none (422 otherwise), and west < east, south < north. A cell is in view when its polygon touches the box, not only its center |
min_prob_tor |
only cells at or above this ProbTor (0 to 100) |
min_prob_severe |
only cells at or above this ProbSevere (0 to 100) |
alerted_only |
only cells currently over the alert threshold. Default false |
limit |
at most this many cells, 1 to 500. Default 500 |
lead |
cone and forecast line lead, in minutes, 0 to 60. Default 30. 0 omits cones and forecast lines |
profile |
wide (default) or tight. See Cone profiles. Anything else is 422 |
cones |
false omits cone and turn-cone features. Default true |
forecast |
false omits forecast-track and couplet-forecast features. Default true |
track_minutes |
history behind each cell, 0 to 180. Default 60. 0 omits track features |
detail_min_prob_tor |
tracks, cones and forecast lines are built only for cells at or above this ProbTor. Default 10. Cells themselves are always returned. Set 0 for a cone on every cell |
couplets |
false omits all four couplet kinds. Default true |
couplet_lead |
lead for couplet cones and forecast lines, 0 to 25. Default 15. Drawn at no more than 20: 21 to 25 are accepted and drawn at 20. 0 omits them |
emerging_couplets |
also return rotations that have not yet held for three scans. Default false |
Cells are ordered by ProbTor, then ProbSevere, highest first, so limit keeps
the ones that matter.
{
"type": "FeatureCollection",
"server_time": "2026-11-09T19:07:28.027599Z",
"feed_valid_time": "2026-11-09T19:04:34Z",
"feed_age_seconds": 172.44656,
"lead_minutes": 45,
"profile": "tight",
"features": [
{
"type": "Feature",
"geometry": { "type": "Polygon", "coordinates": [[[-96.88402, 25.28124], "...", [-96.88402, 25.28124]]] },
"properties": { "kind": "cone", "storm_id": 297397, "lead_minutes": 45, "profile": "tight",
"prob_tor": 1, "prob_severe": 3, "alerted": false }
},
{
"type": "Feature",
"geometry": { "type": "Polygon", "coordinates": [[[-96.98, 25.87], [-96.92, 25.87], "...", [-96.98, 25.87]]] },
"properties": {
"kind": "cell", "storm_id": 297397, "tier": null,
"prob_tor": 1, "prob_severe": 3, "prob_hail": 0, "prob_wind": 1,
"peak_prob_tor": 1, "peak_prob_severe": 6,
"speed_kmh": 20.2, "cone_speed_kmh": 24.4, "bearing_deg": 49,
"alerted": false, "alerted_at": null,
"valid_time": "2026-11-09T19:04:34Z", "first_seen": "2026-11-09T17:32:36Z",
"scan_count": 47, "feed_ids": [81531], "lon": -96.922616, "lat": 25.585429,
"max_llaz": 0.008, "p98_llaz": 0.006,
"couplet_lon": -96.98750000000001, "couplet_lat": 25.8425, "couplet_shear": 0.00800000037997961
}
},
{
"type": "Feature",
"geometry": { "type": "LineString", "coordinates": [[-96.9125, 25.7825], [-96.84111, 25.76027]] },
"properties": {
"kind": "couplet-forecast", "couplet_id": 96819, "storm_id": 297397, "state": "confirmed",
"lead_minutes": 15, "speed_kmh": 30.3, "bearing_deg": 109,
"ticks": [{ "minutes": 15, "lon": -96.84111, "lat": 25.76027, "at": "2026-11-09T19:19:34Z" }],
"shear": 0.006000000052154064, "alerted": false
}
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-95.1525, 29.7375] },
"properties": {
"kind": "couplet", "couplet_id": 96829, "tier": null, "storm_id": 297366, "state": "emerging",
"shear": 0.00800000037997961, "peak_shear": 0.00800000037997961, "mid_shear": 0.003000000026077032,
"rqi": 1.0, "prob_tor": 0, "diameter_km": 0.82, "vrot_ms": 3.3, "stacked": false,
"debris_confirmed": null, "rho_hv": null, "confirmed_at": null,
"speed_kmh": null, "bearing_deg": null, "alerted": false, "alerted_at": null, "scan_count": 2,
"first_seen": "2026-11-09T19:02:37Z", "last_seen": "2026-11-09T19:04:34Z",
"lon": -95.1525, "lat": 29.7375
}
},
{
"type": "Feature",
"geometry": { "type": "LineString",
"coordinates": [[-96.92262, 25.58543], [-96.87691, 25.62156], [-96.83118, 25.65769], [-96.78543, 25.69379]] },
"properties": {
"kind": "forecast-track", "storm_id": 297397, "lead_minutes": 45,
"speed_kmh": 20.2, "cone_speed_kmh": 24.4, "bearing_deg": 49,
"ticks": [
{ "minutes": 15, "lon": -96.87691, "lat": 25.62156, "at": "2026-11-09T19:19:34Z" },
{ "minutes": 30, "lon": -96.83118, "lat": 25.65769, "at": "2026-11-09T19:34:34Z" },
{ "minutes": 45, "lon": -96.78543, "lat": 25.69379, "at": "2026-11-09T19:49:34Z" }
],
"prob_tor": 1, "prob_severe": 3, "alerted": false
}
},
{
"type": "Feature",
"geometry": { "type": "LineString", "coordinates": [[-97.12435, 25.80059], [-97.06814, 25.68414], "...", [-96.92262, 25.58543]] },
"properties": {
"kind": "track", "storm_id": 297397, "points": 29, "minutes": 60, "prob_tor": 1, "alerted": false,
"jumps": [0, 1, 27, 28],
"times": ["2026-11-09T18:08:37Z", "2026-11-09T18:10:33Z", "...", "2026-11-09T19:04:34Z"]
}
}
]
}
That example was requested with detail_min_prob_tor=0&emerging_couplets=true
on a quiet afternoon, so the low numbers are real.
Feature kinds
Every feature carries properties.kind. Features come in draw order: cones
first, then cells, then the couplets with their cones, lines and tracks, then
the cell forecast lines and tracks. Render in sequence and the lines land on top.
| kind | geometry | one per | when |
|---|---|---|---|
cell |
Polygon | cell in view | always |
cone |
Polygon | detailed cell with a motion fit | cones, lead > 0 |
turn-cone |
Polygon | detailed cell whose track and HRRR storm motion disagree by more than 40° | as cone |
forecast-track |
LineString | detailed cell with a motion fit | forecast, lead > 0 |
track |
LineString | detailed cell with two or more scans in track_minutes |
track_minutes > 0 |
couplet |
Point | rotation in view | couplets |
couplet-cone |
Polygon | rotation that has alerted and has a motion fit | couplet_lead > 0 |
couplet-forecast |
LineString | confirmed rotation with a motion fit (not coasting) |
forecast, couplet_lead > 0 |
couplet-track |
LineString | rotation with two or more positions in the track window | couplets |
“Detailed” means at or above detail_min_prob_tor. A cell or rotation with no
motion fit yet (its first scans) gets no cone or forecast line rather than a
circle that would claim it is standing still.
A turn-cone is part of the same cone, reaching along the HRRR storm motion
heading for a storm that may turn. Draw it like the cone, a shade quieter,
never as a second forecast. It is a separate feature so a client that does not
know about it still draws the main cone. What the shapes mean on the map:
The four shapes and
The second, solid cone.
Cell properties
| field | |
|---|---|
prob_tor, prob_severe, prob_hail, prob_wind |
ProbSevere v3’s probabilities on the newest scan, 0 to 100: tornado, any severe hazard, severe hail, severe wind |
peak_prob_tor, peak_prob_severe |
the highest each has reached over the storm’s life |
tier |
the WXA/SA tier this cell’s numbers justify, or null for none. See The tier object |
alerted |
over the notification threshold after hysteresis: fires at ProbTor 30 or more, clears only below 15 held for three scans. alerted_at is when it last fired |
speed_kmh, bearing_deg |
the storm’s motion, fitted from its own centroid history. Bearing is degrees clockwise from true north, the direction it is moving TOWARD. null on a first scan, never 0 |
cone_speed_kmh |
the speed the cone and forecast line are drawn at: the track speed blended with the nearest HRRR storm motion. null where HRRR had no sample, and the cone is drawn at speed_kmh |
max_llaz, p98_llaz |
low level azimuthal shear over the cell, maximum and 98th percentile, in s⁻¹ (0.008 is 8 m/s of shear across a kilometre). An input to ProbTor, so it explains a high ProbTor rather than corroborating it, and it reads weaker far from the radar |
couplet_lon, couplet_lat, couplet_shear |
where the strongest shear inside the polygon is, and how much. A median 5 km from the centroid |
lon, lat |
the cell’s centroid |
valid_time |
the scan this state came from. first_seen, scan_count and feed_ids describe its history |
Couplet properties
A couplet is a rotation found in the MRMS azimuthal shear grid and tracked scan to scan, independent of whichever cell holds it. How they are found and gated: Rotation detection.
| field | |
|---|---|
state |
emerging (fewer than three scans, returned only with emerging_couplets=true), confirmed, or coasting (not found on the newest scan, held in case it comes back) |
shear, peak_shear |
low level azimuthal shear now and its peak, s⁻¹ |
mid_shear, stacked |
mid level shear above it, and whether the rotation is vertically stacked. false means shallow, null means the mid level grid was missing |
vrot_ms, diameter_km |
rotational velocity across the couplet in m/s, and its width |
rqi |
radar quality index at the rotation, 0 to 1. Rotations below 0.5 are not returned, because the mosaic cannot see the low levels there |
prob_tor |
the parent cell’s ProbTor, as environmental context |
debris_confirmed, rho_hv, confirmed_at |
the Level II debris badge. true is debris seen, false is looked and found none, null is could not look. Do not read null as no. confirmed_at stays set once it has been. See The debris badge |
alerted, alerted_at, tier |
whether this rotation is alerting, and its tier. Only an alerting rotation carries a tier |
speed_kmh, bearing_deg |
the rotation’s own motion, which often differs from its cell’s |
last_seen |
the scan this position comes from. For a coasting rotation it is older than feed_valid_time |
Lines, cones and tracks
- Forecast lines (
forecast-track,couplet-forecast) start at the object and carryticks, evenly spaced about every 15 minutes and ending exactly on the lead (20 gives one tick at 20; 45 gives 15, 30, 45). Each tick hasminutesfrom the scan, itslon/lat, andat, the absolute time it stands for. Useatfor anything that quotes an arrival. The line is the middle of the cone, not a promise. The forecast track. - Cones (
cone,turn-cone,couplet-cone) carrylead_minutes,profile, and the owner’sprob_tor,prob_severe(cells) orstate,shear(couplets) so you can color them without a lookup. - Tracks (
track) run oldest first.timesgives each coordinate’s scan time, andjumpslists the indexes where the centroid moved more than 10 km in one scan because the polygon absorbed a neighbour: break the line there rather than draw a storm teleporting.couplet-trackcarriespoints,shearandalerted, with no times.
The tier object
tier on a cell or couplet is null or the WXA/SA tier, served so that every
client renders the same label and the same sentence:
| field | |
|---|---|
rung |
an integer that orders tiers: 40 Thunderstorm Risk, 50 Thunderstorm Detected, 70 and 75 Tornado Risk, 80 Tornado Detected, 85 Tornado Detected with debris confirmed |
name |
the label to show |
hazard |
thunderstorm or tornado |
claim |
the sentence this tier may honestly make, for example “this storm is rotating and is capable of producing a tornado” |
measured |
what the threshold is, in words, or null |
Render claim rather than writing your own. Where the tiers come from:
The ladder.
Cone profiles
profile picks how much area a cone spends for how much certainty. Both are
measured against where storms actually went.
| profile | cells | rotations |
|---|---|---|
wide (default) |
about 95% of storms inside the cone at 60 minutes | about 95% capture |
tight |
about 92%, for roughly 29% less area | about 90%, for roughly two thirds of the area |
Area is the real cost: a wide cell cone grows about five times between 10 and 60 minutes of lead, and everyone inside it is being told something. A cell cone has no rear margin; a rotation’s cone does, because rotations die and re-form upstream. Rotation cones stop at 20 minutes because past that they grow larger than the cell cone they are meant to narrow. The reasoning, with numbers, is in Accuracy and The rotation’s own cone.
GET /storms/{storm_id}/track
Where one storm has been: every scan in the last minutes (1 to 180, default
60), oldest first, across every ProbSevere ID it has worn. The window is
counted back from now, so a storm that ended long ago returns an empty list.
An unknown id is also an empty list, not a 404.
{
"storm_id": 297397,
"points": [
{ "valid_time": "2026-11-09T19:02:37Z", "lon": -96.927201, "lat": 25.479222,
"prob_tor": 0, "prob_severe": 2, "prob_hail": 0, "prob_wind": 0,
"speed_kmh": 20.236125946044922, "bearing_deg": 48.74721145629883,
"feed_id": 81531, "jumped": true },
"..."
],
"geometry": { "type": "LineString", "coordinates": [[-96.91801, 25.75726], "...", [-96.92262, 25.58543]] }
}
geometry is null until there are two points. jumped marks a scan whose
centroid moved more than 10 km: a real observation, left out of the motion fit.
GET /storms/{storm_id}/forecast-track
The forecast line on its own, for any storm, including the ones under
detail_min_prob_tor that the bulk feed draws no line for. lead is 1 to 60,
default 30. There is no profile: both profiles share this line.
{
"type": "Feature",
"geometry": { "type": "LineString", "coordinates": [[-96.92262, 25.58543], [-96.86167, 25.63361]] },
"properties": {
"kind": "forecast-track", "storm_id": 297397, "lead_minutes": 20,
"speed_kmh": 20.2, "cone_speed_kmh": 24.4, "bearing_deg": 49, "valid_time": "2026-11-09T19:04:34Z",
"ticks": [{ "minutes": 20, "lon": -96.86167, "lat": 25.63361, "at": "2026-11-09T19:24:34Z" }]
}
}
The line is drawn at cone_speed_kmh (the track speed blended with the HRRR
storm motion, or the track speed alone where there is none); speed_kmh is
the raw track speed. Read positions and times from ticks rather than
multiplying either.
GET /storms/{storm_id}/cone
One storm’s cone. lead 1 to 60 (default 30), profile wide or tight.
{
"type": "Feature",
"geometry": { "type": "Polygon", "coordinates": [["...", "..."]] },
"properties": {
"kind": "cone", "storm_id": 297397, "lead_minutes": 30, "profile": "wide",
"steered": true, "speed_kmh": 20.2, "cone_speed_kmh": 24.4, "bearing_deg": 49,
"area_km2": 4407, "turn_cone": null
}
}
steered is true when the cone is drawn at the HRRR blended speed with the
steered profile. area_km2 is the main cone’s area. turn_cone is the turn
lobe’s Polygon geometry when the storm has one, else null; it overlaps the
main cone, so the two areas do not add.
GET /v1/storms/active
The same cells as the GeoJSON route, without geometry, as plain JSON. Takes
west, south, east, north, min_prob_tor, min_prob_severe,
alerted_only and limit exactly as above. Needs an API key
(storms:read).
{
"server_time": "2026-11-09T19:07:20.745325Z",
"feed_valid_time": "2026-11-09T19:04:34Z",
"feed_age_seconds": 166.645814,
"storms": [
{
"id": 297397,
"first_seen": "2026-11-09T17:32:36Z", "last_seen": "2026-11-09T19:04:34Z",
"valid_time": "2026-11-09T19:04:34Z",
"prob_severe": 3, "prob_hail": 0, "prob_wind": 1, "prob_tor": 1,
"peak_prob_tor": 1, "peak_prob_severe": 6,
"speed_kmh": 20.236125946044922, "bearing_deg": 48.74721145629883,
"cone_speed_kmh": 24.379592895507812,
"steer_u_kmh": 6.4435505867004395, "steer_v_kmh": 27.785707473754883,
"cross_track_km": 55.9759635925293, "along_track_km": 38.771297454833984,
"scan_count": 47, "feed_ids": [81531],
"alerted": false, "alerted_at": null,
"lon": -96.922616, "lat": 25.585429
},
"..."
]
}
Beyond the cell properties above: steer_u_kmh and steer_v_kmh are the HRRR
storm motion sampled at the storm (east and north components, km/h), and
cross_track_km and along_track_km are the storm’s measured width across and
along its motion, which set the cone’s starting width. Values here are
unrounded; the GeoJSON route rounds motions to 0.1 km/h and whole degrees.
GET /v1/storms/{storm_id}
One storm in full: everything in /v1/storms/active plus the polygon and the
diagnostics behind the score. 404 for an unknown id. Needs an API key
(storms:read).
{
"id": 297397,
"...": "every field of /v1/storms/active",
"geometry": { "type": "Polygon", "coordinates": [["...", "..."]] },
"latest_feed_id": 81531,
"active": true,
"missing_scans": 0,
"motion_fit_points": 0,
"properties": {
"ID": "81531", "ProbTor": "1", "ProbSevere": "3", "ProbHail": "0", "ProbWind": "1",
"MLCAPE": "728", "MUCAPE": "1262", "EBSHEAR": "30", "SRH01KM": "28", "MESH": "0.06",
"MAXLLAZ": "0.008", "P98LLAZ": "0.006", "FLASH_RATE": "0", "COMPREF": "56.0",
"MOTION_EAST": "2.8", "MOTION_SOUTH": "-3",
"...": "..."
}
}
activeis whether the storm is still in the tracked set;missing_scansis how many scans in a row it has not been found.motion_fit_pointsis how many centroids (of a ten point window) back the motion fit on this scan.0means a merge just cut its history and it is coasting on its previous velocity, which is still the best estimate.propertiesis ProbSevere’s own property set for the newest scan, verbatim, every value a string. ItsMOTION_EASTandMOTION_SOUTHare in no identifiable unit; usespeed_kmhandbearing_deg.
GET /v1/storms/couplets/{couplet_id}/cone
A rotation’s own cone, for any couplet id, whatever its state or alert status
(the bulk feed draws couplet cones only for alerting rotations). lead 1 to
25, default 15, drawn at no more than 20. profile as above. Needs an API
key (storms:read).
{
"type": "Feature",
"geometry": { "type": "Polygon", "coordinates": [["...", "..."]] },
"properties": {
"kind": "couplet-cone", "couplet_id": 96829, "storm_id": 297366,
"state": "confirmed", "alerted": false, "lead_minutes": 15, "profile": "wide",
"shear": 0.00800000037997961, "vrot_ms": 3.3, "diameter_km": 0.82, "prob_tor": 0,
"speed_kmh": 8.3, "bearing_deg": 180, "area_km2": 301
}
}
lead_minutes is the lead actually drawn, so lead=25 comes back as 20.
Errors
| status | when |
|---|---|
401 |
a key-only route without /v1 and a key, or a bad key on any route |
404 |
No such storm. or No such couplet. on the detail, cone and forecast routes |
409 |
the storm or couplet exists but has no motion fit yet, so it has no cone or forecast line. Normal on a first scan; try again next scan |
422 |
a parameter out of range, an unknown profile, three of the four bounding box edges, or an inverted box |
The shared error shapes are on the REST overview.

