Skip to content
Docs navigation

Build a weather app

From your first 200 to a working app: where the key lives, the query to send, how long to keep each answer, how to read times and icons, and what to show for attribution. Every field the query returns is listed in the Weather field reference.

1. Call Smarter Weather from your own backend

Call Smarter Weather from your own backend, and have that backend serve only your own app. Your app calls your backend; your backend holds the key and calls https://api.smarterweather.com.

  • Keep the key server-side. A key shipped in browser or mobile code is a key anyone can copy. Acceptable Use §4 covers keys in public clients: use a server-side or edge proxy for them.
  • Serve your app, nobody else. Sharing your key with third parties, or operating a proxy, gateway or bring-your-own-key service that lets third parties use your credentials, is prohibited by Acceptable Use §1. The Acceptable Use Policy is the authority on what is allowed; this guide does not add to it.
  • Put the route behind your app's own sign-in. Your backend's weather route must accept only requests from your signed-in users or your own app, checked with a session or an app token. Without that check it is an open relay for your key.
  • CORS is not that check. In production the API accepts browser calls only from Smarter Weather's own sites, so a page on your domain cannot call it directly. CORS only governs what a browser lets a page read. Any other client can call your backend whatever its CORS headers say.

The two backends below are deliberately narrow:

  • Each serves one route, GET /weather?lat=..&lon=...
  • It answers 401 unless the request carries the X-App-Token header set to APP_TOKEN, compared in constant time. That shared token is a minimal stand-in for your app's sign-in. Replace it with your session check before you ship: a token in page code can be copied too.
  • It refuses to start when SMARTERWEATHER_API_KEY or APP_TOKEN is unset or empty.
  • It rejects a lat or lon that is not a number in range, and calls only /v1/weather with a fixed query.
  • It caches each answer as section 3 describes. Requests for the same location that arrive while a call is in flight share that call's answer, whatever it is, so a burst makes one upstream call.
  • It gives up on the API after 10 seconds for the whole call, body included, and answers 502 for a timeout, a dropped connection or a truncated body.
  • It passes Retry-After and the RateLimit-* headers of the API's answer through to your app. After a 429 it answers every uncached request with a 429 of its own until Retry-After passes, without calling the API. Those local answers carry a recomputed Retry-After and no RateLimit-* headers, which described the API's call.

Set SMARTERWEATHER_API_BASE only to point the backend at a local stub in tests.

Node 20

// server.mjs. Run: node --env-file=.env server.mjs (Node 20.6+)
import { createServer } from "node:http";
import { createHash, timingSafeEqual } from "node:crypto";

const KEY = process.env.SMARTERWEATHER_API_KEY ?? "";
const APP_TOKEN = process.env.APP_TOKEN ?? ""; // stand-in for your app's own sign-in
const BASE = process.env.SMARTERWEATHER_API_BASE ?? "https://api.smarterweather.com";
const QUERY = "view=concise&days=7&hourly_hours=24&units=imperial";
const PASS = /^(content-type|retry-after|ratelimit-.*)$/i; // headers your app sees
const OUTSIDE_COVERAGE_TTL = 3600; // a point outside coverage stays outside
if (!KEY || !APP_TOKEN) {
  console.error("set SMARTERWEATHER_API_KEY and APP_TOKEN");
  process.exit(1);
}

const cache = new Map(); // "lat,lon" -> { expires, status, headers, body }
const inflight = new Map(); // "lat,lon" -> the pending upstream call, shared by every request for it
let throttled = null; // the last 429, answered locally until its Retry-After passes

const coord = (v, limit) => {
  const n = v === null || v.trim() === "" ? NaN : Number(v);
  return Math.abs(n) <= limit ? n.toFixed(4) : null;
};
const maxAge = (cc) =>
  /no-store/i.test(cc) ? 0 : Number(/(?:^|,)\s*max-age=(\d+)/i.exec(cc)?.[1] ?? 0);
const sha256 = (value) => createHash("sha256").update(value).digest();
const WANT = sha256(APP_TOKEN);
// Constant time: equal-length digests, so neither the length nor the bytes leak.
const signedIn = (req) => timingSafeEqual(sha256(String(req.headers["x-app-token"] ?? "")), WANT);
const problemType = (body) => {
  try { return JSON.parse(body).type ?? ""; } catch { return ""; }
};
// RateLimit-* describe one upstream call, so a stored copy keeps just the type.
const stored = (entry) => ({ ...entry, headers: { "content-type": entry.headers["content-type"] } });

async function upstream(lat, lon) {
  // The signal bounds the whole call, body included.
  const res = await fetch(`${BASE}/v1/weather?lat=${lat}&lon=${lon}&${QUERY}`, {
    headers: { Authorization: `Bearer ${KEY}` },
    signal: AbortSignal.timeout(10_000),
  });
  const headers = Object.fromEntries([...res.headers].filter(([name]) => PASS.test(name)));
  const body = await res.text();
  let ttl = res.ok ? maxAge(res.headers.get("cache-control") ?? "") : 0;
  if (res.status === 404 && problemType(body).endsWith("outside-coverage")) ttl = OUTSIDE_COVERAGE_TTL;
  if (res.status === 429) ttl = Number(res.headers.get("retry-after")) || 1;
  return { expires: Date.now() + ttl * 1000, status: res.status, headers, body };
}

async function fetchAndStore(key, lat, lon) {
  const entry = await upstream(lat, lon);
  for (const [k, v] of cache) if (v.expires <= Date.now()) cache.delete(k);
  if (entry.status === 429) throttled = stored(entry);
  else if (entry.expires > Date.now()) cache.set(key, stored(entry));
  return entry;
}

function weather(key, lat, lon) {
  const hit = cache.get(key);
  if (hit?.expires > Date.now()) return hit;
  if (throttled?.expires > Date.now()) return throttled;
  if (!inflight.has(key)) {
    inflight.set(key, fetchAndStore(key, lat, lon).finally(() => inflight.delete(key)));
  }
  return inflight.get(key);
}

