Base URL: https://api.wxalerts.org
The headline numbers on this site: how many alerts are in effect, how much lightning the GOES satellites are seeing, how quickly each alert feed delivers, and lightning per day.
GET /stats/summary
GET /stats/latency?hours=24
GET /v1/stats/lightning/daily?days=7&timezone=America/Chicago
summary and latency need no key. Browsers block calls from other websites
(CORS allows only wxalerts.org), so call them from a server, script or app.
They are limited per address as described under
rate limits. lightning/daily needs an
API key (alerts:read) and is served only under /v1;
on the bare path it answers 401 with code api_key_required.
GET /stats/summary
Active alert counts and the GLM lightning window. No parameters. Cached for 15 seconds, which is about the data’s own cadence, so polling faster gets you the same answer.
{
"server_time": "2026-11-09T19:07:18.638865Z",
"alerts": { "active": 370, "with_geometry": 370 },
"lightning": {
"flashes": 1214878,
"window_start": "2026-11-08T19:30:00.010682Z",
"window_end": "2026-11-09T19:46:39.319450Z",
"window_seconds": 87399,
"rate_per_minute": 834.0,
"retention_hours": 24.0,
"covers_full_day": true
}
}
| field | |
|---|---|
alerts.active |
alerts in effect now, the same set every alerts route counts |
alerts.with_geometry |
how many of those have a shape and can be drawn on a map |
lightning.flashes |
GLM flashes between window_start and window_end |
window_start, window_end |
the oldest and newest flash the live store holds |
window_seconds |
the span between them |
rate_per_minute |
flashes over that span, per minute |
retention_hours |
how long the live store is set to keep flashes |
covers_full_day |
whether the window spans at least 24 hours |
The window is whatever the live store holds, measured, not assumed, so read
its length from window_seconds rather than from retention_hours. Branch on
covers_full_day before calling the count a day’s worth. Even when it is
true, the window is rolling and never a calendar day: at 09:00 it reaches back
into the day before. Do not label it “today”. For lightning per day, use
lightning/daily.
GET /stats/latency
How fast each alert feed delivers, measured against the others. The ingest
takes the same hazards from three feeds (nwws, NWWS-OI over XMPP; emwin,
off the GOES broadcast; api, the api.weather.gov CAP feed) and records when
each one first delivered each hazard.
| parameter | |
|---|---|
hours |
window to report over, 1 to 168. Default 24. Arrival records are pruned on a rolling schedule, so asking for more than is kept gives a shorter answer, not an error |
{
"server_time": "2026-11-09T19:07:19.262194Z",
"window_hours": 24,
"issuance_resolution_seconds": 60,
"sources": [
{
"source": "nwws",
"arrivals": 241, "contested": 241, "wins": 236, "win_share": 0.9793, "exclusive": 0,
"median_behind_seconds": 0.0, "p95_behind_seconds": 0.0, "max_behind_seconds": 9229.67,
"median_issuance_lag_seconds": 58.6
},
{
"source": "api",
"arrivals": 242, "contested": 241, "wins": 5, "win_share": 0.0207, "exclusive": 1,
"median_behind_seconds": 30.43, "p95_behind_seconds": 106.25, "max_behind_seconds": 169.78,
"median_issuance_lag_seconds": 94.3
},
"..."
]
}
Sources come most wins first. Two different measurements are in each row, and they are not interchangeable.
Relative, exact to the millisecond, and the fair way to rank the feeds:
| field | |
|---|---|
arrivals |
hazards this feed delivered in the window |
contested |
of those, how many at least one other feed also delivered. Only these can be raced |
wins |
contested hazards this feed delivered first |
win_share |
wins / contested, 0.0 when nothing was contested |
exclusive |
hazards no other feed carried. Coverage, not speed: the reason to keep a feed that never wins |
median_behind_seconds, p95_behind_seconds, max_behind_seconds |
how far behind the fastest feed this one arrived, over contested hazards. null when none were contested |
Absolute, indicative only:
| field | |
|---|---|
median_issuance_lag_seconds |
median time from the NWS issuance stamp to arrival |
issuance_resolution_seconds |
60. The NWS stamps issuance to the minute, so every absolute lag can be off by up to this much. Treat it as an order of magnitude |
max_behind_seconds catches outliers such as a feed that dropped and caught up
later; the median and p95 are the figures to quote. Cached for 60 seconds.
GET /v1/stats/lightning/daily
GLM flash counts per day, with days cut in any time zone. Needs a key
(alerts:read).
| parameter | |
|---|---|
days |
how far back to look, in days from now, 1 to 400. Default 30 |
timezone |
IANA zone to cut days in, e.g. America/Chicago. Default UTC. An unknown zone is 422, never a silent fall back to UTC |
Pick the zone your readers live in. A UTC day ends in the evening across the Americas, so in UTC an evening storm lands on the next day.
{
"server_time": "2026-11-09T19:07:13.249615Z",
"timezone": "America/Chicago",
"days": [
{
"day": "2026-11-09",
"flashes": 238705,
"peak_hourly_flashes": 40928,
"hours_observed": 11,
"mean_energy_j": 2.0169939528190287E-13,
"first_flash": "2026-11-09T06:00:00.096895Z",
"last_flash": "2026-11-09T16:59:59.906157Z"
},
{
"day": "2026-11-08",
"flashes": 1557001,
"peak_hourly_flashes": 166906,
"hours_observed": 24,
"mean_energy_j": 1.5397250051263375E-13,
"first_flash": "2026-11-08T06:00:00.114443Z",
"last_flash": "2026-11-09T05:59:59.953460Z"
},
"..."
]
}
| field | |
|---|---|
day |
the local date in timezone |
flashes |
flashes that day |
peak_hourly_flashes |
the busiest single hour, a sign of how intense the day got that a total hides |
hours_observed |
hours with at least one flash. Under 24 on a quiet day, but also on a day with a gap in the data: check it before reading a low count as fair weather |
mean_energy_j |
mean optical energy per flash at cloud top, in joules. Not a measure of ground hazard |
first_flash, last_flash |
the first and last flash of that day, UTC |
Days come newest first: today and the days - 1 whole days before it, cut at
midnight in timezone, so days=7 is at most seven rows (a day with no
flashes at all has no row). Today’s row is still filling in. Use hours_observed to tell a partial day from a
complete one. Finished days never change; the response is cached for an hour.

