The National Weather Service API: A Complete Developer's Guide (2026)
If you're building anything weather-related for a United States audience, the National Weather Service API is the first thing you should evaluate. It is operated by NOAA, requires no API key, has no billing page, and serves the same forecast data that drives weather.gov and the alerts your phone screams at you during a tornado warning.
It is also, in places, genuinely strange to work with. The URL structure assumes you already know NWS internal geography. Forecasts come back as GeoJSON with ISO 8601 intervals instead of simple timestamps. And the free-ness comes with a real trade-off: coverage stops at the U.S. border.
This guide covers what the API actually returns, how to get from a latitude/longitude pair to a usable forecast, the gotchas that break production integrations, and when you should reach for a commercial provider instead.
What the National Weather Service API Is
The NWS API — served from https://api.weather.gov — is the public REST interface to the National Weather Service's operational forecast and warning systems. It replaced the old SOAP/XML NDFD services and is now the canonical machine-readable source for:
- Point forecasts — 7-day and hourly forecasts for any coordinate in the U.S.
- Gridpoint data — raw numeric forecast values (temperature, dewpoint, wind, precipitation probability, sky cover) at 2.5km resolution
- Current observations — METAR-derived readings from ASOS/AWOS stations
- Active alerts — watches, warnings, and advisories in CAP (Common Alerting Protocol) format
- Radar and station metadata — station listings, radar site info, office boundaries
- Aviation products — SIGMETs, Center Weather Advisories, TAFs
Everything is public domain. U.S. government works are not copyrightable, so there is no license to comply with, no attribution requirement (though NWS asks nicely), and no commercial-use restriction. That's a meaningfully different legal posture from every commercial weather API, most of which restrict redisplay or require attribution badges.
If you want to browse the listing and its metadata, the US Weather API entry on PublicAPIs.io has the endpoint reference and current status.
Authentication: There Isn't Any (But Read This)
No API key. No OAuth. No signup. You just make requests.
There is one requirement that trips up nearly every first-time integrator: you must send a User-Agent header identifying your application. NWS uses it for abuse contact and will return 403 Forbidden for requests it considers anonymous or bot-like.
curl -H "User-Agent: (myweatherapp.com, contact@myweatherapp.com)" \
-H "Accept: application/geo+json" \
"https://api.weather.gov/points/38.8894,-77.0352"
The convention NWS documents is (your-app-or-domain, contact-email). A browser-looking User-Agent string is more likely to get blocked than a clearly identified one. Set this once in your HTTP client and forget it:
const nws = {
base: 'https://api.weather.gov',
headers: {
'User-Agent': '(myweatherapp.com, contact@myweatherapp.com)',
'Accept': 'application/geo+json',
},
};
The Two-Step Lookup Every Integration Needs
Here is the single biggest structural difference between the National Weather Service API and every commercial weather API: you cannot request a forecast by latitude and longitude directly.
NWS organizes forecasts by gridpoint — a cell in a forecast office's 2.5km grid, addressed as {office}/{gridX},{gridY}. To get a forecast, you first translate coordinates into a gridpoint via the /points endpoint.
Step 1 — Resolve coordinates to a gridpoint:
GET https://api.weather.gov/points/38.8894,-77.0352
The response's properties object contains everything you need:
{
"properties": {
"gridId": "LWX",
"gridX": 96,
"gridY": 70,
"forecast": "https://api.weather.gov/gridpoints/LWX/96,70/forecast",
"forecastHourly": "https://api.weather.gov/gridpoints/LWX/96,70/forecast/hourly",
"forecastGridData": "https://api.weather.gov/gridpoints/LWX/96,70",
"observationStations": "https://api.weather.gov/gridpoints/LWX/96,70/stations",
"relativeLocation": {
"properties": { "city": "Arlington", "state": "VA" }
}
}
}
Note that NWS hands you fully-formed URLs for the follow-up calls. Use them rather than constructing your own — it's HATEOAS-style navigation, and it insulates you from URL changes.
Step 2 — Fetch the forecast:
GET https://api.weather.gov/gridpoints/LWX/96,70/forecast
Coordinates must have at most four decimal places. More precision than that returns a 301 redirect to the truncated version, which some HTTP clients handle poorly on non-GET methods.
Critically: the /points result is stable. A given coordinate maps to the same gridpoint essentially forever. Cache this mapping aggressively — in Redis, in your database, wherever — and you halve your request volume immediately.
async function getForecast(lat, lon) {
const key = `${lat.toFixed(4)},${lon.toFixed(4)}`;
let point = await cache.get(`nws:point:${key}`);
if (!point) {
const res = await fetch(`${nws.base}/points/${key}`, { headers: nws.headers });
if (!res.ok) throw new Error(`NWS points failed: ${res.status}`);
point = (await res.json()).properties;
await cache.set(`nws:point:${key}`, point, { ttl: 60 * 60 * 24 * 30 });
}
const res = await fetch(point.forecast, { headers: nws.headers });
if (!res.ok) throw new Error(`NWS forecast failed: ${res.status}`);
return (await res.json()).properties.periods;
}
Reading the Forecast Response
The /forecast endpoint returns 14 periods covering roughly seven days, split into day and night halves:
{
"properties": {
"updated": "2026-09-02T14:32:00+00:00",
"periods": [
{
"number": 1,
"name": "This Afternoon",
"startTime": "2026-09-02T13:00:00-04:00",
"endTime": "2026-09-02T18:00:00-04:00",
"isDaytime": true,
"temperature": 84,
"temperatureUnit": "F",
"probabilityOfPrecipitation": { "unitCode": "wmoUnit:percent", "value": 20 },
"windSpeed": "8 mph",
"windDirection": "SW",
"shortForecast": "Partly Sunny",
"detailedForecast": "Partly sunny, with a high near 84. Southwest wind around 8 mph."
}
]
}
}
Three things to watch:
windSpeed is a string, not a number. It arrives as "8 mph" or "10 to 15 mph" — a range, sometimes. If you need numerics, parse it, or use the forecastGridData endpoint, which returns proper values with WMO unit codes.
probabilityOfPrecipitation.value can be null. Not zero — null, meaning "no meaningful chance forecast," which is semantically distinct from 0%. Handle both.
Period names are human strings, not enums. "This Afternoon", "Tonight", "Labor Day", "Independence Day Night". Never key logic off them; use isDaytime and startTime.
For raw numeric data, /gridpoints/{office}/{x},{y} returns time-series values with ISO 8601 duration intervals:
{
"temperature": {
"uom": "wmoUnit:degC",
"values": [
{ "validTime": "2026-09-02T13:00:00+00:00/PT3H", "value": 28.888 }
]
}
}
That validTime is a start instant plus an ISO duration (PT3H = three hours). Most date libraries won't parse this out of the box — Duration.from() in Temporal, or luxon's Duration.fromISO(), will handle the second half after you split on /. Also note: raw gridpoint data is metric. The /forecast endpoint does the imperial conversion for you.
Severe Weather Alerts
The alerts endpoint is arguably the most valuable part of the National Weather Service API, because there is no free commercial equivalent for official U.S. government warnings.
# Active alerts for a point
curl -H "User-Agent: (myapp.com, me@myapp.com)" \
"https://api.weather.gov/alerts/active?point=38.8894,-77.0352"
# All active alerts for a state
curl -H "User-Agent: (myapp.com, me@myapp.com)" \
"https://api.weather.gov/alerts/active?area=TX"
# Filter by severity and urgency
curl -H "User-Agent: (myapp.com, me@myapp.com)" \
"https://api.weather.gov/alerts/active?area=OK&severity=Extreme&urgency=Immediate"
Alerts follow the CAP standard, so each carries severity (Extreme, Severe, Moderate, Minor, Unknown), certainty, urgency, event (e.g. "Tornado Warning"), an affected-area geometry, and expires/ends timestamps.
For near-real-time delivery without polling, NWS runs a Server-Sent Events stream:
const stream = new EventSource(
'https://api.weather.gov/alerts/active/stream?area=KS'
);
stream.onmessage = (e) => {
const alert = JSON.parse(e.data).properties;
if (alert.severity === 'Extreme' || alert.severity === 'Severe') {
dispatchNotification(alert);
}
};
The stream can drop connections. Implement reconnect with backoff, and reconcile against /alerts/active on reconnect so you don't miss alerts issued during the gap.
Rate Limits and Caching
NWS doesn't publish a hard numeric rate limit. What it documents is a request to be reasonable, and what it enforces in practice is a sliding window that returns 429 Too Many Requests when you're clearly hammering it. Community reports put the practical ceiling somewhere in the low hundreds of requests per minute per IP, but treat that as folklore rather than a contract — it's not a documented SLA and it can change.
The safe posture:
- Cache
/pointsresults indefinitely. Grid mappings don't move. - Respect
Cache-ControlandExpiresheaders. NWS sets them, and forecasts typically update hourly. Re-fetching every 60 seconds gets you identical bytes. - Use conditional requests. Send
If-Modified-Sinceand handle304. - Back off on 429 and 503. Exponential, with jitter.
- Don't fan out per user. Cache at the gridpoint level server-side, not per session.
Reliability is the other consideration. api.weather.gov is a government service without a published uptime SLA, and it has had multi-hour outages — usually during exactly the severe weather events when your users need it most. If weather data is load-bearing for your product, build a fallback path to a second provider.
When to Use a Commercial Weather API Instead
The National Weather Service API is excellent within its lane. Outside it, you need alternatives.
| Requirement | NWS API | Better option |
|---|---|---|
| U.S. forecasts and alerts | ✅ Best-in-class, free | — |
| International coverage | ❌ U.S. only | WeatherAPI, AccuWeather |
| Minute-by-minute nowcasting | ❌ Not available | ColorfulClouds |
| Long historical archives | ⚠️ Limited, awkward | Commercial providers |
| City-name / IP geocoding | ❌ Coordinates only | WeatherAPI, AccuWeather |
| Uptime SLA | ❌ None published | Any paid tier |
| Aviation products (TAF/METAR) | ⚠️ Partial | AviationWeather |
A few specifics worth knowing:
WeatherAPI is the usual first fallback. It has global coverage, accepts city names and IP addresses as location inputs, returns astronomy and air quality data alongside forecasts, and its free tier is generous enough for prototyping. The response shape is flatter and easier to consume than GeoJSON.
AccuWeather is heavier on the enterprise side — location-key-based lookups, MinuteCast precipitation nowcasts, and severe weather indices. Its free tier is tight (50 calls/day at time of writing), so it's a production-with-a-contract choice rather than a hobby-project one.
ColorfulClouds (Caiyun) specializes in minute-level precipitation nowcasting — "rain starting in 12 minutes" — with particularly strong coverage in China. If your product's core loop is "should I bring an umbrella right now," this class of API does something NWS structurally does not.
AviationWeather is also a NOAA service, and it's the right endpoint for METARs, TAFs, PIREPs, and AIRMETs in the formats pilots and flight-planning tools expect. NWS surfaces some aviation products, but AviationWeather.gov is purpose-built for them.
The common production pattern is NWS-primary, commercial-fallback: serve U.S. coordinates from api.weather.gov, route everything else — and any NWS failure — to a paid provider. You get free data for the majority of requests and pay only for the tail.
A Complete Working Example
Putting it together — coordinates in, forecast and active alerts out, with caching and a graceful failure path:
const HEADERS = {
'User-Agent': '(myweatherapp.com, contact@myweatherapp.com)',
'Accept': 'application/geo+json',
};
async function nwsFetch(url, retries = 2) {
for (let attempt = 0; attempt <= retries; attempt++) {
const res = await fetch(url, { headers: HEADERS });
if (res.ok) return res.json();
// Back off on throttling and transient upstream errors
if (res.status === 429 || res.status >= 500) {
const wait = 2 ** attempt * 500 + Math.random() * 300;
await new Promise((r) => setTimeout(r, wait));
continue;
}
throw new Error(`NWS ${res.status} for ${url}`);
}
throw new Error(`NWS unavailable after ${retries + 1} attempts`);
}
async function getWeather(lat, lon) {
const coords = `${lat.toFixed(4)},${lon.toFixed(4)}`;
const point = await nwsFetch(`https://api.weather.gov/points/${coords}`);
const { forecast, forecastHourly, relativeLocation } = point.properties;
const [daily, hourly, alerts] = await Promise.all([
nwsFetch(forecast),
nwsFetch(forecastHourly),
nwsFetch(`https://api.weather.gov/alerts/active?point=${coords}`),
]);
return {
location: relativeLocation.properties,
current: hourly.properties.periods[0],
daily: daily.properties.periods,
alerts: alerts.features.map((f) => ({
event: f.properties.event,
severity: f.properties.severity,
headline: f.properties.headline,
expires: f.properties.expires,
})),
};
}
Wrap getWeather in a try/catch that falls through to your secondary provider on throw, and you have an integration that survives both NWS outages and non-U.S. coordinates.
Common Errors and What They Mean
| Status | Cause | Fix |
|---|---|---|
403 Forbidden |
Missing or generic User-Agent |
Send (domain, email) format |
404 Not Found on /points |
Coordinates outside NWS coverage | Check bounds before calling; fall back for international |
301 Moved Permanently |
More than 4 decimal places in coordinates | Truncate before requesting |
429 Too Many Requests |
Rate limiting | Back off, cache harder |
500 / 503 |
Upstream NWS issue | Retry with backoff; fail over |
The 404 case deserves attention: NWS coverage includes the 50 states, D.C., Puerto Rico, the U.S. Virgin Islands, Guam, American Samoa, and the Northern Marianas — plus coastal and offshore marine zones. A coordinate in Toronto returns 404, not an empty forecast. Detect it explicitly rather than treating it as a generic failure.
The Bottom Line
For U.S. weather, the National Weather Service API is hard to beat: authoritative data, no key, no cost, no license restrictions, and the only free source of official government severe weather alerts. The cost is developer ergonomics — the two-step grid lookup, string-typed wind speeds, ISO 8601 duration intervals, and no uptime guarantee.
Design around those constraints. Cache the /points mapping permanently, cache forecasts to their Expires header, back off politely, and keep a commercial provider wired up for international coordinates and outage windows. Do that, and you get government-grade U.S. weather data at zero marginal cost with a sane failure mode.
Browse the full set of weather integrations in the PublicAPIs.io directory to compare coverage, pricing, and rate limits before you commit to a provider.