createServer(async (req, res) => {
  const url = new URL(req.url, "http://localhost");
  if (req.method !== "GET" || url.pathname !== "/weather") return res.writeHead(404).end();
  if (!signedIn(req)) return res.writeHead(401).end("sign in first");
  const lat = coord(url.searchParams.get("lat"), 90);
  const lon = coord(url.searchParams.get("lon"), 180);
  if (lat === null || lon === null) return res.writeHead(400).end("lat and lon must be numbers");
  try {
    const entry = await weather(`${lat},${lon}`, lat, lon);
    const headers = { ...entry.headers };
    if (entry.status === 429) {
      headers["retry-after"] = String(Math.max(1, Math.ceil((entry.expires - Date.now()) / 1000)));
    }
    res.writeHead(entry.status, headers).end(entry.body);
  } catch {
    res.writeHead(502).end("weather unavailable");
  }
}).listen(8080);

Python

# server.py. Run: SMARTERWEATHER_API_KEY=... APP_TOKEN=... python3 server.py (stdlib only)
import concurrent.futures, hashlib, hmac, http.client, json, math, os, re, threading, time
import urllib.error, urllib.parse, urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

KEY = os.environ.get("SMARTERWEATHER_API_KEY", "")
APP_TOKEN = os.environ.get("APP_TOKEN", "")  # stand-in for your app's own sign-in
if not KEY or not APP_TOKEN:
    raise SystemExit("set SMARTERWEATHER_API_KEY and APP_TOKEN")
BASE = os.environ.get("SMARTERWEATHER_API_BASE", "https://api.smarterweather.com")
QUERY = "view=concise&days=7&hourly_hours=24&units=imperial"
PASS = re.compile(r"^(content-type|retry-after|ratelimit-.*)$", re.I)  # headers your app sees
OUTSIDE_COVERAGE_TTL = 3600  # a point outside coverage stays outside
DEADLINE = 10  # seconds for the whole upstream call, body included
WANT = hashlib.sha256(APP_TOKEN.encode()).digest()

cache = {}  # "lat,lon" -> (expires, status, headers, body)
inflight = {}  # "lat,lon" -> Pending, shared by every request for it while one thread calls upstream
guard = threading.Lock()  # guards cache, inflight and throttled
pool = concurrent.futures.ThreadPoolExecutor(max_workers=8)  # runs upstream calls under DEADLINE
throttled = None  # the last 429, answered locally until its Retry-After passes
FAILED = (OSError, http.client.HTTPException, concurrent.futures.TimeoutError)

class Pending:
    def __init__(self):
        self.done, self.entry, self.error = threading.Event(), None, None

def coord(values, limit):
    try:
        n = float(values[0])
    except (TypeError, ValueError, IndexError):
        return None
    return f"{n:.4f}" if abs(n) <= limit else None

def max_age(cc):
    m = re.search(r"(?:^|,)\s*max-age=(\d+)", cc or "", re.I)
    return 0 if not m or "no-store" in cc.lower() else int(m.group(1))

def problem_type(body):
    try:
        return json.loads(body).get("type", "")
    except ValueError:
        return ""

def read_body(res, deadline):
    chunks = []
    while chunk := res.read1(65536):  # whatever has arrived, so a slow drip hits the deadline
        if time.monotonic() > deadline:
            raise TimeoutError("upstream took longer than DEADLINE")
        chunks.append(chunk)
    body = b"".join(chunks)
    length = res.headers.get("Content-Length")
    if length and length.isdigit() and len(body) != int(length):
        raise http.client.IncompleteRead(body, int(length) - len(body))
    return body

def upstream(lat, lon):
    deadline = time.monotonic() + DEADLINE
    req = urllib.request.Request(f"{BASE}/v1/weather?lat={lat}&lon={lon}&{QUERY}",
                                 headers={"Authorization": f"Bearer {KEY}"})
    try:
        with urllib.request.urlopen(req, timeout=DEADLINE) as res:
            status, hdrs, body = res.status, res.headers, read_body(res, deadline)
            ttl = max_age(hdrs["Cache-Control"])
    except urllib.error.HTTPError as err:
        status, hdrs, body, ttl = err.code, err.headers, err.read(), 0
    if status == 404 and problem_type(body).endswith("outside-coverage"):
        ttl = OUTSIDE_COVERAGE_TTL
    if status == 429:
        ttl = int(hdrs["Retry-After"]) if (hdrs["Retry-After"] or "").isdigit() else 1
    headers = {k: v for k, v in hdrs.items() if PASS.match(k)}
    return (time.time() + ttl, status, headers, body)

def store(key, entry):
    global throttled
    # RateLimit-* describe one upstream call, so a stored copy keeps just the type.
    types = {k: v for k, v in entry[2].items() if k.lower() == "content-type"}
    kept = (entry[0], entry[1], types, entry[3])
    with guard:
        now = time.time()
        for k in [k for k, v in cache.items() if v[0] <= now]:
            del cache[k]
        if entry[1] == 429:
            throttled = kept
        elif entry[0] > now:
            cache[key] = kept

