Base URL: https://api.wxalerts.org
What real instruments are reporting right now: airfield observations, buoys and coastal stations, and the backyard stations of CWOP and amateur radio. Nothing here is a model. Every number says which station it came from, how far away that station is, and how old the reading was.
GET /conditions?lat=33.215&lon=-97.133&units=us
GET /conditions?q=norman+ok
GET /stations?bbox=-97.9,32.4,-96.3,33.4&units=us
GET /stations/metar:KDTO
GET /stations/metar:KDTO/history?hours=6
No key needed. Browsers block calls from other websites (CORS allows only wxalerts.org), so call it from a server, script or app. The answers are cached for 60 seconds; see rate limits.
For a screen that wants six flat numbers and a forecast beside them, use
GET /weather. Its current block is /conditions
reduced to one value per field and one station name.
Where the readings come from
Every station has an id with its network in front of it, and a source
naming the feed it arrives on.
source |
id looks like | what it is | trust_tier |
|---|---|---|---|
metar |
metar:KDTO |
ASOS and AWOS at airfields, via the Aviation Weather Center. Reports hourly, more often when the weather changes | 1 |
ndbc |
ndbc:42095, ndbc:LONF1 |
NDBC buoys, C-MAN and coastal stations, NOS tide gauges and partner platforms. The only source of sea state | 1 |
aprs |
cwop:GW4373, aprs:KC5CAM-13 |
CWOP and amateur radio weather stations off APRS-IS, mostly every five minutes. cwop: ids are CWOP registrations, aprs: ids are callsigns |
4 |
trust_tier runs from 1 (a maintained instrument) to 4 (somebody’s console).
Tier 2 is for mesonets and tier 3 for stations hosted here directly (source
pws); neither is ingesting yet, so you will only see 1 and 4 today. The
station owner’s guide covers how a
CWOP station is positioned and weighted from the other side.
Two more labels describe the platform rather than the feed:
station_type:land,coastal,buoy,platform,usv,dart,taoorother. A METAR can come from an oil rig.domain:landormarine. Whether the station may speak for conditions ashore. A buoy ismarine; a C-MAN tower on a pier island, because the air it measures is the air on the beach.
Conditions at a point
GET /conditions?lat=33.215&lon=-97.133&units=us
| parameter | default | |
|---|---|---|
lat, lon |
the point. Or q (a place name, up to 120 characters) or location_id |
|
units |
auto |
see Units |
max_age_minutes |
120 | 5 to 1440. Ignore stations whose last report is older |
limit |
12 | 1 to 50. How many of the nearest stations to rank. More fills more fields, from further away |
No point at all is 422; a q or location_id that matches nothing is
404.
{
"server_time": "2026-11-09T19:07:14Z",
"location": { "lat": 33.215, "lon": -97.133, "name": null, "display_name": null, "state": null },
"units": {
"system": "us", "source": "explicit", "country": null,
"fields": {
"temperature": { "unit": "F", "label": "°F", "description": "Air temperature." },
"pressure_altimeter": { "unit": "inHg", "label": "inHg", "description": "Altimeter setting, as reported in a METAR." }
}
},
"fields": {
"temperature": { "value": 69.0, "unit": "F", "label": "°F", "station_id": "aprs:KC5CAM-13",
"source": "aprs", "distance_m": 2485, "age_seconds": 74, "derived": false },
"dewpoint": { "value": 58.8, "unit": "F", "label": "°F", "station_id": "aprs:KC5CAM-13",
"source": "aprs", "distance_m": 2485, "age_seconds": 74, "derived": true },
"pressure_altimeter": { "value": 30.2, "unit": "inHg", "label": "inHg", "station_id": "metar:KDTO",
"source": "metar", "distance_m": 6210, "age_seconds": 854, "derived": false },
"visibility": { "value": 10.0, "unit": "mi", "label": "mi", "station_id": "metar:KDTO",
"source": "metar", "distance_m": 6210, "age_seconds": 854, "derived": false },
"cloud": { "value": [{ "cover": "BKN", "base_ft_agl": 3900.0 }], "unit": null, "label": null,
"station_id": "metar:KDTO", "source": "metar", "distance_m": 6210, "age_seconds": 854,
"derived": false }
},
"marine": {},
"stations": [
{ "station_id": "aprs:KC5CAM-13", "source": "aprs", "station_type": "land", "domain": "land",
"trust_tier": 4, "name": null, "lat": 33.2363, "lon": -97.1412,
"distance_m": 2485, "age_seconds": 74, "observed_at": "2026-11-09T19:06:00Z",
"raw": "KC5CAM-13>APRS,TCPIP*,qAC,AMBCWOP-2:@091906z3314.18N/09708.47W_181/002g003t069r000p000P000h70b10230L216AmbientCWOP.com" },
{ "station_id": "metar:KDTO", "source": "metar", "station_type": "land", "domain": "land",
"trust_tier": 1, "name": "Denton Muni, TX", "lat": 33.2088, "lon": -97.1992,
"distance_m": 6210, "age_seconds": 854, "observed_at": "2026-11-09T18:53:00Z",
"raw": "METAR KDTO 091853Z AUTO 17003KT 10SM BKN039 19/13 A3020 RMK AO2 SLP221 T01940128" }
],
"candidates_considered": 12
}
Every field names its own station
A point’s conditions are assembled one field at a time. The nearest stations are ranked once, and each field takes the best-ranked station that reports it. In the example the temperature, dew point and rain came from a backyard station 2.5 km away, while visibility, the altimeter setting and the cloud layers came from the airfield 6 km away, because the backyard station has no sensor for them. A station missing a sensor costs that one field, not the whole answer.
Stations are never averaged. Two stations disagreeing is a fact about the weather; their mean is a number nobody observed.
The ranking is one line:
score = distance × tier penalty × (1 + age in seconds / 7200)
Lowest wins. The tier penalty is 1.0 for tier 1, 1.3 for tier 2, 1.6 for tier 3 and 2.0 for tier 4, so a CWOP station has to be more than twice as close as an airfield to win a field, and a reading two hours old counts as twice as far away as a fresh one. In the Florida Keys that puts a C-MAN tower 32 km away ahead of a CWOP station 10 km away for temperature and wind.
Each entry in fields carries:
| key | |
|---|---|
value |
in the response’s unit system |
unit, label |
machine code (F, hPa) and display form (°F) |
station_id, source |
who measured it |
distance_m |
from the requested point to that station |
age_seconds |
how old the reading was when served |
derived |
computed rather than measured. A METAR carries no humidity and a CWOP packet carries no dew point, so the missing half of each pair is derived from the other (relative_humidity on metar, dewpoint on aprs) |
A key that is absent had no station reporting it. stations lists only the
stations that supplied at least one field, with their name, position, raw
report and how far and how old they were. candidates_considered is how many
stations were ranked: 0 means nothing has reported nearby within
max_age_minutes, which is a different answer from a point where every field
happens to be missing.
Readings that are held back
A broken backyard thermometer is caught by its neighbors. For temperature
and dewpoint, a reading more than 16.7 °C (about 30 °F) from the median of
at least three land stations within 50 miles is refused, and the field falls
through to the next station. The bound is loose on purpose: sea breezes,
outflow and valley inversions are real and stay. When a derived value fails,
the measurement it came from goes with it (a CWOP dew point fails, so its
humidity is withheld too). Wind and rain are never checked this way, because
one storm really can put them at one station and not the next.
Buoys and offshore platforms (domain: marine) never answer air temperature,
moisture, wind, cloud or present weather for a point, because air over open
water is not the air ashore. Their pressure counts, and they are the main
source of marine.
The fields
| field | |
|---|---|
temperature, dewpoint, relative_humidity |
|
wind_direction |
degrees true, the direction it blows from. A calm METAR reports 0 with a speed of 0 |
wind_speed, wind_gust |
sustained wind and peak gust |
pressure_sea_level |
reduced to sea level. On a CWOP station the owner’s console did the reduction, with an elevation they typed in |
pressure_altimeter |
the altimeter setting in a METAR |
pressure_station |
at the station, not reduced |
pressure_tendency |
change over the last three hours; negative is falling |
visibility |
prevailing |
precip_1h, precip_24h, precip_since_local_midnight |
accumulations over each window |
snow_depth |
|
solar_irradiance |
downwelling, W/m² in every system |
cloud |
METAR sky layers, lowest first: [{ "cover": "BKN", "base_ft_agl": 3900.0 }]. Bases are always feet above ground, whatever units says |
wx_string |
present weather in METAR code: "-RA BR", "VCSH" |
flight_category |
VFR, MVFR, IFR or LIFR |
Three pressures, never one: they are different measurements, and a station
can report all three. /weather picks one for you.
cloud, wx_string and flight_category carry null for unit and
label; they are text, not quantities.
Sea state
"marine": {
"water_temperature": { "value": 83.7, "unit": "F", "label": "°F", "station_id": "ndbc:TBYF1",
"source": "ndbc", "station_type": "coastal", "distance_m": 21126,
"age_seconds": 4035, "derived": false },
"water_level": { "value": 0.3, "unit": "ft", "label": "ft", "station_id": "ndbc:TBYF1",
"source": "ndbc", "station_type": "coastal", "distance_m": 21126,
"age_seconds": 4035, "derived": false }
}
marine is its own block, chosen the same way, because the station that
answers it is often much further away and the distance is the point. It is
{} inland. Each entry adds station_type, so you can tell a buoy 30 km out
from a gauge on the pier.
| field | |
|---|---|
water_temperature |
near the waterline, about 0.6 m down on a buoy hull. Not a skin or satellite temperature |
wave_height |
significant wave height: the mean of the highest third, about half the largest wave present |
wave_period_dominant, wave_period_average |
seconds. In a mixed sea the two differ |
wave_direction |
degrees true, the direction the dominant waves come from |
water_level |
above the station’s datum |
Stations in a viewport
GET /stations?bbox=-97.9,32.4,-96.3,33.4&units=us
GET /stations?bbox=-130,20,-90,50&sources=metar&detail=minimal
GET /stations?bbox=-83,24,-79,27&reports_sea=true
For a map. Returns every station in the box that has reported within
max_age_minutes, airfields and buoys (tier 1) first, then the most recent.
| parameter | default | |
|---|---|---|
bbox |
required | west,south,east,north in degrees. At most 30° a side, or up to 180° when sources=metar alone. Does not cross the antimeridian |
sources |
all | comma separated: metar, aprs, ndbc. An unknown name matches nothing rather than failing |
domain |
both | land or marine |
reports_sea |
false |
only stations measuring the water (sea temperature or wave height) |
detail |
full |
minimal for position and one reading per station |
max_age_minutes |
120 | 5 to 1440 |
limit |
400 | 1 to 2000 |
units |
auto |
see Units |
A malformed, inverted or oversized bbox is 422 with a message saying
which.
{
"server_time": "2026-11-09T19:10:02Z",
"count": 3,
"truncated": true,
"units": { "system": "us", "source": "explicit", "country": null, "fields": { "...": "..." } },
"stations": [
{
"station_id": "metar:KLUD", "source": "metar", "network": "ASOS/AWOS",
"name": "Decatur Muni, TX", "lat": 33.2568, "lon": -97.5826, "elevation_m": 319.0,
"trust_tier": 1, "station_type": "land", "domain": "land",
"observed_at": "2026-11-09T18:55:00Z", "age_seconds": 927,
"temperature": 66.2, "dewpoint": 53.6, "relative_humidity": 63.8,
"wind_direction": 190.0, "wind_speed": 4.6, "wind_gust": null,
"pressure_sea_level": null, "precip_1h": null,
"water_temperature": null, "wave_height": null, "wave_period_dominant": null, "wave_direction": null,
"raw": "METAR KLUD 091855Z AUTO 19004KT 10SM FEW029 FEW036 FEW100 19/12 A3020 RMK AO2",
"withheld": []
}
]
}
truncated: true means the box holds more reporting stations than limit
returned. Zoom in rather than treating the page as all of them.
withheld names any field refused against the station’s neighbors in this
viewport (the same check as above), with that field set to null. The
station stays on the map so its owner can find it. A viewport with fewer
than three peers around a station gets no check.
network is the roster’s name for the program (ASOS/AWOS, CWOP,
APRSWXNET, NOS/CO-OPS, IOOS Partners and so on), finer than source.
A platform that reaches us through two feeds is drawn once.
detail=minimal
Over a continent the full rows are most of a megabyte, nearly all of it raw
reports nobody will click. minimal keeps what a client needs to place and
color a point:
{ "station_id": "metar:KLUD", "source": "metar", "temperature": 66.2,
"lat": 33.2568, "lon": -97.5826, "age_seconds": 927 }
With reports_sea=true or domain=marine, the minimal row swaps the air
temperature for the water:
{ "station_id": "ndbc:41122", "source": "ndbc", "station_type": "buoy",
"water_temperature": 82.9, "wave_height": 1.3,
"lat": 26.001, "lon": -80.096, "age_seconds": 2427 }
One station
GET /stations/metar:KDTO?units=us
Who the station is, where, and its latest reading. Unknown ids are 404.
{
"server_time": "2026-11-09T19:09:12Z",
"units": { "system": "us", "source": "explicit", "country": null, "fields": { "...": "..." } },
"station": {
"station_id": "metar:KDTO", "source": "metar", "network": "ASOS/AWOS",
"name": "Denton Muni, TX", "lat": 33.2088, "lon": -97.1992, "elevation_m": 199.0,
"trust_tier": 1, "position_source": "awc",
"first_seen_at": "2026-08-31T13:55:55Z", "last_seen_at": "2026-11-09T19:09:11Z",
"observed_at": "2026-11-09T18:53:00Z", "age_seconds": 992,
"raw": "METAR KDTO 091853Z AUTO 17003KT 10SM BKN039 19/13 A3020 RMK AO2 SLP221 T01940128",
"latest": {
"temperature": 66.9, "dewpoint": 55.0, "relative_humidity": 65.6,
"wind_direction": 170.0, "wind_speed": 3.5,
"pressure_sea_level": 30.18, "pressure_altimeter": 30.2, "visibility": 10.0,
"cloud": [{ "cover": "BKN", "base_ft_agl": 3900.0 }], "flight_category": "VFR"
}
}
}
latest holds only what this station reports. An absent key means the
station has no such sensor, which is more useful than a row of dashes. Its
field names are the ones in the fields and sea
state.
position_source says where the coordinates came from: a roster (awc,
madis, ndbc) or the station’s own APRS beacon (aprs), which is
self-reported. A station can be on the roster before it has sent anything;
then observed_at, age_seconds and raw are null and latest is {}.
One station’s history
GET /stations/metar:KDTO/history?hours=6&units=us
| parameter | default | |
|---|---|---|
hours |
24 | 1 to 168 |
limit |
500 | 1 to 2000 |
units |
auto |
see Units |
{
"server_time": "2026-11-09T19:09:41Z",
"station_id": "metar:KDTO",
"hours": 3,
"count": 3,
"units": { "system": "us", "source": "explicit", "country": null, "fields": { "...": "..." } },
"samples": [
{ "observed_at": "2026-11-09T16:53:00Z", "temperature": 61.0, "dewpoint": 55.9,
"relative_humidity": 83.5, "wind_direction": 0.0, "wind_speed": 0.0,
"pressure_sea_level": 30.24, "pressure_altimeter": 30.25, "visibility": 10.0,
"cloud": [{ "cover": "OVC", "base_ft_agl": 4500.0 }], "flight_category": "VFR" },
"..."
]
}
Samples run oldest first, and each carries observed_at plus only the
fields that reading had. A known station with nothing in the window returns
200 with count: 0; an id no station has is 404, as on the detail
route.
When the window holds more than limit samples, the newest limit are kept.
A CWOP station reports every five minutes, so a week of it is around 2,000
rows and the default limit of 500 covers about the last two days. If count
equals limit, the start of the window was cut; ask for a higher limit to
reach it.
Units
GET /conditions?lat=33.2&lon=-97.1&units=metric
| value | temperature | speed | pressure | precipitation | visibility | wave height, water level |
|---|---|---|---|---|---|---|
si |
K | m/s | Pa | kg/m² | m | m |
metric |
°C | km/h | hPa | mm | km | m |
us |
°F | mph | inHg | in | mi | ft |
uk |
°C | mph | hPa | mm | mi | m |
auto |
chosen from the caller’s country |
Humidity is %, directions deg, wave periods s and solar irradiance
W/m² in every system. With auto, a country that reads Fahrenheit gets
us, Britain gets uk and everywhere else metric; a caller who cannot be
placed gets us.
Every response’s units block names the system, how it was chosen
(source: explicit, geo or default), the country when it was chosen
from one, and the unit, label and a one-line description for every field the
endpoint can return. The vocabulary is the forecast’s own, so a temperature
here and a temperature on /forecast render through
the same code.

