openapi: 3.1.0
info:
  title: CCA Weather API
  version: "1.0.0"
  description: >
    Public, permanently-archived NOAA/NWS weather for the Cooperative Computing
    Alliance data-service fleet. Ingests and stores alerts, observations,
    forecasts, radar, satellite, lightning, models, tropical systems and river
    gauges — viewable forever — and serves a rewindable tile timeline for the map
    viewer. All NOAA data is public domain. Responses use the fleet envelope
    `{ request_status, message, data, meta }`; GeoJSON and binary tiles are
    documented exceptions.
servers:
  - url: https://weather.datasourceapi.com
tags:
  - name: Catalog
  - name: Tiles
  - name: Weather
  - name: Search
  - name: Ops
paths:
  /api/v1/status:
    get:
      tags: [Ops]
      summary: Service health, per-shard probes and dataset freshness.
      responses: { "200": { description: Healthy }, "503": { description: Degraded/unhealthy } }
  /api/v1/layers:
    get:
      tags: [Catalog]
      summary: Viewer layer catalog (the toggleable map layers).
      responses: { "200": { description: Layer list } }
  /api/v1/frames/{layer}/{product}:
    get:
      tags: [Catalog, Tiles]
      summary: Available tile frames (timestamps) for a layer/product — the scrubber timeline.
      parameters:
        - { name: layer, in: path, required: true, schema: { type: string } }
        - { name: product, in: path, required: true, schema: { type: string } }
        - { name: from, in: query, schema: { type: string, format: date-time } }
        - { name: to, in: query, schema: { type: string, format: date-time } }
        - { name: limit, in: query, schema: { type: integer, default: 600 } }
      responses: { "200": { description: Chronological frame list } }
  /api/v1/tiles/{layer}/{product}/{ts}/tilejson.json:
    get:
      tags: [Tiles]
      summary: TileJSON for one frame (use ts=latest for the newest).
      parameters:
        - { name: layer, in: path, required: true, schema: { type: string } }
        - { name: product, in: path, required: true, schema: { type: string } }
        - { name: ts, in: path, required: true, schema: { type: string } }
      responses: { "200": { description: TileJSON 2.2.0 } }
  /api/v1/tiles/{layer}/{product}/{ts}/{z}/{x}/{y}.{ext}:
    get:
      tags: [Tiles]
      summary: A rendered raster tile (WebP/PNG). 204 = empty tile (sparse coverage).
      parameters:
        - { name: layer, in: path, required: true, schema: { type: string } }
        - { name: product, in: path, required: true, schema: { type: string } }
        - { name: ts, in: path, required: true, schema: { type: string, description: "ISO timestamp or 'latest'" } }
        - { name: z, in: path, required: true, schema: { type: integer } }
        - { name: x, in: path, required: true, schema: { type: integer } }
        - { name: y, in: path, required: true, schema: { type: integer } }
        - { name: ext, in: path, required: true, schema: { type: string, enum: [webp, png] } }
      responses: { "200": { description: Tile image }, "204": { description: Empty tile } }
  /api/v1/alerts:
    get:
      tags: [Weather]
      summary: Active NWS watches/warnings/advisories (append ?format=geojson for polygons).
      parameters:
        - { name: event, in: query, schema: { type: string } }
        - { name: severity, in: query, schema: { type: string, enum: [Extreme, Severe, Moderate, Minor, Unknown] } }
        - { name: area, in: query, schema: { type: string } }
        - { name: format, in: query, schema: { type: string, enum: [json, geojson] } }
      responses: { "200": { description: Active alerts } }
  /api/v1/observations:
    get:
      tags: [Weather]
      summary: Latest surface/marine observations (METAR + NDBC buoys). ?station= for a series.
      parameters:
        - { name: station, in: query, schema: { type: string } }
        - { name: kind, in: query, schema: { type: string, enum: [metar, buoy] } }
        - { name: near, in: query, schema: { type: string, description: "lat,lon — sort by distance, adds distance_km" } }
        - { name: limit, in: query, schema: { type: integer, description: "max results for near= (default 25)" } }
        - { name: bbox, in: query, schema: { type: string, description: "w,s,e,n" } }
        - { name: format, in: query, schema: { type: string, enum: [json, geojson] } }
      responses: { "200": { description: Observations } }
  /api/v1/history:
    get:
      tags: [Weather]
      summary: Hourly and daily reanalysis for a location and past date.
      description: Returns modeled historical weather through Open-Meteo for up to 31 days ending on `date`.
      parameters:
        - { name: lat, in: query, required: true, schema: { type: number, minimum: -90, maximum: 90 } }
        - { name: lon, in: query, required: true, schema: { type: number, minimum: -180, maximum: 180 } }
        - { name: date, in: query, required: true, schema: { type: string, format: date } }
        - { name: days, in: query, schema: { type: integer, minimum: 1, maximum: 31, default: 1 } }
      responses:
        "200": { description: Daily and hourly historical weather }
        "400": { description: Invalid location or date }
        "502": { description: Historical weather upstream error }
  /api/v1/climate/stats:
    get:
      tags: [Weather]
      summary: Weather statistics for a location over a custom date range.
      description: >
        Calculates temperature averages and records, heating/cooling degree days,
        precipitation, snowfall, sunshine, wind, and condition frequencies from
        daily ERA5/ERA5-Land seamless reanalysis. Aggregates are computed at read time and are
        not persisted. The requested coordinate represents an area-scale model grid
        cell rather than one weather station.
      parameters:
        - { name: lat, in: query, required: true, schema: { type: number, minimum: -90, maximum: 90 } }
        - { name: lon, in: query, required: true, schema: { type: number, minimum: -180, maximum: 180 } }
        - { name: start_date, in: query, required: true, description: "On or after 1940-01-01.", schema: { type: string, format: date } }
        - { name: end_date, in: query, required: true, description: "Must account for the roughly five-day ERA5 availability delay.", schema: { type: string, format: date } }
        - { name: group_by, in: query, schema: { type: string, enum: [none, month, year], default: month } }
      responses:
        "200": { description: Period summary plus optional monthly or yearly series }
        "400": { description: Invalid location, range, or grouping }
        "502": { description: Reanalysis upstream failed and no archived copy was available }
        "504": { description: Reanalysis upstream timed out and no archived copy was available }
  /api/v1/climate/normals:
    get:
      tags: [Weather]
      summary: Long-term monthly and annual climate normals for a location.
      description: >
        Returns City-Data-style monthly and annual averages, records, threshold-day
        counts, precipitation, snowfall, sunshine, wind, and hottest/coldest/wettest
        month highlights. Defaults to the standard 1991-2020 baseline; custom
        baselines must contain 10-50 complete years.
      parameters:
        - { name: lat, in: query, required: true, schema: { type: number, minimum: -90, maximum: 90 } }
        - { name: lon, in: query, required: true, schema: { type: number, minimum: -180, maximum: 180 } }
        - { name: start_year, in: query, schema: { type: integer, minimum: 1940, default: 1991 } }
        - { name: end_year, in: query, schema: { type: integer, default: 2020 } }
      responses:
        "200": { description: Twelve monthly normals, annual normals, and climate highlights }
        "400": { description: Invalid location or baseline }
        "502": { description: Reanalysis upstream failed and no archived copy was available }
        "504": { description: Reanalysis upstream timed out and no archived copy was available }
  /api/v1/forecast:
    get:
      tags: [Weather]
      summary: Point forecast (NWS gridded) for lat/lon. ?as_of= time-travels to a stored issuance.
      parameters:
        - { name: lat, in: query, required: true, schema: { type: number } }
        - { name: lon, in: query, required: true, schema: { type: number } }
        - { name: as_of, in: query, schema: { type: string, format: date-time } }
      responses: { "200": { description: Daily + hourly forecast }, "502": { description: Upstream error } }
  /api/v1/tropical:
    get:
      tags: [Weather]
      summary: Active tropical systems (NHC). ?format=geojson for map points.
      responses: { "200": { description: Active systems } }
  /api/v1/hydro/gauges:
    get:
      tags: [Weather]
      summary: River/stream gauges (NWPS) with latest stage + flood category.
      parameters:
        - { name: category, in: query, schema: { type: string, enum: [none, action, minor, moderate, major] } }
        - { name: bbox, in: query, schema: { type: string, description: "w,s,e,n" } }
        - { name: format, in: query, schema: { type: string, enum: [json, geojson] } }
      responses: { "200": { description: Gauges } }
  /api/v1/hydro/gauge/{id}:
    get:
      tags: [Weather]
      summary: One gauge's detail (flood thresholds + forecast + crests).
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses: { "200": { description: Gauge detail } }
  /api/v1/radar/sites/{site}:
    get:
      tags: [Weather]
      summary: On-demand latest raw NEXRAD Level II volume metadata for a site (from AWS NODD).
      parameters:
        - { name: site, in: path, required: true, schema: { type: string, example: KOKX } }
      responses: { "200": { description: Latest volume pointer } }
  /api/v1/radar/list:
    get:
      tags: [Weather]
      summary: Directory of NEXRAD/TDWR radar sites for the single-site radar picker.
      responses: { "200": { description: "{ sites: [{ id, name, lat, lon, type }] }" } }
  /api/v1/radar/l3sites:
    get:
      tags: [Weather]
      summary: Which sites have offline-rendered Level III (dual-pol / base-velocity) frames, per catalog layer.
      responses: { "200": { description: "{ sites: [{ site, layers: { l3-cc: {latest, frames}, ... } }], layers: [l3-cc, l3-zdr, l3-kdp, l3-hc, l3-vel] }" } }
  /api/v1/radar/scans:
    get:
      tags: [Weather]
      summary: Scan-time index for a single site + product (drives the radar scrubber).
      parameters:
        - { name: site, in: query, required: true, schema: { type: string, example: DMX } }
        - { name: product, in: query, schema: { type: string, enum: [N0B, N0S, N0U], default: N0B } }
        - { name: start, in: query, schema: { type: string, format: date-time } }
        - { name: end, in: query, schema: { type: string, format: date-time } }
      responses: { "200": { description: "{ site, product, count, latest_only, tile_template, frames:[{ts,stamp}] }. N0B/N0S rewind (full archive); N0U is live-only (latest_only:true, single frame)." } }
  /api/v1/radar/tile/{site}/{product}/{ts}/{z}/{x}/{y}.png:
    get:
      tags: [Tiles]
      summary: One single-site NEXRAD Level III tile (proxied from IEM RIDGE). 204 on an empty tile.
      parameters:
        - { name: site, in: path, required: true, schema: { type: string, example: DMX } }
        - { name: product, in: path, required: true, schema: { type: string, enum: [N0B, N0S, N0U] } }
        - { name: ts, in: path, required: true, schema: { type: string, description: "compact RIDGE stamp YYYYMMDDHHMM, or 'latest'" } }
        - { name: z, in: path, required: true, schema: { type: integer } }
        - { name: x, in: path, required: true, schema: { type: integer } }
        - { name: y, in: path, required: true, schema: { type: integer } }
      responses:
        "200": { description: WebP/PNG radar tile }
        "204": { description: Empty tile (no echo) }
  /api/v1/radar/live/scans:
    get:
      tags: [Weather]
      summary: Live Level III scan index straight from the NOAAPORT feed (today + yesterday). Worker-rendered — every WSR-88D/TDWR product + tilt, no offline render queue.
      parameters:
        - { name: site, in: query, required: true, schema: { type: string, example: TLX } }
        - { name: product, in: query, schema: { type: string, description: "N0B/N1B/N2B/N3B, N0G, N0C, N0X, N0K, N0H, TZ0, TV0, TZL", default: N0B } }
        - { name: limit, in: query, schema: { type: integer, default: 20, maximum: 100 } }
      responses: { "200": { description: "{ site, product, count, frames:[{key,ts}] } ascending by time; key feeds /radar/live/tile" } }
  /api/v1/radar/live/tile/{site}/{product}/{scanKey}/{z}/{x}/{y}.png:
    get:
      tags: [Tiles]
      summary: One live Level III tile rendered in the Worker from the raw NIDS scan (native super-res, tiler palette). Immutable per scanKey. 204 on an empty tile.
      parameters:
        - { name: site, in: path, required: true, schema: { type: string, example: TLX } }
        - { name: product, in: path, required: true, schema: { type: string, example: N0B } }
        - { name: scanKey, in: path, required: true, schema: { type: string, description: "bucket key from /radar/live/scans, e.g. TLX_N0B_2026_07_06_12_57_33" } }
        - { name: z, in: path, required: true, schema: { type: integer, maximum: 14 } }
        - { name: x, in: path, required: true, schema: { type: integer } }
        - { name: y, in: path, required: true, schema: { type: integer } }
      responses:
        "200": { description: PNG radar tile }
        "204": { description: Empty tile (no echo) }
  /api/v1/spc-outlook.geojson:
    get:
      tags: [Weather]
      summary: SPC Convective Outlook (Day 1/2/3 categorical + Day 1 probabilistic) as GeoJSON.
      parameters:
        - { name: product, in: query, schema: { type: string, enum: [day1-cat, day2-cat, day3-cat, day1-torn, day1-hail, day1-wind], default: day1-cat } }
      responses: { "200": { description: FeatureCollection (raw geo+json exception) } }
  /api/v1/wpc-qpf.geojson:
    get:
      tags: [Weather]
      summary: WPC Quantitative Precipitation Forecast (Day 1/2/3, 24h) as GeoJSON, colored by amount.
      parameters:
        - { name: product, in: query, schema: { type: string, enum: [qpf-day1, qpf-day2, qpf-day3], default: qpf-day1 } }
      responses: { "200": { description: FeatureCollection (raw geo+json exception) } }
  /api/v1/spc-fire.geojson:
    get:
      tags: [Weather]
      summary: SPC Fire Weather Outlook (Day 1-3 wind/RH + dry thunderstorm) as GeoJSON, official NOAA colors.
      parameters:
        - { name: product, in: query, schema: { type: string, enum: [d1-windrh, d1-dryt, d2-windrh, d2-dryt, d3-windrh, d3-dryt], default: d1-windrh } }
      responses: { "200": { description: FeatureCollection (raw geo+json exception) } }
  /api/v1/natl-chart.geojson:
    get:
      tags: [Weather]
      summary: WPC National Forecast Chart (fronts, highs/lows, precip + hazard areas) as a mixed GeoJSON FeatureCollection.
      parameters:
        - { name: product, in: query, schema: { type: string, enum: [day1, day2, day3], default: day1 } }
      responses: { "200": { description: FeatureCollection (raw geo+json exception) } }
  /api/v1/spc-tstm.geojson:
    get:
      tags: [Weather]
      summary: SPC probabilistic Thunderstorm Outlook (10/40/70% in 4-hour windows) as GeoJSON.
      parameters:
        - { name: product, in: query, schema: { type: string, enum: [v1200, v1600, v2000], default: v1200 } }
      responses: { "200": { description: FeatureCollection (raw geo+json exception) } }
  /api/v1/wpc-ero.geojson:
    get:
      tags: [Weather]
      summary: WPC Excessive Rainfall Outlook (Day 1-5) as GeoJSON, official NOAA colors.
      parameters:
        - { name: product, in: query, schema: { type: string, enum: [day1, day2, day3, day4, day5], default: day1 } }
      responses: { "200": { description: FeatureCollection (raw geo+json exception) } }
  /api/v1/wpc-wssi.geojson:
    get:
      tags: [Weather]
      summary: WPC Winter Storm Severity Index (overall impact + components) as GeoJSON.
      parameters:
        - { name: product, in: query, schema: { type: string, enum: [overall-d1, overall-d2, overall-d3, snow-amt-d1, snow-load-d1, ice-d1, blowing-d1], default: overall-d1 } }
      responses: { "200": { description: FeatureCollection (raw geo+json exception) } }
  /api/v1/wpc-winter.geojson:
    get:
      tags: [Weather]
      summary: WPC Winter Precipitation Probability (snow ≥4/8/12in, icing >0.25in) as GeoJSON.
      parameters:
        - { name: product, in: query, schema: { type: string, enum: [snow4-d1, snow8-d1, snow12-d1, ice-d1, snow4-d2, snow4-d3], default: snow4-d1 } }
      responses: { "200": { description: FeatureCollection (raw geo+json exception) } }
  /api/v1/us-hazards.geojson:
    get:
      tags: [Weather]
      summary: US Hazards Outlook (CPC/WPC 3-7 & 8-14 day temp/precip/fire threats) as GeoJSON.
      parameters:
        - { name: product, in: query, schema: { type: string, enum: [temp-37, temp-814, precip-37, precip-814, fire-37, fire-814], default: temp-37 } }
      responses: { "200": { description: FeatureCollection (raw geo+json exception) } }
  /api/v1/cpc-drought.geojson:
    get:
      tags: [Weather]
      summary: CPC Drought Outlook (monthly + seasonal) as GeoJSON, official NOAA colors.
      parameters:
        - { name: product, in: query, schema: { type: string, enum: [monthly, seasonal], default: monthly } }
      responses: { "200": { description: FeatureCollection (raw geo+json exception) } }
  /api/v1/cpc-hazards-814.geojson:
    get:
      tags: [Weather]
      summary: CPC Days 8-14 hazards (deterministic + probabilistic heat/temp/precip/snow/wind/soils) as GeoJSON, parsed from the official CPC GIS shapefiles.
      parameters:
        - { name: product, in: query, schema: { type: string, enum: [heatprob, tempprob, temp, prcpprob, prcp, snowprob, snow, windprob, wind, soils], default: heatprob } }
      responses: { "200": { description: FeatureCollection (raw geo+json exception) } }
  /api/v1/cpc-gth.geojson:
    get:
      tags: [Weather]
      summary: CPC Global Tropics Hazards weeks 2-3 (TC formation, enhanced/suppressed precip, above/below-average temps) as GeoJSON, parsed from the official CPC shapefiles.
      parameters:
        - { name: product, in: query, schema: { type: string, enum: [tc-w2, tc-w3, wet-w2, wet-w3, dry-w2, dry-w3, warm-w2, warm-w3, cold-w2, cold-w3], default: tc-w2 } }
      responses: { "200": { description: FeatureCollection (raw geo+json exception) } }
  /api/v1/cpc-climate.geojson:
    get:
      tags: [Weather]
      summary: CPC 6-10 & 8-14 day climate outlooks (temperature + precipitation) as GeoJSON.
      parameters:
        - { name: product, in: query, schema: { type: string, enum: [temp-610, precip-610, temp-814, precip-814], default: temp-610 } }
      responses: { "200": { description: FeatureCollection (raw geo+json exception) } }
  /api/v1/lsr.geojson:
    get:
      tags: [Weather]
      summary: Local Storm Reports (tornado/hail/wind/flood/snow/heat) as point GeoJSON, color-coded by category.
      parameters:
        - { name: as_of, in: query, schema: { type: string, format: date-time }, description: "Rewind: returns a 12-hour window of reports ending at this instant." }
      responses: { "200": { description: FeatureCollection of point reports (raw geo+json exception) } }
  /api/v1/hms-smoke.geojson:
    get:
      tags: [Weather]
      summary: NESDIS HMS analyst-drawn smoke plumes (Light/Medium/Heavy) as polygon GeoJSON.
      parameters:
        - { name: as_of, in: query, schema: { type: string, format: date-time }, description: "Rewind by UTC day (HMS files are date-keyed)." }
      responses: { "200": { description: FeatureCollection of smoke polygons (raw geo+json exception) } }
  /api/v1/hms-fire.geojson:
    get:
      tags: [Weather]
      summary: NESDIS HMS satellite fire detections, deduplicated ~5 km keeping max fire radiative power, as point GeoJSON.
      parameters:
        - { name: as_of, in: query, schema: { type: string, format: date-time }, description: "Rewind by UTC day (HMS files are date-keyed)." }
      responses: { "200": { description: FeatureCollection of hotspot points (raw geo+json exception) } }
  /api/v1/usdm.geojson:
    get:
      tags: [Weather]
      summary: US Drought Monitor current conditions (D0-D4, weekly) as polygon GeoJSON in official class colors.
      responses: { "200": { description: FeatureCollection of drought-class polygons (raw geo+json exception) } }
  /api/v1/tides/stations.geojson:
    get:
      tags: [Weather]
      summary: NOAA CO-OPS water-level station locations as point GeoJSON (click targets for tide detail).
      responses: { "200": { description: FeatureCollection of station points (raw geo+json exception) } }
  /api/v1/tides/{station}:
    get:
      tags: [Weather]
      summary: Tide predictions (next 48h high/low, MLLW feet) + latest observed water level for one CO-OPS station.
      parameters:
        - { name: station, in: path, required: true, schema: { type: string }, description: "CO-OPS station id, e.g. 8518750 (The Battery, NY)." }
      responses: { "200": { description: "Envelope with predictions[] and observed water level" } }
  /api/v1/afd:
    get:
      tags: [Weather]
      summary: Latest NWS Area Forecast Discussion (forecaster narrative) for a WFO; every issuance archived to R2 forever.
      parameters:
        - { name: wfo, in: query, required: true, schema: { type: string }, description: "3-letter WFO id, e.g. OKX (New York)." }
      responses: { "200": { description: Envelope with office, issued, and full product text } }
  /api/v1/msc/tile/{layer}/{product}/{ts}/{z}/{x}/{y}.png:
    get:
      tags: [Tiles]
      summary: Canada MSC (ECCC GeoMet) WMS tile proxy — msc-radar (rain/snow), msc-thunderstorm (prob), msc-lightning (density), msc-coastal (risk). Frame stamps come from /frames/{layer}; `current` serves the newest image.
      parameters:
        - { name: layer, in: path, required: true, schema: { type: string, enum: [msc-radar, msc-thunderstorm, msc-lightning, msc-coastal] } }
        - { name: product, in: path, required: true, schema: { type: string } }
        - { name: ts, in: path, required: true, schema: { type: string }, description: "ISO frame stamp from /frames, or 'current'" }
        - { name: z, in: path, required: true, schema: { type: integer } }
        - { name: x, in: path, required: true, schema: { type: integer } }
        - { name: y, in: path, required: true, schema: { type: integer } }
      responses:
        "200": { description: PNG tile (rendered upstream by ECCC GeoMet) }
        "204": { description: Empty/sparse tile or upstream error }
  /api/v1/geocode:
    get:
      tags: [Search]
      summary: Forward geocode (proxied to cca-geocode) for the search box.
      parameters:
        - { name: q, in: query, schema: { type: string } }
      responses: { "200": { description: Matches } }
  /api/v1/reverse:
    get:
      tags: [Search]
      summary: Reverse geocode lat/lon (proxied to cca-geocode).
      parameters:
        - { name: lat, in: query, required: true, schema: { type: number } }
        - { name: lon, in: query, required: true, schema: { type: number } }
      responses: { "200": { description: Place } }
  /api/v1/locate:
    get:
      tags: [Search]
      summary: Edge geolocation from Cloudflare request metadata (city-level; no client permission). Powers the viewer's start-near-you boot.
      responses: { "200": { description: "{found, lat, lon, city, region, country, timezone}" } }
  /api/v1/permalink:
    post:
      tags: [Ops]
      summary: Freeze the current view (center, zoom, layers, time) into a shareable /r/{id}.
      requestBody:
        content: { application/json: { schema: { type: object } } }
      responses: { "200": { description: Permalink created } }
  /api/v1/permalink/{id}:
    get:
      tags: [Ops]
      summary: Replay a frozen view.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses: { "200": { description: Frozen view }, "404": { description: Not found } }
  /api/v1/admin/ingest/{dataset}:
    post:
      tags: [Ops]
      summary: "Trigger an ingest (X-Admin-Key required)."
      parameters:
        - { name: dataset, in: path, required: true, schema: { type: string } }
      security: [ { AdminKey: [] } ]
      responses: { "200": { description: Ingest result }, "401": { description: Unauthorized } }
  /api/v1/admin/frames:
    post:
      tags: [Ops]
      summary: "Register rendered tile frame(s) (the offline tiler calls this). X-Admin-Key required."
      security: [ { AdminKey: [] } ]
      requestBody:
        content: { application/json: { schema: { type: object } } }
      responses: { "200": { description: Frame(s) registered } }
components:
  securitySchemes:
    AdminKey:
      type: apiKey
      in: header
      name: X-Admin-Key