def weather(key, lat, lon):
    hit = cache.get(key)
    if hit and hit[0] > time.time():
        return hit
    with guard:
        hit = cache.get(key)
        if hit and hit[0] > time.time():
            return hit
        if throttled and throttled[0] > time.time():
            return throttled
        pending = inflight.get(key)
        owner = pending is None
        if owner:
            pending = inflight[key] = Pending()
    if not owner:  # another request is already calling upstream for this point
        pending.done.wait()
        if pending.error:
            raise pending.error
        return pending.entry
    try:
        pending.entry = pool.submit(upstream, lat, lon).result(timeout=DEADLINE)
        store(key, pending.entry)
        return pending.entry
    except Exception as err:
        pending.error = err
        raise
    finally:
        with guard:
            inflight.pop(key, None)
        pending.done.set()

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        url = urllib.parse.urlsplit(self.path)
        if url.path != "/weather":
            return self.send_error(404)
        got = hashlib.sha256(self.headers.get("X-App-Token", "").encode()).digest()
        if not hmac.compare_digest(got, WANT):  # constant time on equal-length digests
            return self.send_error(401, "sign in first")
        q = urllib.parse.parse_qs(url.query)
        lat, lon = coord(q.get("lat"), 90), coord(q.get("lon"), 180)
        if lat is None or lon is None:
            return self.send_error(400, "lat and lon must be numbers")
        try:
            expires, status, headers, body = weather(f"{lat},{lon}", lat, lon)
        except FAILED:
            return self.send_error(502, "weather unavailable")
        self.send_response(status)
        for name, value in headers.items():
            if not (status == 429 and name.lower() == "retry-after"):
                self.send_header(name, value)
        if status == 429:
            self.send_header("Retry-After", str(max(1, math.ceil(expires - time.time()))))
        self.end_headers()
        self.wfile.write(body)

ThreadingHTTPServer(("", 8080), Handler).serve_forever()

Both round lat and lon to 4 decimal places (about 11 m), so nearby requests share one cached answer. Other errors pass through to your app with their status and are not cached.

2. Send the recommended query

GET /v1/weather?lat=42.3601&lon=-71.0589&view=concise&days=7&hourly_hours=24&units=imperial
  • view=concise returns a documented subset of the hourly[] and daily[] columns, enough for a forecast screen. The subset only grows. view=detailed (the default) returns every column.
  • days=7 returns a week of daily rows, and hourly_hours=24 the next 24 hourly rows.
  • units=imperial (the default) or units=metric. Read the labels from the response's units object (for example units.temperature is F or C) instead of hard-coding them.

The full list of query parameters is on the Weather endpoint page.

3. Cache each answer for its max-age

Every successful /v1/weather answer carries a Cache-Control header. Keep the body on your backend for the max-age that header gives, keyed on the coordinates and the query, and call the API again once it expires. Parse the header on each response rather than hard-coding a lifetime: the value can differ between answers, and an answer built on a degraded upstream source says no-store, which means do not cache it at all. A cached answer never reaches the API, so it does not count against your rate limit or your credits.

Error responses (application/problem+json) carry no Cache-Control, so decide per type:

  • outside-coverage (404). It does not change for a point, so keep it. The backends above keep it for an hour.
  • 429. Wait for Retry-After before any further call; the limit is per key, not per location.
  • Others. Do not cache them.

4. Read times in the location's timezone

  • hourly[].time is an ISO 8601 timestamp in UTC. Convert it to location.timezone (an IANA name such as America/New_York) for display, not to the device's timezone.
  • daily[].date is a calendar date (2026-04-20) that is already local to location.timezone. Display it as is. Parsing it as a timestamp reads it as UTC midnight, which shows the previous day west of Greenwich.
  • Label daily rows with relative_day (-1 yesterday, 0 today, 1 tomorrow) instead of comparing dates with the device clock. Select today by relative_day == 0, not by position: before 6 AM local, an extra relative_day: -1 row for the overnight still in progress comes first, and its high can be null.
  • active_period is day or night on exactly the row that holds the current local time, and null on every other row. Use it to choose between "Today" and "Tonight".

5. Pick an icon

icon is a string slug from an open vocabulary: new slugs can appear without a new API version, so never treat the list below as closed. Each slug is a family name, sometimes followed by -day or -night. The families today are:

clear, partly-cloudy, mostly-cloudy, cloudy, showers, rain, thunder-showers, thunder-rain, thunderstorms, snow-showers, snow, rain-snow-showers, rain-snow, sleet, fog and haze.

  • Suffixes. clear, partly-cloudy, mostly-cloudy, showers, thunder-showers, snow-showers and rain-snow-showers always carry -day or -night, for example partly-cloudy-night. haze can appear with or without one. The other families have none.
  • Which suffix. current.icon and hourly[].icon follow the sun at the location: -day between sunrise and sunset. daily[].icon and daily[].day.icon are worked out for local noon, so they are normally day variants; daily[].night.icon is worked out for late evening, so it is normally a night variant. With view=concise, daily[].day and daily[].night carry only precip_probability: day.icon, night.icon, their conditions and narrative, and every other day/night field need view=detailed.
  • Fallback. For a slug you have no artwork for, strip a trailing -day or -night, then match the longest family name the slug starts with. If nothing matches, show the row's conditions text (for example Partly Cloudy).

6. Show the attribution notice

Display the attribution.notice string from each response wherever you show its data, as returned, and link smarterweather.com/acknowledgements next to it. Read the notice from the response instead of copying it into your app, so a change to the wording reaches your users without a release. attribution.sources lists the sources that contributed, for your own records.

7. Stay inside coverage

Smarter Weather covers the 50 US states and DC only. Every route that takes a point (lat and lon, zip or location) returns 404 outside-coverage for a point outside the 50 states and DC. /v1/alerts and /v1/observations return it for a bounding box that touches no US domain (CONUS, Alaska, Hawaii); a box that touches one may still include area outside the US. /v1/weather/batch reports it per item as outside_coverage, and /v1/point with points and /v1/series/batch mark such points out_of_domain, as they do a point the check could not reach in time (retry it). If the location lookup is unavailable, the check can briefly fail open. Routes keyed by a station, radar site, forecast office or alert id, and nationwide lists, are not filtered by point, so do not treat an empty result as an all-clear. See Errors.

The US territories (Puerto Rico, the US Virgin Islands, Guam, the Northern Mariana Islands and American Samoa) and other countries are outside it.

A point outside coverage gets a 404 with the outside-coverage problem type. Show your own "not covered" message for it, and do not retry it.

8. Look up a field

