Base URL: https://api.wxalerts.org
One endpoint. Give it a point, and it returns what is happening there now and what is expected, sized for something that renders it.
GET /weather?lat=33.215&lon=-97.133
GET /weather?q=denton+tx
GET /weather?location_id=12345
No key needed. Browsers block calls from other websites (CORS allows only wxalerts.org), so call it from a server, script or app.
{
"server_time": "2026-11-09T19:13:00Z",
"location": {
"name": "Denton", "display_name": "Denton, TX", "state": "TX",
"lat": 33.2174, "lon": -97.1413, "timezone": "America/Chicago"
},
"units": { "system": "us", "fields": { "temperature": { "unit": "F", "label": "°F" } } },
"current": {
"observed_at": "2026-11-09T19:11:00Z",
"sky": "Mostly cloudy", "icon": "cloudy", "is_day": true,
"temperature": 68.0, "dewpoint": 57.9, "relative_humidity": 70.0,
"wind_speed": 1.0, "wind_gust": 1.0,
"wind_direction": 179.0, "wind_direction_cardinal": "S",
"pressure": 30.21, "visibility": 10.0,
"precip_1h": 0.0, "precip_24h": 0.0, "snow_depth": null,
"conditions": null,
"station": {
"station_id": "aprs:KC5CAM-13", "name": null,
"source": "aprs", "distance_m": 2094, "age_seconds": 121
}
},
"hourly": [
{
"time": "2026-11-09T13:00:00-06:00",
"sky": "Mostly cloudy", "icon": "cloudy", "is_day": true, "precip_type": "rain",
"temperature": 70.6, "dewpoint": 59.8, "relative_humidity": 68.9,
"wind_speed": 4.3, "wind_gust": 8.7,
"wind_direction_cardinal": "SSW",
"cloud_cover": 83.1, "precip_probability": 3.0, "precip_amount": 0.0
}
],
"daily": [
{
"date": "2026-11-09", "hours": 11,
"sky": "Mostly cloudy", "icon": "cloudy", "precip_type": "rain",
"high": 71.3, "low": 61.1, "dewpoint_high": 61.6,
"wind_speed": 5.4, "wind_gust": 10.1, "wind_direction_cardinal": "SSW",
"cloud_cover": 73.7, "precip_probability": 12.0, "precip_amount": 0.005
}
]
}
Now is measured. The rest is forecast.
current comes from the surface weather stations around the point: airfield
observations, mesonets and CWOP stations. It is not the model’s opinion of the
present. A model’s analysis hour is still a forecast however recently it ran,
and there is usually a thermometer within a few miles that is not guessing.
hourly and daily are the National Blend of Models. The two are never
blended together and they can disagree, which is not a bug: an observation is a
fact about this minute and a forecast is a guess about the next one.
Both start where the reader is standing in time: hourly[0] is the hour
containing server_time and daily[0] is today at the forecast location. A
cycle’s series really begins in the past (NBM runs every hour and its
first step is an hour after the cycle behind it) and that reach-back is cut
off here so that a strip can be drawn straight from the array. GET /forecast
serves the same series whole, past hours included, for clients charting a run
rather than displaying it.
station names the instrument the reading is attributed to: the one that
supplied most of the fields. Where it has no sensor for something, the next
station nearby fills that field silently. In the example the backyard station
has no visibility sensor, so the 10.0 came from the airfield 6 km away.
name comes from the station roster: airfields are named, CWOP stations
usually are not, and it is null there.
current.sky and current.icon are the exception to “measured”. No station
reports total cloud cover, so they come from the forecast hour nearest
server_time. is_day is not borrowed: it is the sun’s position at this
instant and this point.
current is null when no station within 50 miles has reported in the
last two hours. That is a different statement from every field coming back
empty, and it is deliberate: a reading from an airfield 200 miles away is a
true reading and a false claim about here.
conditions is the station’s own words for what is falling, in METAR code:
"-RA BR", "HZ". It is null when the station reports none, which is not the same as
fair weather. For a phrase covering the sky as well, read hourly[0].sky.
One word for the sky
Every hour and every day carries sky: a single phrase, decided on the server.
Clear · Mostly sunny · Partly cloudy · Mostly cloudy · Overcast |
from cloud cover (Clear below 10%, then from 10%, 30%, 60% and 88%), when little or nothing is expected to fall |
Scattered rain · Scattered snow · Scattered sleet · Scattered freezing rain · Scattered showers |
chance of precipitation ≥ 25%. showers when no type is forecast |
Rain likely · Snow likely · Sleet likely · Freezing rain likely · Precipitation likely |
≥ 55% |
Storms likely |
≥ 55%, with a thunder probability ≥ 30% |
precip_type is one of rain, snow, sleet or freezing rain: the
likeliest kind should anything fall. It is set whenever any type has a chance
above zero, so read it beside precip_probability (the example’s hour says
rain at a 3% chance). It is null when every type is zero.
One picture
icon is the same decision as sky, made as a picture: clear-day,
clear-night, partly-cloudy-day, partly-cloudy-night, cloudy,
overcast, rain, snow, sleet, freezing-rain or thunderstorm. A
client supplies artwork for those eleven and nothing else. At a 25% chance of
precipitation or more it draws the precipitation (thunderstorm with a
thunder probability ≥ 30%); below that it draws the cloud.
Day and night come from is_day, which is the sun’s position at that hour
and place rather than the clock, because “day is 6 to 6” is wrong for months
at a time in Alaska. Days carry no is_day and their icon is always the
daytime one: a card for Thursday showing a moon would say Thursday is night.
The store publishes numbers, not prose, so this wording is derived. It is derived here rather than in each client on purpose: the underlying inputs are four independent precipitation-type probabilities that do not sum to 100, a cloud fraction and a thunder probability, and two panels in one house deriving their own answers is how they end up disagreeing about the same hour.
Fields
A null almost always means nothing publishes that field at that location, not
that something failed.
current
Measured. Every one of these keys is always present; the value is null where
no station nearby reports it.
| field | notes |
|---|---|
temperature, dewpoint, relative_humidity |
A METAR carries no humidity and a CWOP packet carries no dew point; the missing half of that pair is derived from the other |
wind_speed, wind_gust, wind_direction |
Direction is degrees true, blowing from. wind_direction_cardinal gives the compass point |
pressure |
Stations report up to three pressures; this is one of them, chosen in this order: sea-level, then the altimeter setting, then station pressure |
visibility |
|
observed_at |
When the named station reported |
sky, icon, is_day |
See above. The only keys here not measured |
precip_1h, precip_24h, snow_depth |
hourly
From the hour containing server_time; see above.
| field | notes |
|---|---|
time |
Local to the forecast location, with its offset. There is no separate offset field |
sky, icon, is_day, precip_type |
See above |
temperature, dewpoint, relative_humidity |
|
wind_speed, wind_gust, wind_direction_cardinal |
|
cloud_cover |
|
precip_probability |
P(> 0.254 mm), the chance of rain |
precip_amount |
Liquid-equivalent, over the accumulation window ending at this step, which widens when the steps do |
daily
Local calendar days at the forecast location, not UTC days, from today onward. A UTC boundary falls in the evening across the Americas, which files an evening thunderstorm under tomorrow and puts tonight’s low on the wrong card.
| field | notes |
|---|---|
date |
|
sky, icon, precip_type |
From the day’s mean cloud cover and its highest chances |
high, low |
The day’s temperature range |
dewpoint_high |
|
wind_speed, wind_gust |
The day’s maxima |
wind_direction_cardinal |
Prevailing, averaged as a unit vector, because the arithmetic mean of 350° and 10° is 180°, the opposite direction |
cloud_cover |
The day’s mean |
precip_probability |
The day’s maximum |
precip_amount |
The day’s total |
hours |
Forecast hours behind this day |
How much to ask for
GET /weather?lat=33.2&lon=-97.1&hours=12&days=5
| default | range | |
|---|---|---|
hours |
24 | 1 to 168 |
days |
7 | 1 to 8 |
A panel showing the next twelve hours should ask for twelve. hours counts
steps, not clock hours: the series is hourly for about two days, 3-hourly to
about eight, then 6-hourly to about eleven, so hours=168 returns all of it,
around a hundred steps. These only decide how much of the forecast crosses the
wire, not how far ahead it reaches.
Units
GET /weather?lat=33.2&lon=-97.1&units=us
| value | meaning |
|---|---|
si |
Exactly what the model published: Kelvin, m/s, Pascals, kg/m², meters |
metric |
°C, km/h, mm, hPa, km |
us |
°F, mph, in, inHg, mi |
uk |
°C, mm and hPa, but mph and miles |
auto |
Chosen from the caller’s country, via Cloudflare. A caller it cannot place gets us |
Every response carries a units block naming the unit of every field it
returned, so a client never has to infer whether pressure came back in hPa or
inHg. A gauge with a mislabeled scale looks entirely correct.
Observed and forecast fields share one vocabulary and one map. A temperature
in current and a temperature in hourly are the same kind of number in the
same unit, so both render through the same code, including high and low,
which are declared as temperatures rather than leaving you to work it out.
Coverage
The forecast covers the United States and its territories (the Lower 48,
Alaska, Hawaii, Puerto Rico and Guam) and nowhere else. A point outside it
returns 200 with hourly and daily empty, not 404. There is no error to
handle; there is simply no model there.
current is not bounded that way. CWOP and the airfield network are
worldwide, so a point with no forecast can still have a thermometer next to
it: London gets a current and an empty forecast.
Polling
See rate limits. The response is cached for 60 seconds, which is the station’s cadence rather than the model’s: CWOP stations report every five minutes and METARs hourly, while NBM runs once an hour. Polling faster than a minute gains nothing; polling once a minute is the most a display can usefully do.

