{
  "openapi": "3.1.0",
  "info": {
    "title": "mswx — Monte Sano hyperlocal weather API",
    "version": "1.0.0",
    "description": "Public read-only JSON for the Monte Sano plateau (Huntsville, AL). Same data that powers mswx.net. No auth, no key, CORS open. Cache the responses — see each endpoint's Cache-Control.",
    "contact": {
      "name": "mswx",
      "url": "https://mswx.net"
    }
  },
  "servers": [
    {
      "url": "https://mswx.net"
    }
  ],
  "paths": {
    "/api/v1/now": {
      "get": {
        "summary": "Current conditions snapshot",
        "description": "One round-trip for the home screen: plateau (MSWX composite) temp/RH, valley (Downtown) temp, pool water temp, sun times, sky/cloud, today & tomorrow rain chance, today's plateau rainfall so far (`rain.today_in`, inches since Chicago midnight, composited across the plateau rain gauges), thunder outlook, live lightning, and the live radar rain-nowcast summary (`rain_now`). Refreshes every ~5 min.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "example": {
                  "as_of": "2026-05-25T19:00:00.000Z",
                  "mswx": {
                    "temp_f": 74.2,
                    "rh_pct": 61,
                    "today_high_f": 76.6,
                    "today_low_f": 66.2,
                    "n_stations": 5
                  },
                  "valley": {
                    "temp_f": 79.9,
                    "station_id": "HSVWX",
                    "label": "Downtown",
                    "n_stations": 2,
                    "obs_time": "2026-05-25T18:55:00.000Z"
                  },
                  "pool": {
                    "temp_f": 75.9,
                    "obs_time": "2026-05-25T18:58:00.000Z",
                    "age_seconds": 120
                  },
                  "sun": {
                    "sunrise_utc": "2026-05-25T10:43:00.000Z",
                    "sunset_utc": "2026-05-26T00:58:00.000Z",
                    "is_day": true
                  },
                  "sky": {
                    "cloud_pct": 81,
                    "label": "mostly cloudy"
                  },
                  "rain": {
                    "today_pct": 88,
                    "today_peak_at": "2026-05-25T21:00:00.000Z",
                    "today_in": 0.42,
                    "tomorrow_pct": 40,
                    "tomorrow_peak_at": "2026-05-26T20:00:00.000Z"
                  },
                  "thunder": {
                    "peak_pct": 35,
                    "peak_at": "2026-05-25T22:00:00.000Z"
                  },
                  "lightning": {
                    "cg_density": null,
                    "strikes_nearby": false,
                    "prob_30min": 5,
                    "prob_60min": 7,
                    "obs_time": "2026-05-25T18:31:01.000Z"
                  },
                  "rain_now": {
                    "currently_raining": true,
                    "current_rate_mm_hr": 6.4,
                    "outcome": "keeps_raining",
                    "starts_iso": null,
                    "ends_iso": null,
                    "peak_iso": "2026-05-25T19:05:00.000Z",
                    "peak_rate_mm_hr": 10.02,
                    "certainty_pct": 90,
                    "n_scans": 9,
                    "nowcast_age_seconds": 344
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/forecast/daily": {
      "get": {
        "summary": "Daily forecast (air + pool, cloud, POP)",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10,
              "default": 7
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "example": {
                  "days": [
                    {
                      "date": "2026-05-25",
                      "air_high_f": 76.6,
                      "air_low_f": 66.2,
                      "pool_high_f": 77.3,
                      "pool_low_f": 73.9,
                      "cloud_pct": 81.4,
                      "cloud_label": "mostly cloudy",
                      "pop_pct": 88
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/forecast/hourly": {
      "get": {
        "summary": "Hourly forecast (air + pool, cloud, POP, UV)",
        "parameters": [
          {
            "name": "hours",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 168,
              "default": 24
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK. uv_index is the forecast UV index (Pirate/NBM), rounded to a whole number; null when unavailable for that hour.",
            "content": {
              "application/json": {
                "example": {
                  "hours": [
                    {
                      "t": "2026-05-25T19:00:00.000Z",
                      "air_temp_f": 76.6,
                      "pool_temp_f": 75.9,
                      "cloud_pct": 83,
                      "pop_pct": 79,
                      "uv_index": 4
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/forecast/valley": {
      "get": {
        "summary": "Valley / Downtown air-temp forecast",
        "parameters": [
          {
            "name": "hours",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 168,
              "default": 48
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "example": {
                  "hours": [
                    {
                      "t": "2026-05-25T19:00:00.000Z",
                      "air_temp_f": 79.9
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/observed/hourly": {
      "get": {
        "summary": "Observed history (plateau, valley, pool, rainfall)",
        "description": "Hourly buckets centered on HH:30 for closed hours, plus a 10-min live tail covering the past ~2h. `rain_in` is inches of plateau rainfall in that bucket — hour buckets use the MSWX rainfall composite (trim-mean across the rain gauges); tail 10-min buckets use per-member precip_total_in differential, AVG across members.",
        "parameters": [
          {
            "name": "hours",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 168,
              "default": 24
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "example": {
                  "hours": [
                    {
                      "t": "2026-05-24T19:30:00.000Z",
                      "air_temp_f": 70.1,
                      "valley_temp_f": 74.8,
                      "pool_temp_f": null,
                      "rain_in": 0.08
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/alerts": {
      "get": {
        "summary": "Active NWS alerts",
        "description": "Active watches/warnings affecting the plateau — both those that cover our point and nearby storm-based warnings within ~50 mi. Each alert carries proximity (`contains_us`, `distance_mi`, `bearing`) and a ready-to-show `local_area` label ('Madison Co.' when over us; '~5 mi NW' for a nearby cell). `priority` ranks them (warning > watch > advisory, then severity); `alerts` is sorted by priority then closeness, so `alerts[0]` is the headline. `banner` flags the severe few (Severe Tstorm / Tornado) the site renders as a top banner; `top_priority` is the max priority among banner alerts (clients cycle the tied-top set). `geometry` is GeoJSON when present.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "example": {
                  "as_of": "2026-06-01T16:30:00.000Z",
                  "count": 2,
                  "banner_count": 1,
                  "top_priority": 43,
                  "alerts": [
                    {
                      "id": "https://api.weather.gov/alerts/urn:oid:...",
                      "event": "Severe Thunderstorm Warning",
                      "severity": "Severe",
                      "urgency": "Immediate",
                      "certainty": "Observed",
                      "headline": "Severe Thunderstorm Warning issued...",
                      "area": "Limestone, AL; Madison, AL; Lincoln, TN",
                      "description": "* WHAT...",
                      "instruction": "Move indoors...",
                      "onset": "2026-06-01T15:30:00-05:00",
                      "ends": "2026-06-01T16:15:00-05:00",
                      "sent": "2026-06-01T15:30:00-05:00",
                      "banner": true,
                      "geometry": {
                        "type": "Polygon",
                        "coordinates": [
                          "…"
                        ]
                      },
                      "contains_us": false,
                      "distance_mi": 5.3,
                      "bearing": "NW",
                      "local_area": "~5 mi NW",
                      "priority": 43
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/radar/nowcast": {
      "get": {
        "summary": "Radar rain-nowcast (is it raining / when does it start or stop)",
        "description": "Live MRMS-driven nowcast. `rain_now` is the same summary block as /api/v1/now (absolute ISO timestamps — compute the countdown client-side). `observed` is box-max precip rate over the last 2h; `forecast` is the 0..+60 min advected rate; `pop_hourly` compares blended vs radar-bent vs raw-NWS probability of precip.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "example": {
                  "as_of": "2026-05-25T19:00:00.000Z",
                  "location": {
                    "lat": 34.7335,
                    "lon": -86.5236,
                    "label": "MSWX center / plateau"
                  },
                  "rain_now": {
                    "currently_raining": true,
                    "current_rate_mm_hr": 6.4,
                    "outcome": "keeps_raining",
                    "starts_iso": null,
                    "ends_iso": null,
                    "peak_iso": "2026-05-25T19:05:00.000Z",
                    "peak_rate_mm_hr": 10.02,
                    "certainty_pct": 90,
                    "n_scans": 9,
                    "nowcast_age_seconds": 344
                  },
                  "motion": {
                    "u_km_min": 0.31,
                    "v_km_min": 0.12,
                    "method": "pysteps"
                  },
                  "freshness": {
                    "latest_obs_iso": "2026-05-25T18:58:00.000Z",
                    "latest_obs_age_seconds": 120,
                    "latest_nowcast_iso": "2026-05-25T18:54:16.000Z",
                    "latest_nowcast_age_seconds": 344
                  },
                  "observed": [
                    {
                      "t": "2026-05-25T18:58:00.000Z",
                      "rate_mm_hr": 6.4
                    }
                  ],
                  "forecast": [
                    {
                      "t": "2026-05-25T19:05:00.000Z",
                      "rate_mm_hr": 10.02
                    }
                  ],
                  "pop_hourly": [
                    {
                      "t": "2026-05-25T20:00:00.000Z",
                      "blend_pct": 70,
                      "radar_blend_pct": 88,
                      "nws_pct": 60
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/radar/map": {
      "get": {
        "summary": "Radar map manifest (frames + basemap)",
        "description": "Everything to draw the animated radar map: `bbox` (drape each PNG over it), `frames` (absolute PNG URLs, oldest→newest; kind=obs|fcst), `lightning` strike geometry, `basemap` PMTiles pointers (Protomaps dark), and the neighborhood overlay GeoJSON URL.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "example": {
                  "as_of": "2026-05-25T18:34:12Z",
                  "site": "KHTX",
                  "bbox": [
                    -87.55,
                    33.9,
                    -85.5,
                    35.55
                  ],
                  "frames": [
                    {
                      "url": "https://mswx.net/radar-frames/o_12.png",
                      "valid_time": "2026-05-25T17:20:00Z",
                      "kind": "obs"
                    }
                  ],
                  "frame_cadence_seconds": 300,
                  "lightning": null,
                  "basemap": {
                    "pmtiles_url": "https://mswx.net/tiles/mswx-20260601.pmtiles",
                    "protocol": "pmtiles",
                    "flavor": "dark",
                    "glyphs": "https://protomaps.github.io/basemaps-assets/fonts/{fontstack}/{range}.pbf",
                    "sprite": "https://protomaps.github.io/basemaps-assets/sprites/v4/dark",
                    "center": [
                      -86.5236,
                      34.7335
                    ],
                    "min_zoom": 5,
                    "max_zoom": 14,
                    "max_bounds": [
                      [
                        -88.8,
                        33.1
                      ],
                      [
                        -84.2,
                        36.4
                      ]
                    ],
                    "attribution": "Protomaps © OpenStreetMap"
                  },
                  "overlays": {
                    "neighborhood_geojson_url": "https://mswx.net/static/neighborhood.geojson"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/widget": {
      "get": {
        "summary": "Compact widget payload (home-screen widgets)",
        "description": "Pre-shaped for a small home-screen widget: current sky, a few hourly slots, a daily strip, and one human 'insight' line.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "example": {
                  "as_of": "2026-05-25T19:00:00.000Z",
                  "center": {
                    "temp_f": 74.2,
                    "label": "mostly cloudy"
                  },
                  "sky": {
                    "cloud_pct": 81,
                    "label": "mostly cloudy"
                  },
                  "slots": [],
                  "daily": [
                    {
                      "date": "2026-05-25",
                      "hi_f": 76.6,
                      "lo_f": 66.2,
                      "icon": "☁",
                      "pop_pct": 90
                    }
                  ],
                  "insight": {
                    "kind": "rain",
                    "emoji": "🌧",
                    "text": "Rain 9p · 79%"
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}