The Weather field reference lists every field of every object the /v1/weather body can carry, with its type, whether it can be null and whether it is always present, and the unit each number is in. It is generated from openapi.yaml, the full contract.

Weather field reference

Every object the GET /v1/weather body can carry, generated from openapi.yaml at build. Tables show the detailed view: view=concise returns a subset of the hourly[] and daily[] columns, and in it daily[].day and daily[].night carry only precip_probability. A section appears only when it is included; opt-in sections say which include token adds them. "Always present" means the schema requires the field.

Units by field

The list covers current, hourly[], daily[] and daily[].day / night; the Unit column in each table shows the same.

Converted to the unit system you asked for; read the label from the response's units object:

  • units.temperature: temperature, feels_like, dew_point, high, low, feels_like_high, feels_like_low, dew_point_max, dew_point_min.
  • units.wind_speed: wind_speed, wind_gust, wind_speed_max, wind_gust_max.
  • units.pressure: pressure, pressure_avg.
  • units.visibility: visibility, visibility_min.
  • units.precip_amount: precip_amount, precip_total, snow_amount, snow_total, ice_total.
  • units.precip_amount per hour: precip_rate.
  • units.altitude: snow_level, cloud_ceiling, cloud_ceiling_min.
  • units.solar_radiation: solar_radiation_max.

Fixed, whatever units says, as are fields whose name ends in a unit suffix (_ft, _km, _m, _mm, _prob):

  • percent (0 to 100): humidity, humidity_avg, cloud_cover, cloud_cover_avg, cloud_cover_low, cloud_cover_mid, cloud_cover_high, precip_probability, thunderstorm_probability, thunderstorm_probability_max, precip_type_rain_prob, precip_type_snow_prob, precip_type_freezing_rain_prob, precip_type_ice_pellets_prob, lightning_prob.
  • degrees (0 = N, 90 = E): wind_direction.
  • mm: precip_24h.
  • J/kg: cape, cape_max.
  • strikes per unit area per time window: lightning_density.
  • none (AQI scale): aqi, aqi_max, pm25, pm10, ozone.
  • none (UV index): uv_index, uv_index_max.
  • whole days from today, local: relative_day.

Top level (Weather)

Field Type Unit Nullable Always present Description
location Location no yes Resolved location.
current Current no no Current weather conditions derived from the RTMA + MRMS blend.
hourly Hourly[] no no Hourly forecast, ordered ascending by time.
daily Daily[] no no Daily forecast, ordered ascending by date.
alerts Alert[] no no Active NWS alerts for the location, present whenever the section is included and its fetch succeeded.
outlooks Outlook[] no no SPC convective outlooks active for the location, present whenever the section is included and its fetch succeeded.
astro Astro no no Astronomical data for the requested location and date.
gis GIS no yes Geographic metadata for the resolved location.
units Units no no Unit labels for the numeric fields in this response.
attribution Attribution no yes Attribution for the data sources that contributed to this response.
narrations Narrations no no Rule-based narrative text for current and short-term conditions, generated deterministically from the blended forecast (not an LLM).
guidance Guidance no no NWS forecaster text products (AFD, HWO, NOW, HLS, MCD, MPD, PMD) for the point: structured fields plus raw bulletin body when available.
discussions Discussions no no Deprecated.
nowcast Nowcast no no Minute-scale MRMS radar precipitation nowcast for the queried location.
climate ClimateNormals no no Day-of-year climate normals for the location, populated when include=climate is requested.
metar Metar no no Compact nearest METAR observation for the queried point.
meta Meta no yes Per-response metadata: response schema version, generation time, upstream data freshness, and any non-fatal warnings.

location, climate.location (Location)

Field Type Unit Nullable Always present Description
lat number no yes
lon number no yes
name string no yes Human-readable place name, usually without the state; see region.
region string no no State or region abbreviation.
country string no no ISO 3166-1 alpha-2 country code.
timezone string no yes IANA timezone identifier.
county string no no County name (US only).
zip string no no ZIP code (US only).

current (Current)

Field Type Unit Nullable Always present Description
time string (date-time) no yes Observation time (ISO 8601, UTC).
temperature number units.temperature yes no Temperature (unit per units.temperature).
feels_like number units.temperature yes no Apparent temperature (wind chill or heat index).
humidity number percent (0 to 100) yes no Relative humidity, percent.
dew_point number units.temperature yes no
pressure number units.pressure yes no Atmospheric pressure (unit per units.pressure).
wind_speed number units.wind_speed yes no
wind_gust number units.wind_speed yes no
wind_direction integer degrees (0 = N, 90 = E) yes no Wind direction in degrees (0 = N, 90 = E).
visibility number units.visibility yes no Visibility distance (null when no station or model value is available).
cloud_cover number percent (0 to 100) yes no Cloud cover, percent.
cloud_ceiling number units.altitude yes no Analysis cloud-base height above ground (RTMA/URMA), in the units.altitude unit.
cloud_cover_low number percent (0 to 100) yes no HRRR low-cloud-layer cover, percent (CONUS).
cloud_cover_mid number percent (0 to 100) yes no HRRR mid-cloud-layer cover, percent (CONUS).
cloud_cover_high number percent (0 to 100) yes no HRRR high-cloud-layer cover, percent (CONUS).
uv_index number none (UV index) yes no
aqi integer none (AQI scale) yes no Air-quality index (US EPA scale): usually 0-500, can exceed 500 in extreme events ("Beyond the AQI").
aqi_category string no no Human-readable AQI category (e.g., "Good", "Moderate", "Unhealthy for Sensitive Groups").
pm25 number none (AQI scale) yes no AirNow AQI sub-index for PM2.5, on the same scale as aqi: usually 0-500, can exceed 500 in extreme events.
ozone number none (AQI scale) yes no AirNow AQI sub-index for ozone, on the same scale as aqi: usually 0-500, can exceed 500 in extreme events.
pm10 number none (AQI scale) yes no AirNow AQI sub-index for PM10, on the same scale as aqi: usually 0-500, can exceed 500 in extreme events.
precip_rate number units.precip_amount per hour yes no Current precipitation rate.
precip_type string no no Current precipitation type.
precip_24h number mm yes no Observed precipitation accumulated over the trailing 24 hours, from MRMS MultiSensor QPE Pass 2.
lightning_density number strikes per unit area per time window yes no Lightning strike density (strikes per unit area per time window).
lightning_prob integer percent (0 to 100) yes no Lightning probability, percent.
conditions string no yes Human-readable weather description.
icon string no yes Icon slug for UI rendering.
severe boolean no no True when severe conditions are present or imminent.

