{
  "openapi": "3.0.3",
  "info": {
    "title": "Hail History API",
    "version": "1.0.0",
    "description": "Radar-detected hail history for any US address or coordinate.\n\nKeyless: no API key, no registration, no rate limit documented by the upstream source. CORS is open to `*`.\n\n**Resolution — read this before using the data.** NOAA's Severe Weather Data Inventory answers on a 0.1-degree grid tile, roughly 6.9 miles north-to-south (east-west narrows with latitude). A hail day means radar detected hail *somewhere inside that tile*, not that hail fell on a specific structure. Radar resolution is finest near the radar site and coarsens with distance from it, and terrain can block the beam. No detection is an absence of evidence, not proof no hail fell.\n\n**Upstream:** NOAA NCEI Severe Weather Data Inventory, dataset `nx3hail` (NEXRAD Level-III Hail Detection Algorithm). Period of record begins 1992-05-07. Addresses are resolved with the free US Census geocoder. All dates and times are GMT.",
    "contact": { "name": "Source documentation", "url": "https://www.ncdc.noaa.gov/swdiws/" },
    "license": { "name": "NOAA data — US Government work, public domain", "url": "https://www.noaa.gov/information-technology/open-data-dissemination" }
  },
  "servers": [{ "url": "/", "description": "Same origin as this document" }],
  "tags": [{ "name": "hail", "description": "Radar hail history" }],
  "paths": {
    "/hail-history": {
      "get": {
        "tags": ["hail"],
        "summary": "Hail history for a US address or coordinate",
        "description": "Supply either `address` or both `lat` and `lon`. Returns every day NOAA radar detected hail in the 0.1-degree tile containing the point, rolled up per calendar year, optionally with the individual radar hail cells for the busiest days.",
        "operationId": "getHailHistory",
        "parameters": [
          { "name": "address", "in": "query", "required": false, "schema": { "type": "string" },
            "description": "US one-line address. Mutually exclusive with lat/lon.",
            "example": "100 N Broadway Ave, Oklahoma City, OK 73102" },
          { "name": "lat", "in": "query", "required": false, "schema": { "type": "number", "minimum": -90, "maximum": 90 }, "example": 35.4676 },
          { "name": "lon", "in": "query", "required": false, "schema": { "type": "number", "minimum": -180, "maximum": 180 }, "example": -97.5164 },
          { "name": "from", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1992 },
            "description": "Start calendar year, inclusive. Clamped to the 1992 period-of-record start. Default: `to` minus 19." },
          { "name": "to", "in": "query", "required": false, "schema": { "type": "integer" },
            "description": "End calendar year, inclusive. Clamped to the current year. Default: current year. The span may not exceed 30 years." },
          { "name": "detail", "in": "query", "required": false,
            "schema": { "type": "string", "default": "none" },
            "description": "`none` (default), `top` for the busiest days, or a specific day as `YYYY-MM-DD`. Each detail day costs one extra upstream request." },
          { "name": "topN", "in": "query", "required": false,
            "schema": { "type": "integer", "minimum": 1, "maximum": 5, "default": 3 },
            "description": "How many busiest days to expand when `detail=top`." }
        ],
        "responses": {
          "200": {
            "description": "Hail history. Returned for both `detections_found` and `no_detections` — a point with no hail on record is a successful answer, not an error.",
            "headers": { "Access-Control-Allow-Origin": { "schema": { "type": "string" }, "description": "`*`" } },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HailHistory" } } }
          },
          "400": { "description": "Missing or malformed parameters.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "The address could not be geocoded by the US Census geocoder.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "Coordinates were well-formed but out of range.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "502": { "description": "NOAA SWDI could not be reached or returned an error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/health": {
      "get": {
        "summary": "Service status and self-description",
        "operationId": "getHealth",
        "description": "Makes no upstream calls.",
        "responses": { "200": { "description": "Service metadata.", "content": { "application/json": { "schema": { "type": "object" } } } } }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": { "type": "string", "example": "bad_request" },
          "message": { "type": "string" },
          "detail": { "type": "object", "nullable": true }
        }
      },
      "Location": {
        "type": "object",
        "properties": {
          "matchedAddress": { "type": "string", "nullable": true, "description": "Null when the request used lat/lon." },
          "lat": { "type": "number" },
          "lon": { "type": "number" },
          "county": { "type": "string", "nullable": true },
          "countyFips": { "type": "string", "nullable": true },
          "zip": { "type": "string", "nullable": true }
        }
      },
      "SearchArea": {
        "type": "object",
        "description": "The exact area that was searched. Returned so the caller can see the resolution rather than assume rooftop precision.",
        "properties": {
          "note": { "type": "string" },
          "tileCenter": { "type": "object", "properties": { "lat": { "type": "number" }, "lon": { "type": "number" } } },
          "tileCenterSource": { "type": "string", "enum": ["reported-by-noaa", "computed-locally (no rows returned)"],
            "description": "`reported-by-noaa` when NOAA returned rows carrying CENTERLAT/CENTERLON; otherwise the center was rounded locally." },
          "bounds": { "type": "object", "properties": { "minLon": { "type": "number" }, "maxLon": { "type": "number" }, "minLat": { "type": "number" }, "maxLat": { "type": "number" } } },
          "approxSize": { "type": "object", "properties": { "northSouthMiles": { "type": "number" }, "eastWestMiles": { "type": "number" }, "northSouthKm": { "type": "number" }, "eastWestKm": { "type": "number" } } }
        }
      },
      "YearSummary": {
        "type": "object",
        "properties": {
          "year": { "type": "integer" },
          "hailDays": { "type": "integer", "description": "Distinct GMT days with at least one radar hail detection in the tile." },
          "detections": { "type": "integer", "description": "Sum of NOAA FCOUNT — individual radar hail-cell records, NOT distinct hailstorms. One storm scanned repeatedly by four radars produces many detections." }
        }
      },
      "HailCell": {
        "type": "object",
        "description": "One raw NEXRAD Level-III hail-cell record. Column meanings are interpretation: the published SWDI web-service spec does not define PROB, SEVPROB or MAXSIZE.",
        "properties": {
          "timeUtc": { "type": "string", "example": "2024-06-26T04:02:41Z" },
          "radar": { "type": "string", "nullable": true, "description": "NEXRAD site id, e.g. KTLX.", "example": "KINX" },
          "cellId": { "type": "string", "nullable": true },
          "probabilityOfHailPct": { "type": "number", "nullable": true, "description": "SWDI column PROB. Interpreted as probability of hail, percent." },
          "probabilityOfSevereHailPct": { "type": "number", "nullable": true, "description": "SWDI column SEVPROB. Interpreted as probability of severe hail, percent." },
          "maxSize": { "type": "number", "nullable": true, "description": "SWDI column MAXSIZE. Interpreted as maximum expected hail size in inches. NOT confirmed by an authoritative units statement — observed values range roughly 0.25 to 4." },
          "lat": { "type": "number", "nullable": true },
          "lon": { "type": "number", "nullable": true }
        }
      },
      "DayDetail": {
        "type": "object",
        "properties": {
          "day": { "type": "string", "example": "2013-06-01" },
          "radarDetections": { "type": "integer" },
          "distinctRadarSites": { "type": "array", "items": { "type": "string" } },
          "maxSizeObserved": { "type": "number", "nullable": true },
          "maxProbabilityOfSevereHailPct": { "type": "number", "nullable": true },
          "firstDetectionUtc": { "type": "string", "nullable": true },
          "lastDetectionUtc": { "type": "string", "nullable": true },
          "cells": { "type": "array", "items": { "$ref": "#/components/schemas/HailCell" } }
        }
      },
      "HailHistory": {
        "type": "object",
        "properties": {
          "query": { "type": "object", "description": "Echo of the effective, clamped parameters." },
          "location": { "$ref": "#/components/schemas/Location" },
          "searchArea": { "$ref": "#/components/schemas/SearchArea" },
          "result": {
            "type": "object",
            "properties": {
              "coverage": { "type": "string", "enum": ["detections_found", "no_detections"] },
              "yearsCovered": { "type": "integer" },
              "fromYear": { "type": "integer" },
              "toYear": { "type": "integer" },
              "hailDays": { "type": "integer" },
              "totalRadarDetections": { "type": "integer" },
              "yearsWithHail": { "type": "integer" },
              "avgHailDaysPerYear": { "type": "number" },
              "frequencyBand": { "type": "string", "enum": ["very-high", "high", "moderate", "low", "very-low"],
                "description": "OUR editorial banding on average hail days per year, not a NOAA classification and not a risk score. The thresholds are returned alongside it so the label is auditable." },
              "frequencyBandThresholds": { "type": "array", "items": { "type": "object" } },
              "frequencyBandNote": { "type": "string" },
              "mostRecentHailDay": { "type": "string", "nullable": true },
              "largestSingleDayByDetections": { "type": "object", "nullable": true,
                "description": "Day with the most radar detections. This is a proxy for storm intensity, not a measured maximum hail size — use `detail` for size." },
              "perYear": { "type": "array", "items": { "$ref": "#/components/schemas/YearSummary" } },
              "hailDaysDetail": { "type": "array", "items": { "type": "object", "properties": { "day": { "type": "string" }, "radarDetections": { "type": "integer" } } } },
              "meaning": { "type": "string", "description": "Plain-English summary, worded to avoid overclaiming." },
              "dayDetail": { "type": "array", "nullable": true, "items": { "$ref": "#/components/schemas/DayDetail" } },
              "dayDetailNote": { "type": "string", "nullable": true }
            }
          },
          "fetchStrategy": {
            "type": "object",
            "description": "How many upstream calls this answer cost and which strategy was used. NOAA documents a one-year maximum range but does not enforce it for stat=tilesum, so the whole span is requested in one call, with automatic fallback to documented-legal per-year requests.",
            "properties": {
              "strategy": { "type": "string", "enum": ["single-range", "per-year-chunked"] },
              "upstreamRequests": { "type": "integer" },
              "note": { "type": "string" },
              "singleRangeError": { "type": "string", "nullable": true }
            }
          },
          "source": { "type": "object" },
          "retrievedAt": { "type": "string", "format": "date-time" },
          "disclaimer": { "type": "string" }
        }
      }
    }
  }
}
