Community. Driven. Weather. Data. | Always Ad-Free | Developer FriendlyChecking statusDiscord
Become a supporter
Sign in
API

Storm cells

The storm cells ProbSevere is tracking, their rotations, where each has been, where it is headed and how sure, as GeoJSON and JSON.


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 carry ticks, 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 has minutes from the scan, its lon/lat, and at, the absolute time it stands for. Use at for 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) carry lead_minutes, profile, and the owner’s prob_tor, prob_severe (cells) or state, shear (couplets) so you can color them without a lookup.
  • Tracks (track) run oldest first. times gives each coordinate’s scan time, and jumps lists 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-track carries points, shear and alerted, 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",
    "...": "..."
  }
}
  • active is whether the storm is still in the tracked set; missing_scans is how many scans in a row it has not been found.
  • motion_fit_points is how many centroids (of a ten point window) back the motion fit on this scan. 0 means a merge just cut its history and it is coasting on its previous velocity, which is still the best estimate.
  • properties is ProbSevere’s own property set for the newest scan, verbatim, every value a string. Its MOTION_EAST and MOTION_SOUTH are in no identifiable unit; use speed_kmh and bearing_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.

May we use Microsoft Clarity to see how pages are used (clicks, scrolling and replays of a visit, with anything you type hidden), plus a random ID linking that to crash reports? Only to find bugs and confusing screens. Never advertising, never sold.