hourly[] (Hourly)

Field Type Unit Nullable Always present Description
time string (date-time) no yes Forecast valid time (ISO 8601, UTC).
temperature number units.temperature yes no
feels_like number units.temperature yes no
humidity number percent (0 to 100) yes no
wind_speed number units.wind_speed yes no
wind_direction integer degrees (0 = N, 90 = E) yes no
precip_probability number percent (0 to 100) yes no Probability of measurable precipitation (0–100) for the hour.
precip_amount number units.precip_amount yes no Liquid-equivalent precipitation for the hour ending at time.
conditions string no no
icon string no no
severe boolean no no
wind_gust number units.wind_speed yes no
dew_point number units.temperature yes no
cloud_cover number percent (0 to 100) yes no
visibility number units.visibility yes no
pressure number units.pressure yes no
thunderstorm_probability number percent (0 to 100) yes no
uv_index number none (UV index) yes no
aqi number none (AQI scale) yes no
precip_type string no no
precip_type_rain_prob number percent (0 to 100) yes no
precip_type_snow_prob number percent (0 to 100) yes no
precip_type_freezing_rain_prob number percent (0 to 100) yes no
precip_type_ice_pellets_prob number percent (0 to 100) yes no
cape number J/kg yes no Convective available potential energy (J/kg).
snow_amount number units.precip_amount yes no Null when no snow is forecast or no source covers the hour.
cloud_ceiling number units.altitude yes no NBM forecast cloud-base height above ground, in the units.altitude unit.
cloud_cover_low number percent (0 to 100) yes no HRRR low-cloud-layer cover, percent (CONUS).
cloud_cover_mid number percent (0 to 100) yes no HRRR mid-cloud-layer cover, percent (CONUS).
cloud_cover_high number percent (0 to 100) yes no HRRR high-cloud-layer cover, percent (CONUS).
snow_level number units.altitude yes no
thunder_possible boolean no no True when any hourly thunder indicator (thunderstorm_probability or cape) crosses the blender's lightning-risk threshold for this hour.
uncertainty HourlyUncertainty yes no Optional probabilistic sub-object sourced from the NBM model.

daily[] (Daily)

Field Type Unit Nullable Always present Description
date string (date) no yes Forecast date (ISO 8601, local to the location's timezone).
high number units.temperature yes no Daytime maximum.
low number units.temperature yes no Overnight minimum, covering 00:00–05:59 of the following calendar date.
conditions string no no
icon string no no
severe boolean no no
precip_probability number percent (0 to 100) yes no
precip_total number units.precip_amount yes no Null when no forecast source covers any hour of the day.
snow_total number units.precip_amount yes no Null when no forecast source covers any hour of the day.
ice_total number units.precip_amount yes no Null when no forecast source covers any hour of the day.
wind_speed_max number units.wind_speed yes no
wind_gust_max number units.wind_speed yes no Maximum sustained-gust value across the calendar day.
wind_direction integer degrees (0 = N, 90 = E) yes no
humidity_avg number percent (0 to 100) yes no
cloud_cover_avg number percent (0 to 100) yes no
uv_index_max number none (UV index) yes no The day's maximum UV index.
cape_max number J/kg yes no
dew_point_max number units.temperature yes no
dew_point_min number units.temperature yes no
pressure_avg number units.pressure yes no
visibility_min number units.visibility yes no
cloud_ceiling_min number units.altitude yes no
thunderstorm_probability_max number percent (0 to 100) yes no
aqi_max number none (AQI scale) yes no
feels_like_high number units.temperature yes no
feels_like_low number units.temperature yes no
solar_radiation_max number units.solar_radiation yes no Peak downward shortwave radiation across the day (W/m^2).
snow_level number units.altitude yes no
thunder_possible boolean no no Distilled flag; true if any hour in the day crosses the blender's lightning-risk threshold.
astro Astro no no Astronomical data for the requested location and date.
day DailyPeriod no no Forecast data for a single period (day or night) within a Daily entry.
night DailyPeriod no no Forecast data for a single period (day or night) within a Daily entry.
threats DailyThreat[] no no Per-day hazard callouts (winter, severe, heat, etc.).
relative_day integer whole days from today, local no no Signed calendar offset from the local current date at request time (-1 = yesterday, 0 = today, 1 = tomorrow).
active_period string yes no Non-null on exactly the row containing the current local time (day during 06:00–17:59, night during 18:00–05:59).
uncertainty DailyUncertainty yes no Optional NBM probabilistic sub-object for this day.
climate DailyClimate yes no Optional climate sub-object for this day.

alerts[] (Alert)

Field Type Unit Nullable Always present Description
id string no yes Unique alert identifier (the NWS message id).
event string no yes
severity string no yes
urgency string no yes
certainty string no yes
headline string no yes
sender string no yes Issuing NWS office identifier.
status string no yes Alert lifecycle status (e.g., Actual, Exercise, Test).
onset string (date-time) no yes
ends string (date-time) no yes
effective string (date-time) no yes
expires string (date-time) no yes
expiration string (date-time) no yes Final expiration time used for cache invalidation.
area_desc string no no Human-readable affected area.
highlights map of string no no Map of salient callouts extracted from the alert text (e.g., {"hazard": "Strong winds", "impact": "Loose debris possible"}).

outlooks[] (Outlook)

Field Type Unit Nullable Always present Description
product_id string no yes Upstream product identifier (e.g., spc_otlk_day1).
type string no yes Hazard category (severe, tornado, hail, wind, etc.).
day integer no yes Day number relative to issuance (1 = today).
risk string no no Raw risk identifier from the upstream source.
probability number no no Fractional probability (0-1) when the source provides one.
risk_label string no yes Human-readable risk label.
valid string (date-time) no yes
expires string (date-time) no yes
significant boolean no no The location falls inside SPC's hatched significant-severe area for this hazard — tornadoes of EF2+, hail 2 in or larger, or wind gusts of 65 kt or more.

astro, daily[].astro (Astro)

Field Type Unit Nullable Always present Description
civil_dawn string (date-time) yes no Null when civil twilight does not begin or end on this date.
sunrise string (date-time) yes no Null on polar day and night (see polar).
solar_noon string (date-time) yes no
sunset string (date-time) yes no Null on polar day and night (see polar).
civil_dusk string (date-time) yes no Null when civil twilight does not begin or end on this date.
day_length integer no yes Seconds from sunrise to sunset; 86400 under midnight sun, 0 in polar night.
daylight_length integer no yes Seconds from civil dawn to civil dusk; 86400 when civil twilight never ends (white nights, midnight sun).
moonrise string (date-time) yes no Null when no moonrise occurs on this date.
moonset string (date-time) yes no Null when no moonset occurs on this date.
moon_phase number no yes Moon phase (0 = new, 0.5 = full, 1 = new).
moon_phase_name string no yes
moon_illumination number no yes
polar string yes no Set when the sun neither rises nor sets on this date: midnight_sun (above the horizon all day; day_length is 86400) or polar_night (below it all day; day_length is 0).

gis (GIS)

Field Type Unit Nullable Always present Description
lat number no yes
lon number no yes
city string no no City name covering the point (if the point lies inside city limits).
nearest_city string no no Nearest named city (useful when city is not populated).
state string no no State or equivalent subdivision (abbreviation).
county string no no
country string no no ISO 3166-1 alpha-2 country code.
population integer yes no Approximate population of city or nearest_city.
timezone string no yes IANA timezone identifier.
nearest_radar NearestRadar no no Metadata for the nearest NEXRAD radar site.
nearest_station NearestStation no no Metadata for the nearest METAR observation station.

units (Units)

Dimension Labels Description
temperature F, C Temperature unit.
wind_speed mph, kph
pressure inHg, hPa
visibility mi, km
precip_amount in, mm
altitude m, ft Unit for altitude-dimensioned fields (snow_level, cloud_ceiling).
solar_radiation W/m^2 Unit for irradiance fields (currently always W/m^2).

attribution (Attribution)

Field Type Unit Nullable Always present Description
sources string[] no yes Canonical source identifiers that contributed data.
notice string no yes Human-readable attribution notice.

meta (Meta)

Field Type Unit Nullable Always present Description
schema_version string no yes Response schema version, a semver string (currently 1.0.0).
generated_at string (date-time) no yes Server time at which this response body was generated.
freshness SourceFreshness[] no no Freshness metadata for each upstream data source used.
warnings MetaWarning[] no no Non-fatal warnings generated during request fulfillment.
hourly_sampling object no no Present only when hourly_step dropped at least one hourly row.

daily[].day, daily[].night (DailyPeriod)

Field Type Unit Nullable Always present Description
temperature number units.temperature yes no High for day, low for night; null when the half has no temperature.
wind_speed number units.wind_speed yes no
wind_direction integer degrees (0 = N, 90 = E) yes no
precip_probability number percent (0 to 100) yes no Probability of measurable precipitation (0–100) for the day or night period.
precip_total number units.precip_amount yes no Null when no forecast source covers any hour of the day.
snow_total number units.precip_amount yes no Null when no forecast source covers any hour of the day.
ice_total number units.precip_amount yes no Null when no forecast source covers any hour of the day.
humidity number percent (0 to 100) yes no
cloud_cover number percent (0 to 100) yes no
wind_gust number units.wind_speed yes no
dew_point number units.temperature yes no
feels_like number units.temperature yes no High-for-day / low-for-night of hourly apparent temperature.
visibility number units.visibility yes no
pressure number units.pressure yes no
cloud_ceiling number units.altitude yes no
uv_index number none (UV index) yes no Max UV index within the period (day only; ~0 overnight).
thunderstorm_probability number percent (0 to 100) yes no
cape number J/kg yes no
aqi number none (AQI scale) yes no
precip_type string no no
thunder_possible boolean no no
conditions string no no
icon string no no
severe boolean no no
narrative string no no Human-readable narrative for this period.

daily[].threats[] (DailyThreat)

Field Type Unit Nullable Always present Description
type string no yes Hazard category (e.g., severe, winter, heat, fire).
risk string no no Raw risk identifier from the upstream source.
probability number no no Fractional probability (0-1) when known.
risk_label string no yes Human-readable risk label (e.g., "Slight", "Enhanced").

gis.nearest_radar (NearestRadar)

Field Type Unit Nullable Always present Description
id string no yes Radar site code without ICAO prefix (e.g., LOT, OKX, AKC).
name string no yes
lat number no yes
lon number no yes
distance_km number km no yes Great-circle distance from the requested point to the radar site, rounded to 0.1 km.

gis.nearest_station (NearestStation)

Field Type Unit Nullable Always present Description
id string no yes ICAO station identifier (e.g., KJFK).
name string no no
state string no no
lat number no yes
lon number no yes
distance_km number km no yes Great-circle distance from the requested point to the station, rounded to 0.1 km.
elevation_m number m yes no

meta.freshness[] (SourceFreshness)

Field Type Unit Nullable Always present Description
source string no yes Source family (e.g., rtma, hrrr, nbm, mrms).
product_id string no yes Specific product identifier.
reference_time string (date-time) yes no Model run / observation reference time.
latest_valid string (date-time) yes no Latest valid time represented in the source.
age_seconds integer no yes Seconds elapsed since reference_time.

meta.warnings[] (MetaWarning)

Field Type Unit Nullable Always present Description
code string no yes Stable machine-parseable warning code.
section string no yes Response section the warning applies to (e.g., "alerts", "hourly", "outlooks").
detail string no no Human-readable explanation.
product_id string no no Catalog product id (GET /v1/catalog/{product_id}) of the upstream product the warning is about; the same id as the product's meta.freshness entry.
fields string[] no no Response fields in section that this warning leaves null or omits.

meta.hourly_sampling

Field Type Unit Nullable Always present Description
step_hours integer no yes Effective hourly_step.
from_hour integer no yes Effective hourly_step_from.

narrations (Narrations), with include=narrations

Field Type Unit Nullable Always present Description
current string no no Time-of-day-aware summary of current conditions.
short_term string no no Narrative covering the next few hours.

guidance (Guidance), with include=guidance

Field Type Unit Nullable Always present Description
afd AFDGuidance no no Area Forecast Discussion: structured fields plus optional raw bulletin body.
hwo HWOGuidance no no Hazardous Weather Outlook: structured fields plus optional raw bulletin body.
now NOWGuidance no no WFO short-term NOW product (NWS NOW, not radar nowcast): pipeline LLM summary plus optional raw bulletin body.
hls HLSGuidance no no WFO Hurricane Local Statement for the office covering the point: pipeline LLM summary plus optional raw bulletin body.
active_mcds integer no yes Count of non-expired SPC Mesoscale Discussions whose polygons overlap the location (matches mcds length when present).
mcds MCDGuidance[] no no SPC Mesoscale Discussions overlapping the location (structured fields plus raw body).
active_mpds integer no no Count of non-expired WPC Mesoscale Precipitation Discussions whose polygons overlap the location (matches mpds length when present).
mpds MPDGuidance[] no no WPC Mesoscale Precipitation Discussions overlapping the location.
pmds PMDGuidance[] no no Latest national Prognostic Discussion per enabled desk: WPC (PMDSPD, PMDEPD, PMDHI, PMDCA) and CPC (PMDMRD 6-10/8-14 day, PMDTHR week-2 hazards, PMDENS ENSO).
swo_day1_risk string no no SPC Day-1 convective outlook categorical risk label (when active).

discussions (Discussions), with include=discussions

Field Type Unit Nullable Always present Description
mesoscale MCDGuidance[] no yes Active SPC Mesoscale Discussions whose polygons overlap the location.
area_forecast AFDGuidance no no Latest Area Forecast Discussion for the WFO covering the point.

nowcast (Nowcast), with include=nowcast

Field Type Unit Nullable Always present Description
interval_seconds integer no no Length of each step, in seconds: the most common spacing between the frame's lead times (300 today).
generated_at string (date-time) no no Nowcast reference (anchor) time from the latest radar frame.
summary NowcastSummary no no Server-computed headline for the first precipitation episode in the nowcast window, derived from the frame's steps.
points NowcastPoint[] no no

climate (ClimateNormals), with include=climate

Field Type Unit Nullable Always present Description
station ClimateNormalsStation no yes The station a normals series was drawn from.
meta ClimateNormalsMeta no yes Provenance for a normals response.
days ClimateNormalsDay[] no yes Per-day normals series, in requested date order.
location Location no no Resolved location, as /v1/weather returns it.

metar (Metar), with include=metar

Field Type Unit Nullable Always present Description
station_id string no yes
distance_km number km no yes Great-circle distance from the requested point to the station, rounded to 0.1 km.
observed_at string (date-time) no yes
age_min number no yes Minutes between observed_at and response meta.generated_at.
ceiling_ft integer ft no no Lowest BKN/OVC/VV base, feet AGL.
layers MetarSkyLayer[] no no Parsed sky groups.
weather string[] no no Raw present-weather tokens (-RA, VCTS, TSRA).

hourly[].uncertainty (HourlyUncertainty), with include=uncertainty

Field Type Unit Nullable Always present Description
temperature_stddev number yes no Ensemble standard deviation of temperature.
dew_point_stddev number yes no
wind_speed_stddev number yes no
wind_gust_stddev number yes no
visibility_prob_below_1mi number yes no
visibility_prob_below_2mi number yes no
visibility_prob_below_3mi number yes no
visibility_prob_below_5mi number yes no
ceiling_prob_below_500ft number yes no
ceiling_prob_below_1000ft number yes no
ceiling_prob_below_2000ft number yes no
ceiling_prob_below_3000ft number yes no
ceiling_prob_below_6600ft number yes no
wind_speed_p50 number yes no NBM median 10m wind speed.
wind_speed_prob_gt_75mph number yes no

daily[].uncertainty (DailyUncertainty), with include=uncertainty

Field Type Unit Nullable Always present Description
temperature_stddev_avg number yes no
dew_point_stddev_avg number yes no
wind_speed_stddev_avg number yes no
wind_gust_stddev_avg number yes no
visibility_prob_below_1mi_max number yes no
ceiling_prob_below_1000ft_max number yes no
snow_total_p10 number yes no
snow_total_p50 number yes no
snow_total_p90 number yes no
snow_prob_gt_1in number yes no
snow_prob_gt_2in number yes no
snow_prob_gt_4in number yes no

daily[].climate (DailyClimate), with include=climate

Field Type Unit Nullable Always present Description
normal_high number yes no Normal daily maximum temperature for this day of year.
normal_low number yes no Normal daily minimum temperature for this day of year.
high_departure number yes no Forecast high minus normal high; positive is warmer than normal.
low_departure number yes no Forecast low minus normal low; positive is warmer than normal.

guidance.afd, discussions.area_forecast (AFDGuidance), with include=guidance or include=discussions

Field Type Unit Nullable Always present Description
synopsis string no no
key_takeaway string no no
confidence string no no Forecaster-stated confidence level (free text).
issued_at string (date-time) yes no
wfo string no no Issuing Weather Forecast Office identifier.
body string no no Full AFD bulletin text when available.

guidance.hwo (HWOGuidance), with include=guidance

Field Type Unit Nullable Always present Description
day_one string no no Day-one hazard summary text.
summary string no no Pipeline LLM overall hazard summary when enrichment has run.
has_active_hazards boolean no yes
issued_at string (date-time) yes no
body string no no Full HWO bulletin text when available.

guidance.now (NOWGuidance), with include=guidance

Field Type Unit Nullable Always present Description
summary string no no Pipeline LLM summary when enrichment has run.
issued_at string (date-time) yes no
wfo string no no Issuing Weather Forecast Office identifier.
body string no no Full NOW bulletin text when available.

guidance.hls (HLSGuidance), with include=guidance

Field Type Unit Nullable Always present Description
summary string no no Pipeline LLM summary when enrichment has run.
issued_at string (date-time) yes no
wfo string no no Issuing Weather Forecast Office identifier.
body string no no Full HLS bulletin text when available.

guidance.mcds[], discussions.mesoscale[] (MCDGuidance), with include=guidance or include=discussions

Field Type Unit Nullable Always present Description
mcd_number integer no no SPC mesoscale discussion number.
issued_at string (date-time) yes no
expires_at string (date-time) yes no End of the product valid window when parsed.
concerning string no no SPC "Concerning..." line (e.g. severe potential / watch possible).
areas_affected string no no
watch_probability integer no no Stated probability of watch issuance, percent.
summary string no no Pipeline LLM summary when enrichment has run.
body string no no Full MCD bulletin text when available.

guidance.mpds[] (MPDGuidance), with include=guidance

Field Type Unit Nullable Always present Description
mpd_number integer no no WPC mesoscale precipitation discussion number.
issued_at string (date-time) yes no
expires_at string (date-time) yes no
concerning string no no
areas_affected string no no
flash_flood_tier string no no Headline tier (possible / likely / occurring).
summary string no no
body string no no

guidance.pmds[] (PMDGuidance), with include=guidance

Field Type Unit Nullable Always present Description
awips_id string no no Full AWIPS identifier (e.g. PMDSPD).
pmd_desk string no no Desk suffix after the PMD prefix (SPD, EPD, HI, CA, MRD, THR, ENS).
issued_at string (date-time) yes no
summary string no no Pipeline LLM summary when enrichment has run.
body string no no Full PMD bulletin text when available.

nowcast.summary (NowcastSummary), with include=nowcast

Field Type Unit Nullable Always present Description
starts_at string (date-time) no no time of the onset step: the first of two consecutive wet steps.
ends_at string (date-time) yes no End of the last wet step of the episode: that step's time plus interval_seconds.
duration_minutes integer yes no Minutes from starts_at to ends_at, so a multiple of the step length (at least 10 for a two-step episode).
type string no no Precipitation type of the onset step, as on points[].type (rain, snow, freezing_rain, wintry_mix, or unknown for an unrecognized radar type code), or none when there is no onset.
peak_intensity string no no Highest intensity across the episode, from the onset to its end (or to the end of the window while it continues).
already_occurring boolean no no True when starts_at is at or before the request time: the onset step has begun.

nowcast.points[] (NowcastPoint), with include=nowcast

Field Type Unit Nullable Always present Description
time string (date-time) no yes
type string no yes Radar precipitation type at this step: none, rain, snow, freezing_rain, wintry_mix, no_coverage (outside radar coverage), or unknown for an unrecognized radar type code.
intensity_dbz number no yes
intensity string no yes

climate.station (ClimateNormalsStation), with include=climate

Field Type Unit Nullable Always present Description
station_id string no yes GHCN station identifier.
name string no no Station name as published by NCEI.
state string no no Two-letter state or territory code.
latitude number no yes Station latitude.
longitude number no yes Station longitude.
elevation_m number m no no Station elevation in metres.
distance_km number km no yes Great-circle distance from the requested point to the station, rounded to 0.1 km.

climate.meta (ClimateNormalsMeta), with include=climate

Field Type Unit Nullable Always present Description
normals_period string no no Base period the normals were computed over.
source string no yes Where the values came from: station for the NCEI station product, grid for gridded normals, mixed when a series draws on both.
normals_method string no no How the normals were produced, which source does not answer.
unit string no yes Unit system applied to the values: °F and inches for imperial, °C and millimetres for metric.
approximate boolean no no True when a station more than 50 km away supplied values that are actually being served — station.distance_km says how far.
missing_days integer no no How many days in the requested range no normal is available for, from either the grid or the station.

climate.days[] (ClimateNormalsDay), with include=climate

Field Type Unit Nullable Always present Description
date string (date) no yes Calendar date this slice was requested for.
month integer no yes Month of the day-of-year slice (1-12).
day integer no yes Day of month (1-31).
normal_high number no no Average daily maximum temperature.
normal_low number no no Average daily minimum temperature.
normal_mean number no no Average daily mean temperature.
normal_high_stddev number no no Standard deviation of the daily maximum.
normal_low_stddev number no no Standard deviation of the daily minimum.
normal_mean_stddev number no no Standard deviation of the daily mean.
normal_precip number no no Average daily precipitation amount — inches when unit=imperial, millimetres when unit=metric.
years integer no no Years of the base period contributing to this day, at the station.
completeness_flag string no no NCEI per-value completeness code, for the station.

metar.layers[] (MetarSkyLayer), with include=metar

Field Type Unit Nullable Always present Description
cover string no yes
base_ft integer ft no no Layer base in feet AGL.
convective string no no