{"openapi":"3.1.0","info":{"title":"Atmostorm API","description":"Severe weather intelligence for everyone. Aggregates 12+ public sources (NWS, SPC, NHC, MRMS, GOES, HRRR, USGS, IEM, mesonets) into a single REST API with GeoJSON output and spatial queries.\n\nThe API is free and open — every public endpoint answers anonymously. An API key raises your rate limit; send it as the `X-API-Key` header (create one at /account/api-keys).\n\n**Not a life-safety source.** Always confirm at weather.gov.\n\nFull guide: https://atmostorm.com/api-docs","termsOfService":"https://atmostorm.com/api-docs#terms","contact":{"name":"Atmostorm","url":"https://atmostorm.com/api-docs"},"license":{"name":"CC0 1.0 Universal (public domain dedication)","identifier":"CC0-1.0"},"version":"0.1.0"},"servers":[{"url":"https://atmostorm.com","description":"Atmostorm"}],"paths":{"/api/v1/alerts":{"get":{"tags":["Alerts"],"summary":"Get Alerts","description":"Get active and recent NWS weather alerts.\n\nReturns a GeoJSON FeatureCollection of alert polygons with full alert details\nin the properties of each feature.\n\n**`point` without `radius` is a 50 km proximity search, not containment.**\nThat default is easy to misread — the Android app assumed containment and\nrendered a full-screen \"take shelter\" for a storm 40 km away. Pass\n`contains=true` for \"am I inside this warning\", which uses the same\n`ST_Covers` test the push gate uses, so a client cannot disagree with the\nnotification it just received.","operationId":"get_alerts_api_v1_alerts_get","parameters":[{"name":"bbox","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Bounding box: west,south,east,north","title":"Bbox"},"description":"Bounding box: west,south,east,north"},{"name":"point","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Center point: lat,lon","title":"Point"},"description":"Center point: lat,lon"},{"name":"radius","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Radius from point (e.g., 100km)","title":"Radius"},"description":"Radius from point (e.g., 100km)"},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"US state abbreviation","title":"State"},"description":"US state abbreviation"},{"name":"fips","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"5-digit county FIPS — only features intersecting that county's boundary","title":"Fips"},"description":"5-digit county FIPS — only features intersecting that county's boundary"},{"name":"wfo","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NWS Weather Forecast Office ID","title":"Wfo"},"description":"NWS Weather Forecast Office ID"},{"name":"start","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Start of time range (ISO 8601)","title":"Start"},"description":"Start of time range (ISO 8601)"},{"name":"end","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"End of time range (ISO 8601)","title":"End"},"description":"End of time range (ISO 8601)"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Max results","default":100,"title":"Limit"},"description":"Max results"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Pagination offset","default":0,"title":"Offset"},"description":"Pagination offset"},{"name":"active","in":"query","required":false,"schema":{"type":"boolean","description":"Only active (non-expired) alerts","default":true,"title":"Active"},"description":"Only active (non-expired) alerts"},{"name":"event","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by event type","title":"Event"},"description":"Filter by event type"},{"name":"severity","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter: Extreme|Severe|Moderate|Minor","title":"Severity"},"description":"Filter: Extreme|Severe|Moderate|Minor"},{"name":"urgency","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter: Immediate|Expected|Future","title":"Urgency"},"description":"Filter: Immediate|Expected|Future"},{"name":"contains","in":"query","required":false,"schema":{"type":"boolean","description":"With `point`: return only alerts whose polygon CONTAINS the point, instead of the default 50 km proximity search.","default":false,"title":"Contains"},"description":"With `point`: return only alerts whose polygon CONTAINS the point, instead of the default 50 km proximity search."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoJSONFeatureCollection"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/alerts/zone-warnings":{"get":{"tags":["Alerts"],"summary":"Get Zone Warnings","description":"Active warnings issued by zone (winter, heat, cold, wind, Red Flag), light:\nevent, the NWS's area names, the issuing office, the \"until\" time from the\nheadline, the states (from the zone codes), and where the zones are (for a\nmap link) when their shapes are stored. Ordered by ZONE_LIST_EVENTS, then\nsoonest-ending. For /storms.","operationId":"get_zone_warnings_api_v1_alerts_zone_warnings_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/alerts/{alert_id}":{"get":{"tags":["Alerts"],"summary":"Get Alert By Id","description":"Get a single alert by ID.","operationId":"get_alert_by_id_api_v1_alerts__alert_id__get","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"integer","title":"Alert Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/observations":{"get":{"tags":["Observations"],"summary":"Get Observations","description":"Get surface weather observations from all networks.","operationId":"get_observations_api_v1_observations_get","parameters":[{"name":"bbox","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Bounding box: west,south,east,north","title":"Bbox"},"description":"Bounding box: west,south,east,north"},{"name":"point","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Center point: lat,lon","title":"Point"},"description":"Center point: lat,lon"},{"name":"radius","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Radius from point (e.g., 100km)","title":"Radius"},"description":"Radius from point (e.g., 100km)"},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"US state abbreviation","title":"State"},"description":"US state abbreviation"},{"name":"wfo","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NWS Weather Forecast Office ID","title":"Wfo"},"description":"NWS Weather Forecast Office ID"},{"name":"start","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Start of time range (ISO 8601)","title":"Start"},"description":"Start of time range (ISO 8601)"},{"name":"end","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"End of time range (ISO 8601)","title":"End"},"description":"End of time range (ISO 8601)"},{"name":"latest","in":"query","required":false,"schema":{"type":"boolean","description":"Return most recent data only","default":false,"title":"Latest"},"description":"Return most recent data only"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Max results","default":100,"title":"Limit"},"description":"Max results"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Pagination offset","default":0,"title":"Offset"},"description":"Pagination offset"},{"name":"source","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by network: nws|iem|synoptic","title":"Source"},"description":"Filter by network: nws|iem|synoptic"},{"name":"station","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Specific station ID","title":"Station"},"description":"Specific station ID"},{"name":"units","in":"query","required":false,"schema":{"$ref":"#/components/schemas/UnitSystem","description":"Unit system for converted values","default":"metric"},"description":"Unit system for converted values"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoJSONFeatureCollection"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/outlooks":{"get":{"tags":["Outlooks"],"summary":"Get Outlooks","description":"Get SPC convective outlooks.\n\nReturns a GeoJSON FeatureCollection of outlook polygons with risk levels,\nvalid times, and display colors in the properties.","operationId":"get_outlooks_api_v1_outlooks_get","parameters":[{"name":"bbox","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Bounding box: west,south,east,north","title":"Bbox"},"description":"Bounding box: west,south,east,north"},{"name":"point","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Center point: lat,lon","title":"Point"},"description":"Center point: lat,lon"},{"name":"radius","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Radius from point (e.g., 100km)","title":"Radius"},"description":"Radius from point (e.g., 100km)"},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"US state abbreviation","title":"State"},"description":"US state abbreviation"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Max results","default":100,"title":"Limit"},"description":"Max results"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Pagination offset","default":0,"title":"Offset"},"description":"Pagination offset"},{"name":"day","in":"query","required":false,"schema":{"type":"integer","maximum":8,"minimum":1,"description":"Outlook day (1-8)","default":1,"title":"Day"},"description":"Outlook day (1-8)"},{"name":"type","in":"query","required":false,"schema":{"type":"string","description":"Type: categorical|tornado|hail|wind","default":"categorical","title":"Type"},"description":"Type: categorical|tornado|hail|wind"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoJSONFeatureCollection"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/outlooks/where":{"get":{"tags":["Outlooks"],"summary":"Outlook Where","description":"Plain-language \"where\" for the daily-outlook page: which states the top\n*severe* (MRGL+) Day-1 categorical risk covers, plus states with dangerous\nflooding right now. Retained for backward compatibility; new callers should\nprefer /briefing, which returns this plus the full day summary + hazards.","operationId":"outlook_where_api_v1_outlooks_where_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/outlooks/briefing":{"get":{"tags":["Outlooks"],"summary":"Outlook Briefing","description":"Deterministic daily severe-weather briefing (see build_briefing), plus the\nid of today's outlook video on YouTube if it's been posted (for the /outlook\npage embed).\n\nCached in Redis for a short window. A Redis outage must never take the\nbriefing down, so every cache interaction is best-effort and falls through\nto the real query.","operationId":"outlook_briefing_api_v1_outlooks_briefing_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/reports":{"get":{"tags":["Reports"],"summary":"Get Reports","description":"Get storm reports from SPC, mPING, and Spotter Network.","operationId":"get_reports_api_v1_reports_get","parameters":[{"name":"bbox","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Bounding box: west,south,east,north","title":"Bbox"},"description":"Bounding box: west,south,east,north"},{"name":"point","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Center point: lat,lon","title":"Point"},"description":"Center point: lat,lon"},{"name":"radius","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Radius from point (e.g., 100km)","title":"Radius"},"description":"Radius from point (e.g., 100km)"},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"US state abbreviation","title":"State"},"description":"US state abbreviation"},{"name":"start","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Start of time range (ISO 8601)","title":"Start"},"description":"Start of time range (ISO 8601)"},{"name":"end","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"End of time range (ISO 8601)","title":"End"},"description":"End of time range (ISO 8601)"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Max results","default":100,"title":"Limit"},"description":"Max results"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Pagination offset","default":0,"title":"Offset"},"description":"Pagination offset"},{"name":"type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter: tornado|hail|wind|flood","title":"Type"},"description":"Filter: tornado|hail|wind|flood"},{"name":"source","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter: spc|mping|spotter_network","title":"Source"},"description":"Filter: spc|mping|spotter_network"},{"name":"min_magnitude","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"Min magnitude (e.g., hail size)","title":"Min Magnitude"},"description":"Min magnitude (e.g., hail size)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoJSONFeatureCollection"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/earthquakes":{"get":{"tags":["Earthquakes"],"summary":"Get Earthquakes","description":"Significant earthquakes from the USGS feed, as GeoJSON.","operationId":"get_earthquakes_api_v1_earthquakes_get","parameters":[{"name":"bbox","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Bounding box: west,south,east,north","title":"Bbox"},"description":"Bounding box: west,south,east,north"},{"name":"point","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Center point: lat,lon","title":"Point"},"description":"Center point: lat,lon"},{"name":"radius","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Radius from point (e.g., 100km)","title":"Radius"},"description":"Radius from point (e.g., 100km)"},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"US state abbreviation","title":"State"},"description":"US state abbreviation"},{"name":"start","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Start of time range (ISO 8601)","title":"Start"},"description":"Start of time range (ISO 8601)"},{"name":"end","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"End of time range (ISO 8601)","title":"End"},"description":"End of time range (ISO 8601)"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Max results","default":100,"title":"Limit"},"description":"Max results"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Pagination offset","default":0,"title":"Offset"},"description":"Pagination offset"},{"name":"min_magnitude","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"Minimum magnitude","title":"Min Magnitude"},"description":"Minimum magnitude"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoJSONFeatureCollection"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/radar/tiles/{product}/{z}/{x}/{y}.png":{"get":{"tags":["Radar"],"summary":"Get Radar Tile","description":"Return a radar tile as PNG.","operationId":"get_radar_tile_api_v1_radar_tiles__product___z___x___y__png_get","parameters":[{"name":"product","in":"path","required":true,"schema":{"type":"string","description":"Radar product ID","title":"Product"},"description":"Radar product ID"},{"name":"z","in":"path","required":true,"schema":{"type":"integer","maximum":14,"minimum":0,"title":"Z"}},{"name":"x","in":"path","required":true,"schema":{"type":"integer","minimum":0,"title":"X"}},{"name":"y","in":"path","required":true,"schema":{"type":"integer","minimum":0,"title":"Y"}},{"name":"ts","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Frame timestamp YYYYMMDDHHMM; omit for latest","title":"Ts"},"description":"Frame timestamp YYYYMMDDHHMM; omit for latest"},{"name":"smooth","in":"query","required":false,"schema":{"type":"boolean","description":"Bilinearly resample the source grid for smoother tiles","default":false,"title":"Smooth"},"description":"Bilinearly resample the source grid for smoother tiles"},{"name":"palette","in":"query","required":false,"schema":{"type":"string","description":"Reflectivity colormap: a built-in (nws | classic | spectrum) or a custom:<id> ref from POST /radar/palettes. Ignored for non-reflectivity products like mesh/rotation.","default":"nws","title":"Palette"},"description":"Reflectivity colormap: a built-in (nws | classic | spectrum) or a custom:<id> ref from POST /radar/palettes. Ignored for non-reflectivity products like mesh/rotation."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/radar/point":{"get":{"tags":["Radar"],"summary":"Get Radar Point","description":"Sample an MRMS product's physical value at a single lat/lon point.\n\n\"What hail size covered this address?\" — returns the decoded value at the\npoint (e.g. MESH in mm), or `null` where the grid is below its display\nthreshold or has no data (i.e. no hail there). Only the CONUS grid extent is\ncovered; a point outside it clamps to the nearest edge cell. Encoding is\nlossy (~vrange/253 per step), so treat the value as approximate.","operationId":"get_radar_point_api_v1_radar_point_get","parameters":[{"name":"lat","in":"query","required":true,"schema":{"type":"number","maximum":90,"minimum":-90,"title":"Lat"}},{"name":"lon","in":"query","required":true,"schema":{"type":"number","maximum":180,"minimum":-180,"title":"Lon"}},{"name":"product","in":"query","required":false,"schema":{"type":"string","description":"MRMS product id (see /radar/products)","default":"mesh-30min","title":"Product"},"description":"MRMS product id (see /radar/products)"},{"name":"ts","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Frame timestamp YYYYMMDDHHMM; omit for latest","title":"Ts"},"description":"Frame timestamp YYYYMMDDHHMM; omit for latest"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/radar/frames":{"get":{"tags":["Radar"],"summary":"Get Radar Frames","description":"Return the list of available frame timestamps for animation.\n\n`minutes` is the look-back window (up to 24 h); `interval` thins the frames\nto roughly that many minutes apart so a long window stays a watchable,\ntile-fetch-bounded number of frames — a 24 h loop at native 2-min cadence is\n~720 frames, but at a 15-min interval it's ~96. The most recent frame is\nalways included so \"LIVE\" is exact.","operationId":"get_radar_frames_api_v1_radar_frames_get","parameters":[{"name":"product","in":"query","required":false,"schema":{"type":"string","default":"composite-reflectivity","title":"Product"}},{"name":"minutes","in":"query","required":false,"schema":{"type":"integer","maximum":1440,"minimum":5,"default":30,"title":"Minutes"}},{"name":"interval","in":"query","required":false,"schema":{"type":"integer","maximum":30,"minimum":2,"default":2,"title":"Interval"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/radar/palettes":{"post":{"tags":["Radar"],"summary":"Register Radar Palette","description":"Register a custom reflectivity colormap from RadarScope/GR ``.pal`` text.\n\nReturns a content-hash id to use as ``palette=custom:<id>`` on the\nreflectivity tile endpoints. Anonymous and idempotent — identical tables\nreturn the same id — and applied purely at render time (no re-ingest). The\nparsed stops come back so the client can render a legend/preview without\nre-parsing.","operationId":"register_radar_palette_api_v1_radar_palettes_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaletteUpload"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/radar/products":{"get":{"tags":["Radar"],"summary":"List Products","operationId":"list_products_api_v1_radar_products_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/aifs/steps":{"get":{"tags":["AI Models"],"summary":"Get Steps","description":"Available lead times for the latest AIFS cycle — drives the lead slider.","operationId":"get_steps_api_v1_aifs_steps_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/aifs/tiles/{field}/{z}/{x}/{y}.png":{"get":{"tags":["AI Models"],"summary":"Get Field Tile","description":"An AIFS field tile at forecast lead `step` (hours) of the latest run.","operationId":"get_field_tile_api_v1_aifs_tiles__field___z___x___y__png_get","parameters":[{"name":"field","in":"path","required":true,"schema":{"type":"string","description":"AIFS field: temp | precip","title":"Field"},"description":"AIFS field: temp | precip"},{"name":"z","in":"path","required":true,"schema":{"type":"integer","maximum":14,"minimum":0,"title":"Z"}},{"name":"x","in":"path","required":true,"schema":{"type":"integer","minimum":0,"title":"X"}},{"name":"y","in":"path","required":true,"schema":{"type":"integer","minimum":0,"title":"Y"}},{"name":"step","in":"query","required":false,"schema":{"type":"integer","description":"Forecast lead time in hours","default":24,"title":"Step"},"description":"Forecast lead time in hours"},{"name":"smooth","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Smooth"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/nexrad/sites":{"get":{"tags":["NEXRAD Level II"],"summary":"Nexrad Sites","description":"Catalog of selectable radar sites. 404 when the feature is disabled —\nthe frontend uses that to hide the whole NEXRAD UI.","operationId":"nexrad_sites_api_v1_nexrad_sites_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/nexrad/{site}/meta":{"get":{"tags":["NEXRAD Level II"],"summary":"Nexrad Meta","description":"Latest-volume metadata + available products + tile URL template.","operationId":"nexrad_meta_api_v1_nexrad__site__meta_get","parameters":[{"name":"site","in":"path","required":true,"schema":{"type":"string","title":"Site"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/nexrad/{site}/{product}/{z}/{x}/{y}.png":{"get":{"tags":["NEXRAD Level II"],"summary":"Nexrad Tile","description":"Single-tilt tile for a site + product (reflectivity | velocity | srm),\nrendered on demand. ``tilt`` selects the elevation sweep (0 = lowest). For\nsrm, sm_dir/sm_kt set the storm motion removed.","operationId":"nexrad_tile_api_v1_nexrad__site___product___z___x___y__png_get","parameters":[{"name":"site","in":"path","required":true,"schema":{"type":"string","title":"Site"}},{"name":"product","in":"path","required":true,"schema":{"type":"string","title":"Product"}},{"name":"z","in":"path","required":true,"schema":{"type":"integer","maximum":14,"minimum":0,"title":"Z"}},{"name":"x","in":"path","required":true,"schema":{"type":"integer","minimum":0,"title":"X"}},{"name":"y","in":"path","required":true,"schema":{"type":"integer","minimum":0,"title":"Y"}},{"name":"palette","in":"query","required":false,"schema":{"type":"string","default":"nws","title":"Palette"}},{"name":"sm_dir","in":"query","required":false,"schema":{"type":"number","default":0.0,"title":"Sm Dir"}},{"name":"sm_kt","in":"query","required":false,"schema":{"type":"number","default":0.0,"title":"Sm Kt"}},{"name":"tilt","in":"query","required":false,"schema":{"type":"integer","maximum":20,"minimum":0,"default":0,"title":"Tilt"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/hrrr/tiles/{variable}/{z}/{x}/{y}.png":{"get":{"tags":["HRRR Forecasts"],"summary":"Get Hrrr Tile","operationId":"get_hrrr_tile_api_v1_hrrr_tiles__variable___z___x___y__png_get","parameters":[{"name":"variable","in":"path","required":true,"schema":{"type":"string","description":"HRRR variable id","title":"Variable"},"description":"HRRR variable id"},{"name":"z","in":"path","required":true,"schema":{"type":"integer","maximum":14,"minimum":0,"title":"Z"}},{"name":"x","in":"path","required":true,"schema":{"type":"integer","minimum":0,"title":"X"}},{"name":"y","in":"path","required":true,"schema":{"type":"integer","minimum":0,"title":"Y"}},{"name":"ts","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Frame timestamp YYYYMMDDHHFF (cycle date+hour, forecast offset). Mutually exclusive with `valid`; omit both for latest analysis.","title":"Ts"},"description":"Frame timestamp YYYYMMDDHHFF (cycle date+hour, forecast offset). Mutually exclusive with `valid`; omit both for latest analysis."},{"name":"valid","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Valid time in UTC (YYYYMMDDHH or YYYY-MM-DDTHH). Server resolves to the most recent cycle producing that valid time. Falls back to the closest available frame with X-Frame-Mismatch header set.","title":"Valid"},"description":"Valid time in UTC (YYYYMMDDHH or YYYY-MM-DDTHH). Server resolves to the most recent cycle producing that valid time. Falls back to the closest available frame with X-Frame-Mismatch header set."},{"name":"smooth","in":"query","required":false,"schema":{"type":"boolean","description":"Bilinearly resample the source grid for smoother tiles. HRRR's ~3 km native grid looks blocky at zoom 5+ without it.","default":true,"title":"Smooth"},"description":"Bilinearly resample the source grid for smoother tiles. HRRR's ~3 km native grid looks blocky at zoom 5+ without it."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/hrrr/variables":{"get":{"tags":["HRRR Forecasts"],"summary":"List Variables","description":"Catalog of available HRRR variables for the docs page / clients.","operationId":"list_variables_api_v1_hrrr_variables_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/hrrr/timeline":{"get":{"tags":["HRRR Forecasts"],"summary":"Get Timeline","description":"Per-variable list of valid times currently stored on disk.\n\nFrontend uses this to populate the map-side forecast-hour scrubber\nwith real availability instead of assuming F00 → F18 of every\nvariable is ready. Each entry returns:\n\n  - latest_cycle: cycle id (YYYYMMDDHH) of the most recent cycle\n    with any frames stored.\n  - valid_times: sorted list of UTC hours (YYYYMMDDHH) where at\n    least one frame is available. The list spans the union of all\n    cycles in retention, so two-cycle overlap regions get a single\n    entry (the resolver prefers the most recent cycle for those).","operationId":"get_timeline_api_v1_hrrr_timeline_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/satellite/tiles/{product}/{z}/{x}/{y}.png":{"get":{"tags":["Satellite"],"summary":"Get Satellite Tile","description":"Return a satellite tile as PNG.","operationId":"get_satellite_tile_api_v1_satellite_tiles__product___z___x___y__png_get","parameters":[{"name":"product","in":"path","required":true,"schema":{"type":"string","title":"Product"}},{"name":"z","in":"path","required":true,"schema":{"type":"integer","maximum":10,"minimum":0,"title":"Z"}},{"name":"x","in":"path","required":true,"schema":{"type":"integer","minimum":0,"title":"X"}},{"name":"y","in":"path","required":true,"schema":{"type":"integer","minimum":0,"title":"Y"}},{"name":"ts","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Frame timestamp YYYYMMDDHHMM","title":"Ts"},"description":"Frame timestamp YYYYMMDDHHMM"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/satellite/products":{"get":{"tags":["Satellite"],"summary":"List Products","operationId":"list_products_api_v1_satellite_products_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/satellite/frames":{"get":{"tags":["Satellite"],"summary":"Get Frames","description":"List animation frame timestamps for a local product.","operationId":"get_frames_api_v1_satellite_frames_get","parameters":[{"name":"product","in":"query","required":false,"schema":{"type":"string","default":"ir","title":"Product"}},{"name":"minutes","in":"query","required":false,"schema":{"type":"integer","maximum":360,"minimum":5,"default":60,"title":"Minutes"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/soundings":{"get":{"tags":["Soundings"],"summary":"List Soundings","description":"List available soundings with summary and derived parameters.\n\nDoes not return the full profile arrays — use /api/v1/soundings/{id}\nor /api/v1/soundings/{station}/latest for full profiles.","operationId":"list_soundings_api_v1_soundings_get","parameters":[{"name":"bbox","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Bounding box: west,south,east,north","title":"Bbox"},"description":"Bounding box: west,south,east,north"},{"name":"point","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Center point: lat,lon","title":"Point"},"description":"Center point: lat,lon"},{"name":"radius","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Radius from point (e.g., 100km)","title":"Radius"},"description":"Radius from point (e.g., 100km)"},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"US state abbreviation","title":"State"},"description":"US state abbreviation"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Max results","default":50,"title":"Limit"},"description":"Max results"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Pagination offset","default":0,"title":"Offset"},"description":"Pagination offset"},{"name":"station","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Station ID filter (e.g., OUN, KOUN)","title":"Station"},"description":"Station ID filter (e.g., OUN, KOUN)"},{"name":"latest","in":"query","required":false,"schema":{"type":"boolean","description":"Return only latest sounding per station","default":true,"title":"Latest"},"description":"Return only latest sounding per station"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoJSONFeatureCollection"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/soundings/{station}/latest":{"get":{"tags":["Soundings"],"summary":"Latest Sounding","description":"Get the latest sounding for a station with full profile.","operationId":"latest_sounding_api_v1_soundings__station__latest_get","parameters":[{"name":"station","in":"path","required":true,"schema":{"type":"string","title":"Station"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/soundings/point":{"get":{"tags":["Soundings"],"summary":"Point Sounding","description":"Model-derived sounding series (HRRR Skew-T) at any lat/lon point.\n\nPulls HRRR pressure-level data from Open-Meteo's free forecast API\nand returns the FULL forecast series — every hourly slot Open-Meteo\npublishes for the day (typically 24 entries). The frontend caches\nthe series and lets meteorologists scrub through model evolution\nwithout re-hitting the API for each forecast hour.\n\nEach entry contains the same profile + derived block (all three\nparcel types + Bunkers RM) that the station endpoint returns.\n`current_index` flags the slot closest to \"now\" so the panel can\ndefault the scrubber sensibly.","operationId":"point_sounding_api_v1_soundings_point_get","parameters":[{"name":"lat","in":"query","required":true,"schema":{"type":"number","maximum":90,"minimum":-90,"description":"Latitude","title":"Lat"},"description":"Latitude"},{"name":"lon","in":"query","required":true,"schema":{"type":"number","maximum":180,"minimum":-180,"description":"Longitude","title":"Lon"},"description":"Longitude"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/models/hrrr/point":{"get":{"tags":["Models"],"summary":"Hrrr Point Forecast","description":"Get HRRR point forecast for severe weather parameters.\n\nReturns hourly forecasts for CAPE, CIN, lifted index, wind, and precipitation.","operationId":"hrrr_point_forecast_api_v1_models_hrrr_point_get","parameters":[{"name":"lat","in":"query","required":true,"schema":{"type":"number","maximum":90,"minimum":-90,"title":"Lat"}},{"name":"lon","in":"query","required":true,"schema":{"type":"number","maximum":180,"minimum":-180,"title":"Lon"}},{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":48,"minimum":1,"description":"Forecast hours","default":18,"title":"Hours"},"description":"Forecast hours"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/models/available":{"get":{"tags":["Models"],"summary":"List Models","description":"List available NWP models and their capabilities.","operationId":"list_models_api_v1_models_available_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/geocode/search":{"get":{"tags":["Geocoding"],"summary":"Search","description":"Forward geocode a query string to lat/lon candidates.","operationId":"search_api_v1_geocode_search_get","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":2,"description":"City, state, ZIP, address, or place","title":"Q"},"description":"City, state, ZIP, address, or place"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":10,"minimum":1,"default":5,"title":"Limit"}},{"name":"country","in":"query","required":false,"schema":{"type":"string","description":"ISO country code (default 'us')","default":"us","title":"Country"},"description":"ISO country code (default 'us')"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/geocode/reverse":{"get":{"tags":["Geocoding"],"summary":"Reverse","description":"Reverse geocode a lat/lon to a place name.","operationId":"reverse_api_v1_geocode_reverse_get","parameters":[{"name":"lat","in":"query","required":true,"schema":{"type":"number","maximum":90,"minimum":-90,"title":"Lat"}},{"name":"lon","in":"query","required":true,"schema":{"type":"number","maximum":180,"minimum":-180,"title":"Lon"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/locations/summary":{"get":{"tags":["Locations"],"summary":"Location Summary","description":"Return a complete weather summary for a lat/lon point.","operationId":"location_summary_api_v1_locations_summary_get","parameters":[{"name":"lat","in":"query","required":true,"schema":{"type":"number","maximum":90,"minimum":-90,"title":"Lat"}},{"name":"lon","in":"query","required":true,"schema":{"type":"number","maximum":180,"minimum":-180,"title":"Lon"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/gauges":{"get":{"tags":["Gauges"],"summary":"List Gauges","description":"List USGS river/stream gauges with their latest readings.","operationId":"list_gauges_api_v1_gauges_get","parameters":[{"name":"bbox","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Bounding box: west,south,east,north","title":"Bbox"},"description":"Bounding box: west,south,east,north"},{"name":"point","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Center point: lat,lon","title":"Point"},"description":"Center point: lat,lon"},{"name":"radius","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Radius from point (e.g., 100km)","title":"Radius"},"description":"Radius from point (e.g., 100km)"},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"US state abbreviation","title":"State"},"description":"US state abbreviation"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Max results","default":200,"title":"Limit"},"description":"Max results"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Pagination offset","default":0,"title":"Offset"},"description":"Pagination offset"},{"name":"above_flood","in":"query","required":false,"schema":{"type":"boolean","description":"Only gauges currently above known flood stage","default":false,"title":"Above Flood"},"description":"Only gauges currently above known flood stage"},{"name":"parameter","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to gauges reporting this parameter: streamflow|gage_height|water_temp","title":"Parameter"},"description":"Filter to gauges reporting this parameter: streamflow|gage_height|water_temp"},{"name":"recent_hours","in":"query","required":false,"schema":{"type":"integer","maximum":48,"minimum":1,"description":"Only gauges with readings in the past N hours","default":6,"title":"Recent Hours"},"description":"Only gauges with readings in the past N hours"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoJSONFeatureCollection"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/gauges/{gauge_id}/forecast":{"get":{"tags":["Gauges"],"summary":"Get Gauge Forecast","description":"The stored NWPS forecast stage/flow series for one gauge.\n\n`forecast` is null when the gauge has no active forecast (not an AHPS point,\nor a quiet river). Series shape:\n`{issued_at, stage_unit, flow_unit, points: [{valid_time, stage, flow}]}`.\nRefreshed hourly; `forecast_updated_at` is when we last fetched it.","operationId":"get_gauge_forecast_api_v1_gauges__gauge_id__forecast_get","parameters":[{"name":"gauge_id","in":"path","required":true,"schema":{"type":"integer","title":"Gauge Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/lightning":{"get":{"tags":["Lightning"],"summary":"Get Lightning","description":"Recent lightning flashes from GOES-19 GLM.\n\nReturns a GeoJSON FeatureCollection of flash point geometries. Properties\ninclude flash_time (ISO 8601), age_seconds (relative to now), and optional\nenergy/area fields when the source file included them.","operationId":"get_lightning_api_v1_lightning_get","parameters":[{"name":"bbox","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Bounding box: west,south,east,north","title":"Bbox"},"description":"Bounding box: west,south,east,north"},{"name":"point","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Center point: lat,lon","title":"Point"},"description":"Center point: lat,lon"},{"name":"radius","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Radius from point (e.g., 100km)","title":"Radius"},"description":"Radius from point (e.g., 100km)"},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"US state abbreviation","title":"State"},"description":"US state abbreviation"},{"name":"minutes","in":"query","required":false,"schema":{"type":"integer","maximum":180,"minimum":1,"description":"Return flashes from the last N minutes","default":15,"title":"Minutes"},"description":"Return flashes from the last N minutes"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":20000,"minimum":1,"description":"Max features to return. Lightning can easily produce several thousand flashes over a 15-minute window during active storms.","default":5000,"title":"Limit"},"description":"Max features to return. Lightning can easily produce several thousand flashes over a 15-minute window during active storms."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoJSONFeatureCollection"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/signup":{"post":{"tags":["Auth"],"summary":"Signup","description":"Create a new user account and issue a session cookie.","operationId":"signup_api_v1_auth_signup_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/wxagg__api__v1__auth__SignupRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/login":{"post":{"tags":["Auth"],"summary":"Login","operationId":"login_api_v1_auth_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/logout":{"post":{"tags":["Auth"],"summary":"Logout","operationId":"logout_api_v1_auth_logout_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/auth/me":{"get":{"tags":["Auth"],"summary":"Me","operationId":"me_api_v1_auth_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}}}}},"/api/v1/auth/verify-email":{"post":{"tags":["Auth"],"summary":"Verify Email","operationId":"verify_email_api_v1_auth_verify_email_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyEmailRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/resend-verification":{"post":{"tags":["Auth"],"summary":"Resend Verification","operationId":"resend_verification_api_v1_auth_resend_verification_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/auth/forgot-password":{"post":{"tags":["Auth"],"summary":"Forgot Password","description":"Always returns success so we don't leak account existence.","operationId":"forgot_password_api_v1_auth_forgot_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForgotPasswordRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/reset-password":{"post":{"tags":["Auth"],"summary":"Reset Password","operationId":"reset_password_api_v1_auth_reset_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/unsubscribe":{"post":{"tags":["Account"],"summary":"Unsubscribe","description":"Opt a user out of commercial / lifecycle email. Public and login-free\n(CAN-SPAM): the HMAC token in the link authenticates the request. Idempotent\nand intentionally enumeration-safe — returns ok whether or not the user\nexists, only rejecting a tampered/invalid token.","operationId":"unsubscribe_api_v1_account_unsubscribe_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnsubscribeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/api-keys":{"get":{"tags":["Account"],"summary":"List Api Keys","operationId":"list_api_keys_api_v1_account_api_keys_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/APIKeyResponse"},"type":"array","title":"Response List Api Keys Api V1 Account Api Keys Get"}}}}}},"post":{"tags":["Account"],"summary":"Create Api Key","operationId":"create_api_key_api_v1_account_api_keys_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAPIKeyRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/APIKeyCreatedResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/api-keys/{key_id}":{"delete":{"tags":["Account"],"summary":"Revoke Api Key","operationId":"revoke_api_key_api_v1_account_api_keys__key_id__delete","parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"integer","title":"Key Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/api-keys/{key_id}/rotate":{"post":{"tags":["Account"],"summary":"Rotate Api Key","description":"Replace a key's secret in place: same row (id, name, rate_limit,\ncreated_at), brand-new token. The old token stops authenticating the moment\nthe new hash is written — no create-then-delete dance against the key cap.\nLike creation, the full token is returned exactly once.","operationId":"rotate_api_key_api_v1_account_api_keys__key_id__rotate_post","parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"integer","title":"Key Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/APIKeyCreatedResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/api-keys/usage":{"get":{"tags":["Account"],"summary":"Api Key Usage","description":"Request + throttle totals for the last 7 days across all of the caller's\nkeys, from the limiter's per-key daily counters (services/rate_limit.py).\nDegrades to zeros when Redis is unavailable rather than erroring.","operationId":"api_key_usage_api_v1_account_api_keys_usage_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyUsageResponse"}}}}}}},"/api/v1/account/locations":{"get":{"tags":["Account"],"summary":"List Locations","operationId":"list_locations_api_v1_account_locations_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/SavedLocationResponse"},"type":"array","title":"Response List Locations Api V1 Account Locations Get"}}}}}},"post":{"tags":["Account"],"summary":"Create Location","operationId":"create_location_api_v1_account_locations_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLocationRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedLocationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/locations/{loc_id}":{"patch":{"tags":["Account"],"summary":"Update Location","operationId":"update_location_api_v1_account_locations__loc_id__patch","parameters":[{"name":"loc_id","in":"path","required":true,"schema":{"type":"integer","title":"Loc Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLocationRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedLocationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Account"],"summary":"Delete Location","operationId":"delete_location_api_v1_account_locations__loc_id__delete","parameters":[{"name":"loc_id","in":"path","required":true,"schema":{"type":"integer","title":"Loc Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/composite-alerts":{"get":{"tags":["Account"],"summary":"List Composite Alerts","description":"Recent composite-index (SCP/SHIP) alerts pushed to this user.\n\nJoined to saved_locations for the display name. Capped at `limit`\n(default 50, ~the last few weeks of alerts for an active user) and\nordered newest-first.","operationId":"list_composite_alerts_api_v1_account_composite_alerts_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CompositeAlertHistoryItem"},"title":"Response List Composite Alerts Api V1 Account Composite Alerts Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/tropical-cone-alerts":{"get":{"tags":["Account"],"summary":"List Cone Alerts","description":"Storms whose forecast cone has covered this user's saved locations.\n\nOne row per (location, storm); `covered` reflects whether the cone\ncurrently covers it (vs. cleared after the storm moved off). Newest\nfirst. Mirrors the composite-alert history endpoint.","operationId":"list_cone_alerts_api_v1_account_tropical_cone_alerts_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ConeAlertHistoryItem"},"title":"Response List Cone Alerts Api V1 Account Tropical Cone Alerts Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/push/vapid-public-key":{"get":{"tags":["Account"],"summary":"Get Vapid Public Key","description":"Public VAPID key the frontend uses when calling pushManager.subscribe().","operationId":"get_vapid_public_key_api_v1_account_push_vapid_public_key_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/account/push/subscriptions":{"get":{"tags":["Account"],"summary":"List Push Subscriptions","operationId":"list_push_subscriptions_api_v1_account_push_subscriptions_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/PushSubscriptionResponse"},"type":"array","title":"Response List Push Subscriptions Api V1 Account Push Subscriptions Get"}}}}}}},"/api/v1/account/push/subscribe":{"post":{"tags":["Account"],"summary":"Create Push Subscription","description":"Register a browser's push subscription. Idempotent on endpoint —\nre-subscribing the same browser updates the existing row instead of\ncreating a duplicate.","operationId":"create_push_subscription_api_v1_account_push_subscribe_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PushSubscribeRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PushSubscriptionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/push/unsubscribe":{"post":{"tags":["Account"],"summary":"Delete Push Subscription","description":"Remove a single browser's subscription. Used when the user disables\npush in the UI. Idempotent — succeeds even if the row is gone.","operationId":"delete_push_subscription_api_v1_account_push_unsubscribe_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PushUnsubscribeRequest"}}},"required":true},"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/device-location":{"get":{"tags":["Account"],"summary":"Get Device Location","description":"Current opt-in state for this device's location anchor. Returns\ndisabled when the device isn't subscribed or has never opted in.","operationId":"get_device_location_api_v1_account_device_location_get","parameters":[{"name":"endpoint","in":"query","required":true,"schema":{"type":"string","title":"Endpoint"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceLocationStateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Account"],"summary":"Upsert Device Location","description":"Opt this device in (or refresh its position). Requires the device to\nalready have a push subscription registered.","operationId":"upsert_device_location_api_v1_account_device_location_put","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceLocationRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceLocationStateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Account"],"summary":"Delete Device Location","description":"Opt this device out — removes its location anchor. Idempotent.\nReuses PushUnsubscribeRequest ({endpoint}).","operationId":"delete_device_location_api_v1_account_device_location_delete","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PushUnsubscribeRequest"}}}},"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/webhooks":{"get":{"tags":["Developer Webhooks"],"summary":"List Webhooks","operationId":"list_webhooks_api_v1_account_webhooks_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/WebhookResponse"},"type":"array","title":"Response List Webhooks Api V1 Account Webhooks Get"}}}}}},"post":{"tags":["Developer Webhooks"],"summary":"Create Webhook","operationId":"create_webhook_api_v1_account_webhooks_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreatedResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/webhooks/{webhook_id}":{"patch":{"tags":["Developer Webhooks"],"summary":"Update Webhook","operationId":"update_webhook_api_v1_account_webhooks__webhook_id__patch","parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"integer","title":"Webhook Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWebhookRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Developer Webhooks"],"summary":"Delete Webhook","operationId":"delete_webhook_api_v1_account_webhooks__webhook_id__delete","parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"integer","title":"Webhook Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/webhooks/{webhook_id}/rotate":{"post":{"tags":["Developer Webhooks"],"summary":"Rotate Webhook Secret","operationId":"rotate_webhook_secret_api_v1_account_webhooks__webhook_id__rotate_post","parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"integer","title":"Webhook Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreatedResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/webhooks/{webhook_id}/test":{"post":{"tags":["Developer Webhooks"],"summary":"Test Webhook","description":"Send a synthetic ``webhook.test`` payload to the endpoint right now and\nreturn the delivery outcome. Doesn't touch the dedup ledger or health.","operationId":"test_webhook_api_v1_account_webhooks__webhook_id__test_post","parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"integer","title":"Webhook Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/push/subscribe":{"post":{"tags":["Push (anonymous devices)"],"summary":"Anon Subscribe","description":"Register an anonymous browser's push subscription (user_id NULL).\n\nThis endpoint is unauthenticated — the only thing identifying the caller is\nthe push `endpoint` URL they present. That makes overwriting an existing\nrow's crypto keys dangerous: whoever knows an endpoint could otherwise\nrepoint it at *their* p256dh/auth, and we'd then encrypt every future alert\nto keys the real device can't read — silently killing that device's tornado\nwarnings (API_REVIEW 1.4). So the rules are:\n\n- Unknown endpoint → register a new anonymous subscription.\n- Known endpoint, SAME keys → the same device refreshing; update only the\n  user-agent label. (A browser reuses one subscription and always sends back\n  the same keys, so this is the normal re-subscribe path.)\n- Known endpoint, DIFFERENT keys → refuse (409). We can't authenticate the\n  caller, and a real browser never gets here: its keys are stable for the\n  life of a subscription, and a rotated subscription arrives with a NEW\n  endpoint (the insert path). If an endpoint is ever recycled to a new\n  device, the sender prunes the dead row on the next 404/410 and the new\n  device then registers cleanly.\n\nThe endpoint host is validated first so an unauthenticated caller can't seed\njunk rows or point our push sender at an internal host.","operationId":"anon_subscribe_api_v1_push_subscribe_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnonSubscribeRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/push/unsubscribe":{"post":{"tags":["Push (anonymous devices)"],"summary":"Anon Unsubscribe","description":"Remove an ANONYMOUS subscription by endpoint (idempotent). Scoped to\n`user_id IS NULL` so it can never delete a logged-in user's subscription.","operationId":"anon_unsubscribe_api_v1_push_unsubscribe_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnonUnsubscribeRequest"}}},"required":true},"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/push/device-location":{"get":{"tags":["Push (anonymous devices)"],"summary":"Anon Get Device Location","description":"Current opt-in state for this device's location anchor, by endpoint.","operationId":"anon_get_device_location_api_v1_push_device_location_get","parameters":[{"name":"endpoint","in":"query","required":true,"schema":{"type":"string","minLength":1,"title":"Endpoint"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceLocationState"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Push (anonymous devices)"],"summary":"Anon Upsert Device Location","description":"Opt this device in / refresh its last-known anchor, keyed by the push\nendpoint. The device must already have a subscription. Coordinates are\ncoarsened to ~1 km. `user_id` is copied from the subscription (NULL for an\nanonymous device).","operationId":"anon_upsert_device_location_api_v1_push_device_location_put","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnonDeviceLocationRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceLocationState"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Push (anonymous devices)"],"summary":"Anon Disable Device Location","description":"Opt out: clear this device's location anchor (keyed by endpoint).\nIdempotent.","operationId":"anon_disable_device_location_api_v1_push_device_location_delete","parameters":[{"name":"endpoint","in":"query","required":true,"schema":{"type":"string","minLength":1,"title":"Endpoint"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/push/device-token":{"post":{"tags":["Push (anonymous devices)"],"summary":"Register Device Token","description":"Register (or refresh) a native device's push token. Idempotent.\n\nUnauthenticated, like `/subscribe` above, and it carries the same hazard in\na different shape. There the danger was repointing a known endpoint at an\nattacker's crypto keys, silently killing that device's tornado warnings\n(API_REVIEW 1.4). Here a token cannot be repointed — it *is* the address,\nand only Google and the device can mint one — but a known token could be\nre-registered to claim it, so this path never writes `user_id`. An\nanonymous refresh therefore cannot detach a device from the account that\nowns it, and cannot attach one either.\n\nFCM reissues tokens on reinstall and restore, so re-registration is the\nnormal case, not an edge one.","operationId":"register_device_token_api_v1_push_device_token_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceTokenRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/push/device-token/delete":{"post":{"tags":["Push (anonymous devices)"],"summary":"Delete Device Token","description":"Forget an ANONYMOUS device token (idempotent).\n\nScoped to `user_id IS NULL` for the same reason `/unsubscribe` is: an\nunauthenticated caller must not be able to delete the push route of a device\nthat belongs to a signed-in account.","operationId":"delete_device_token_api_v1_push_device_token_delete_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceTokenRequest"}}},"required":true},"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/push/device-token/location":{"get":{"tags":["Push (anonymous devices)"],"summary":"Get Device Token Location","description":"Current opt-in state for a native device's location anchor.","operationId":"get_device_token_location_api_v1_push_device_token_location_get","parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string","minLength":16,"title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceLocationState"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Push (anonymous devices)"],"summary":"Upsert Device Token Location","description":"Opt a native device in / refresh its anchor. Coordinates are coarsened to\n~1 km, exactly as the browser path does — a warning polygon is county-scale,\nso nothing is lost, and we never hold a precise address.\n\n`user_id` is copied from the token row, which is NULL until the app signs\nin. That is the point: \"warn me where I am\" has to work before anyone has an\naccount, or the app's first run has nothing to offer.","operationId":"upsert_device_token_location_api_v1_push_device_token_location_put","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceTokenLocationRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceLocationState"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Push (anonymous devices)"],"summary":"Disable Device Token Location","description":"Opt out: clear a native device's anchor. Idempotent.","operationId":"disable_device_token_location_api_v1_push_device_token_location_delete","parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string","minLength":16,"title":"Token"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/push/device-token/heartbeat":{"post":{"tags":["Push (anonymous devices)"],"summary":"Heartbeat Device Token Anchor","description":"\"I have not moved\" — confirm an anchor without measuring it.\n\nThe problem this solves: a device whose user declined background location\ncannot refresh its anchor while the app is closed, so the six-hour staleness\ngate stops its warnings before morning. The background worker runs fine; it\nsimply has nothing it can truthfully say.\n\nThe evidence is the OS's own: Android enters deep idle only after the device\nhas been stationary, and leaves it on significant motion. So a device\nreporting itself idle is Android asserting the phone has not moved — which\nis what the six-hour cutoff was compensating for in the first place.\n\nThis bumps `updated_at` and deliberately **not** `last_fix_at`: the position\nis confirmed, not re-measured, and `FIX_MAX_AGE_SECONDS` caps how long that\ntrust can run. A device that only ever heartbeats expires within a day rather\nthan warning a place someone has left.\n\nRefused unless the device asserts it is stationary, so this cannot become a\ngeneric \"keep my anchor alive\" call.\n\nA **fallback**, not the mechanism: an app holding `ACCESS_BACKGROUND_LOCATION`\nreads location in Doze perfectly well (verified 2026-09-12,\ndocs/DOZE_LOCATION_QUESTION.md). This serves the device that granted only\n\"Approximate\" or declined background location, whose anchor would otherwise\nexpire overnight.","operationId":"heartbeat_device_token_anchor_api_v1_push_device_token_heartbeat_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceTokenHeartbeatRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceLocationState"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/push/device-token/places":{"put":{"tags":["Push (anonymous devices)"],"summary":"Replace Device Places","description":"Replace this device's saved places with exactly this list.\n\nThe whole set rather than add/remove, so the phone's list is the source of\ntruth and a retry after a flaky network can't leave a duplicate or a place\nthe user already deleted. Empty clears them.\n\nPoints only, rounded to ~1 km like the anchor. The app keeps the names (\"the\nkids' school\"); the server never needs them to match a warning. See\n`models/device_saved_place.py`.\n\n404 on an unknown token for the same reason as the anchor: the app\nre-registers on it and sends the list again. Its background worker re-sends\nthe list on every run, which is what keeps a places-only device from being\npruned.","operationId":"replace_device_places_api_v1_push_device_token_places_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DevicePlacesRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DevicePlacesState"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/push/device-token/ack":{"post":{"tags":["Push (anonymous devices)"],"summary":"Ack Device Push","description":"\"It arrived\" — the half of delivery the transport cannot tell us.\n\nAn FCM 200 means Google accepted the message for delivery, not that a phone\never woke up. So a push that was accepted and then silently dropped is\nrecorded exactly like one that got someone out of bed, and \"all Android\ndelivery broke\" looks identical to a quiet week of weather. This is the\nother end of the wire.\n\nDeliberately cheap and idempotent: the app posts it as the *first* thing it\ndoes on receipt, before the detail fetch, because the detail fetch is the\npart that can fail or be killed. A receipt that waited for it would go\nmissing in exactly the conditions worth measuring.\n\nThe token is required and must own the delivery. Without that check a\n`msg_id` — which travels through Google — would be enough for anyone holding\none to mark someone else's warning as delivered, quietly hiding a real\noutage. Wrong pairs are answered as if unknown rather than forbidden, so\nthis cannot be used to test whether a `msg_id` exists.\n\nAlready-acked messages keep their first timestamp. A retry after a flaky\nnetwork should not move the clock, and the first arrival is the honest\nnumber.","operationId":"ack_device_push_api_v1_push_device_token_ack_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceTokenAckRequest"}}},"required":true},"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/push/device-token/message":{"post":{"tags":["Push (anonymous devices)"],"summary":"Device Message","description":"The text of a follow-up push, for the install it was sent to. Wrong pairs\nare answered as unknown, like the receipt, so a `msg_id` (which travels\nthrough Google) is useless to anyone else.","operationId":"device_message_api_v1_push_device_token_message_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceTokenAckRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceMessageOut"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/push/device-token/test-warning":{"post":{"tags":["Push (anonymous devices)"],"summary":"Send Test Warning","description":"\"Will this actually wake me at 2am?\" — answered, on demand.\n\nEverything else about this app is a promise the user cannot check. They grant\nnotification permission, grant background location, and then have to take it\non faith that a tornado warning at 2am will get through Do Not Disturb, a\ndozing phone and a silenced ringer. Until 2026-09-12 nobody could verify that\nwithout hand-editing an anchor in the production database and waiting for an\ningest tick, which is not an answer a user or support can reach.\n\nSent through the **real** dispatch path, with the real interrupt severity, so\nit lands on the channel a genuine warning would and proves the thing that\nmatters — the DND bypass and the sound — rather than simulating it.\n\nIt is also an operational probe: it is the one push we can generate on demand,\nso \"is native delivery working right now\" stops depending on the weather.\n\nUnmistakably labelled as a test on the device. NWS test alerts are filtered\nout of ingest precisely so nothing fake reaches a user looking like a real\nwarning (CLAUDE.md invariant #3), and a feature that manufactures a warning\nhas to honour that rule rather than sit outside it.","operationId":"send_test_warning_api_v1_push_device_token_test_warning_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestWarningRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestWarningResult"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/stats":{"get":{"tags":["Admin"],"summary":"Get Stats","operationId":"get_stats_api_v1_admin_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminStats"}}}}}}},"/api/v1/admin/users":{"get":{"tags":["Admin"],"summary":"List Users","operationId":"list_users_api_v1_admin_users_get","parameters":[{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Search email or name","title":"Q"},"description":"Search email or name"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/users/{user_id}":{"patch":{"tags":["Admin"],"summary":"Update User","operationId":"update_user_api_v1_admin_users__user_id__patch","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserRow"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/em-interest":{"get":{"tags":["Admin"],"summary":"Em Interest","description":"Recent EM pilot / early-access submissions, newest first (admin only).","operationId":"em_interest_api_v1_admin_em_interest_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":200,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/EmInterestRow"},"title":"Response Em Interest Api V1 Admin Em Interest Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/ingest-health":{"get":{"tags":["Admin"],"summary":"Get Ingest Health","description":"Per-source ingest health (read from Redis, populated by scheduler).","operationId":"get_ingest_health_api_v1_admin_ingest_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Ingest Health Api V1 Admin Ingest Health Get"}}}}}}},"/api/v1/admin/social":{"get":{"tags":["Admin"],"summary":"Get Social","description":"Social-pipeline observability: which platforms are connected, whether any\nare currently backed off (integrity block / upload cap), the send/fail\nledger per platform, the most recent posts, and rotating-token freshness.","operationId":"get_social_api_v1_admin_social_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSocial"}}}}}}},"/api/v1/admin/social/flags/{key}":{"patch":{"tags":["Admin"],"summary":"Update Content Flag","description":"Toggle a content-type gate at runtime. Writes (or clears) an `app_flags`\noverride that BOTH the API and the posters read through — so a flip takes\neffect across containers within one poll, no redeploy. `reset` (or a null\n`enabled`) deletes the override, reverting to the env default.","operationId":"update_content_flag_api_v1_admin_social_flags__key__patch","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string","title":"Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFlagRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentFlagRow"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/social/settings/{key}":{"patch":{"tags":["Admin"],"summary":"Update Setting","description":"Set a typed operational knob (a per-platform cap) at runtime — writes (or\nclears) an `app_settings` override both the API and the posters read through,\nso it takes effect across containers within one poll, no redeploy.","operationId":"update_setting_api_v1_admin_social_settings__key__patch","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string","title":"Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSettingRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SettingRow"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/social/tiktok/test-post":{"post":{"tags":["Admin"],"summary":"Tiktok Test Post","description":"Post one current storm's radar loop to TikTok right now via the Content\nPosting API → Direct Post — the on-demand button for the TikTok app-audit\nrecording. Reuses the exact caption, video URL, and publisher the live poster\nuses. Pre-audit the app only offers SELF_ONLY, so it lands private on the\nconnected account; once audited, the same path posts publicly with no change.","operationId":"tiktok_test_post_api_v1_admin_social_tiktok_test_post_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TikTokTestPostResponse"}}}}}}},"/api/v1/admin/email":{"get":{"tags":["Admin"],"summary":"Get Email","description":"Email-delivery ledger from user_email_log. Covers lifecycle/broadcast\nmail only (feedback drips today); transactional sends aren't logged.","operationId":"get_email_api_v1_admin_email_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminEmail"}}}}}}},"/api/v1/admin/postmortems":{"get":{"tags":["Admin"],"summary":"List Postmortems","description":"Drafts first (the review queue), then the rest newest-first.","operationId":"list_postmortems_api_v1_admin_postmortems_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/PostmortemRow"},"type":"array","title":"Response List Postmortems Api V1 Admin Postmortems Get"}}}}}}},"/api/v1/admin/postmortems/{vtec}/publish":{"post":{"tags":["Admin"],"summary":"Publish Postmortem Endpoint","description":"The human click: upload the reviewed draft to YouTube (public), thumbnail,\nplaylist, and mark it published. A draft only; anything else is a 409.","operationId":"publish_postmortem_endpoint_api_v1_admin_postmortems__vtec__publish_post","parameters":[{"name":"vtec","in":"path","required":true,"schema":{"type":"string","title":"Vtec"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostmortemRow"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/postmortems/{vtec}/reject":{"post":{"tags":["Admin"],"summary":"Reject Postmortem Endpoint","operationId":"reject_postmortem_endpoint_api_v1_admin_postmortems__vtec__reject_post","parameters":[{"name":"vtec","in":"path","required":true,"schema":{"type":"string","title":"Vtec"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostmortemRow"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/explainers":{"get":{"tags":["Admin"],"summary":"List Explainers","operationId":"list_explainers_api_v1_admin_explainers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ExplainerRow"},"type":"array","title":"Response List Explainers Api V1 Admin Explainers Get"}}}}}}},"/api/v1/admin/explainers/{slug}/render":{"post":{"tags":["Admin"],"summary":"Render Explainer Endpoint","description":"Kick off a (re)render in the background; poll the list for `draft`.","operationId":"render_explainer_endpoint_api_v1_admin_explainers__slug__render_post","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExplainerRow"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/explainers/{slug}/publish":{"post":{"tags":["Admin"],"summary":"Publish Explainer Endpoint","description":"Upload the rendered draft to YouTube (public 16:9), thumbnail, playlist;\nthe trailer entry also becomes the channel trailer. Re-publishing a stale\nrender uploads a new video (the old one stays; unlist it in Studio).","operationId":"publish_explainer_endpoint_api_v1_admin_explainers__slug__publish_post","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExplainerRow"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/sponsorships":{"get":{"tags":["Admin"],"summary":"List Sponsorships","operationId":"list_sponsorships_api_v1_admin_sponsorships_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/SponsorshipRow"},"type":"array","title":"Response List Sponsorships Api V1 Admin Sponsorships Get"}}}}}},"post":{"tags":["Admin"],"summary":"Create Sponsorship","description":"Approve a sponsor credit by hand. The name is the ONLY free text that\never reaches a video, so it's charset-limited (letters, digits, spaces,\nbasic punctuation — no URLs/handles) and short.","operationId":"create_sponsorship_api_v1_admin_sponsorships_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SponsorshipCreate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SponsorshipRow"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/sponsorships/{sponsorship_id}":{"delete":{"tags":["Admin"],"summary":"Delete Sponsorship","operationId":"delete_sponsorship_api_v1_admin_sponsorships__sponsorship_id__delete","parameters":[{"name":"sponsorship_id","in":"path","required":true,"schema":{"type":"integer","title":"Sponsorship Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Delete Sponsorship Api V1 Admin Sponsorships  Sponsorship Id  Delete"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/ux":{"get":{"tags":["Admin"],"summary":"Get Ux Summary","description":"Map UX read-back: persona choice rate, layer/scene use, share and play\ncounts, all from the anonymous ux_events beacon. The metrics the map plan\nis judged on (MAP_UX_PLAN.md §5).","operationId":"get_ux_summary_api_v1_admin_ux_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"default":30,"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UxSummary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/em/interest":{"post":{"tags":["Emergency Management"],"summary":"Submit Interest","description":"Record an EM early-access interest submission + notify the owner.","operationId":"submit_interest_api_v1_em_interest_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmInterestIn"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"boolean"},"type":"object","title":"Response Submit Interest Api V1 Em Interest Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/watches":{"get":{"tags":["Watches"],"summary":"Get Watches","description":"Get SPC watches (tornado and severe thunderstorm).\n\nReturns a GeoJSON FeatureCollection of watch box polygons with\nthreat parameters (max hail, max wind, PDS status).","operationId":"get_watches_api_v1_watches_get","parameters":[{"name":"bbox","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Bounding box: west,south,east,north","title":"Bbox"},"description":"Bounding box: west,south,east,north"},{"name":"point","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Center point: lat,lon","title":"Point"},"description":"Center point: lat,lon"},{"name":"radius","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Radius from point (e.g., 100km)","title":"Radius"},"description":"Radius from point (e.g., 100km)"},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"US state abbreviation","title":"State"},"description":"US state abbreviation"},{"name":"fips","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"5-digit county FIPS — only features intersecting that county's boundary","title":"Fips"},"description":"5-digit county FIPS — only features intersecting that county's boundary"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Max results","default":100,"title":"Limit"},"description":"Max results"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Pagination offset","default":0,"title":"Offset"},"description":"Pagination offset"},{"name":"active","in":"query","required":false,"schema":{"type":"boolean","description":"Only active (non-expired) watches","default":true,"title":"Active"},"description":"Only active (non-expired) watches"},{"name":"type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter: TOR|SVR","title":"Type"},"description":"Filter: TOR|SVR"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoJSONFeatureCollection"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/discussions":{"get":{"tags":["Mesoscale Discussions"],"summary":"Get Discussions","description":"Get SPC mesoscale discussions.\n\nReturns a GeoJSON FeatureCollection of MCD polygons with watch confidence,\nthreat parameters, and discussion text.","operationId":"get_discussions_api_v1_discussions_get","parameters":[{"name":"bbox","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Bounding box: west,south,east,north","title":"Bbox"},"description":"Bounding box: west,south,east,north"},{"name":"point","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Center point: lat,lon","title":"Point"},"description":"Center point: lat,lon"},{"name":"radius","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Radius from point (e.g., 100km)","title":"Radius"},"description":"Radius from point (e.g., 100km)"},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"US state abbreviation","title":"State"},"description":"US state abbreviation"},{"name":"fips","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"5-digit county FIPS — only features intersecting that county's boundary","title":"Fips"},"description":"5-digit county FIPS — only features intersecting that county's boundary"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Max results","default":50,"title":"Limit"},"description":"Max results"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Pagination offset","default":0,"title":"Offset"},"description":"Pagination offset"},{"name":"active","in":"query","required":false,"schema":{"type":"boolean","description":"Only active (non-expired) MCDs","default":true,"title":"Active"},"description":"Only active (non-expired) MCDs"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoJSONFeatureCollection"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/eros":{"get":{"tags":["WPC Excessive Rainfall Outlooks"],"summary":"Get Eros","description":"Get WPC Excessive Rainfall Outlooks.\n\nReturns a GeoJSON FeatureCollection of ERO polygons with risk levels,\nissue / valid times, and display colors.","operationId":"get_eros_api_v1_eros_get","parameters":[{"name":"bbox","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Bounding box: west,south,east,north","title":"Bbox"},"description":"Bounding box: west,south,east,north"},{"name":"point","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Center point: lat,lon","title":"Point"},"description":"Center point: lat,lon"},{"name":"radius","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Radius from point (e.g., 100km)","title":"Radius"},"description":"Radius from point (e.g., 100km)"},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"US state abbreviation","title":"State"},"description":"US state abbreviation"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Max results","default":100,"title":"Limit"},"description":"Max results"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Pagination offset","default":0,"title":"Offset"},"description":"Pagination offset"},{"name":"day","in":"query","required":false,"schema":{"type":"integer","maximum":3,"minimum":1,"description":"ERO day (1-3)","default":1,"title":"Day"},"description":"ERO day (1-3)"},{"name":"active","in":"query","required":false,"schema":{"type":"boolean","description":"Only outlooks whose valid period hasn't ended yet","default":true,"title":"Active"},"description":"Only outlooks whose valid period hasn't ended yet"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoJSONFeatureCollection"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tropical/outlook":{"get":{"tags":["Tropical (NHC)"],"summary":"Get Tropical Outlook","description":"NHC Graphical Tropical Weather Outlook genesis areas.\n\nReturns a GeoJSON FeatureCollection of formation-probability area\npolygons, plus a `metadata.basins` block with each basin's issuance\nheadline + time (present even in the off-season, when there are no\nareas — that's how the UI knows to show \"all quiet in the tropics\"\nrather than a broken/empty layer).","operationId":"get_tropical_outlook_api_v1_tropical_outlook_get","parameters":[{"name":"bbox","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Bounding box: west,south,east,north","title":"Bbox"},"description":"Bounding box: west,south,east,north"},{"name":"point","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Center point: lat,lon","title":"Point"},"description":"Center point: lat,lon"},{"name":"radius","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Radius from point (e.g., 100km)","title":"Radius"},"description":"Radius from point (e.g., 100km)"},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"US state abbreviation","title":"State"},"description":"US state abbreviation"},{"name":"basin","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to a basin (AL | EP | CP); omit for all","title":"Basin"},"description":"Filter to a basin (AL | EP | CP); omit for all"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoJSONFeatureCollection"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tropical/systems":{"get":{"tags":["Tropical (NHC)"],"summary":"List Tropical Systems","description":"Active tropical systems (strongest first), then those that ended in the\nlast 45 days (newest first), each with the address of its /tropics page.\nFeeds the Tropics section of /storms and the sitemap.","operationId":"list_tropical_systems_api_v1_tropical_systems_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/tropical/systems/{slug}":{"get":{"tags":["Tropical (NHC)"],"summary":"Get Tropical System","description":"One storm's page: live while it's active, an archive after. `slug` is\nany address the storm has had (\"eighteen-e-2026\" for what became Rachel);\n`slug` in the response is the current one, for the page to redirect to.","operationId":"get_tropical_system_api_v1_tropical_systems__slug__get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tropical/systems/{slug}/follow":{"post":{"tags":["Tropical (NHC)"],"summary":"Follow Tropical System","description":"Start following a storm by email. Sends a confirm link; nothing about the\nstorm is sent until it's clicked. The response never says whether the\naddress was already following.","operationId":"follow_tropical_system_api_v1_tropical_systems__slug__follow_post","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FollowRequest"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tropical/follows/{follow_id}/confirm":{"post":{"tags":["Tropical (NHC)"],"summary":"Confirm Tropical Follow","description":"The confirm link. Starts from what the storm is now: only a later change\nsends anything. Idempotent.","operationId":"confirm_tropical_follow_api_v1_tropical_follows__follow_id__confirm_post","parameters":[{"name":"follow_id","in":"path","required":true,"schema":{"type":"integer","title":"Follow Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FollowToken"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/wxagg__api__v1__tropical__FollowState"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tropical/follows/{follow_id}/stop":{"post":{"tags":["Tropical (NHC)"],"summary":"Stop Tropical Follow","description":"The stop link in every update email.","operationId":"stop_tropical_follow_api_v1_tropical_follows__follow_id__stop_post","parameters":[{"name":"follow_id","in":"path","required":true,"schema":{"type":"integer","title":"Follow Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FollowToken"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/wxagg__api__v1__tropical__FollowState"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tropical/storms":{"get":{"tags":["Tropical (NHC)"],"summary":"Get Tropical Storms","description":"Active NHC tropical systems.\n\nReturns a GeoJSON FeatureCollection where each system contributes\nup to three features, tagged `feature_kind` so the frontend can\nrender them as distinct layers:\n  - \"cone\"     — the forecast cone of uncertainty (polygon)\n  - \"track\"    — the forecast track line\n  - \"position\" — the current center (point), carrying the full\n                 intensity metadata in its properties\n\nEmpty in the off-season (no active systems).","operationId":"get_tropical_storms_api_v1_tropical_storms_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoJSONFeatureCollection"}}}}}}},"/api/v1/storms/{alert_id}/relative":{"get":{"tags":["Storms"],"summary":"Get Storm Relative","description":"Where this warning's storm is relative to a point, and when it could arrive.\n\nFor the Android storm screen: \"12 miles west of you, moving east at 35 mph,\ncould reach you in about 20 minutes.\" Everything comes from the storm\nposition and vector NWS codes into the warning (eventMotionDescription), the\nsame source the saved-location email uses for its ETA.\n\nThe point is rounded to ~1 km before use, the same precision as a device\nanchor. The answer is county-scale and the extra digits buy nothing.","operationId":"get_storm_relative_api_v1_storms__alert_id__relative_get","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"integer","title":"Alert Id"}},{"name":"lat","in":"query","required":true,"schema":{"type":"number","maximum":90,"minimum":-90,"title":"Lat"}},{"name":"lon","in":"query","required":true,"schema":{"type":"number","maximum":180,"minimum":-180,"title":"Lon"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StormRelative"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/storms/{alert_id}":{"get":{"tags":["Storms"],"summary":"Get Storm Cell","description":"Storm-cell view of an active warning.\n\nReturns a GeoJSON Feature with the warning polygon and properties\nenriched with the linked SPC watch, mesoscale discussion, and any\nstorm reports filed inside the polygon in the recent past.","operationId":"get_storm_cell_api_v1_storms__alert_id__get","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"integer","title":"Alert Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/storm-events":{"get":{"tags":["Storm Events"],"summary":"List Storm Events Endpoint","description":"Recent warned storms (one per VTEC) for the browse index — active first.","operationId":"list_storm_events_endpoint_api_v1_storm_events_get","parameters":[{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":720,"minimum":1,"default":168,"title":"Hours"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":300,"minimum":1,"default":100,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/StormEventSummary"},"title":"Response List Storm Events Endpoint Api V1 Storm Events Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/storm-events/{vtec}":{"get":{"tags":["Storm Events"],"summary":"Get Storm Event","description":"The storm-event object for a VTEC (e.g. `KILN.TO.W.0043`), or 404.","operationId":"get_storm_event_api_v1_storm_events__vtec__get","parameters":[{"name":"vtec","in":"path","required":true,"schema":{"type":"string","title":"Vtec"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StormEvent"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/storm-events/{vtec}/follow":{"post":{"tags":["Storm Events"],"summary":"Follow Storm","description":"Start following a storm by email. Sends a confirm link; nothing about\nthe storm is sent until it is clicked.","operationId":"follow_storm_api_v1_storm_events__vtec__follow_post","parameters":[{"name":"vtec","in":"path","required":true,"schema":{"type":"string","title":"Vtec"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FollowRequest"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FollowAccepted"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/storm-events/follows/{follow_id}/confirm":{"post":{"tags":["Storm Events"],"summary":"Confirm Follow","description":"The confirm link. Idempotent: a second click reports the same state.","operationId":"confirm_follow_api_v1_storm_events_follows__follow_id__confirm_post","parameters":[{"name":"follow_id","in":"path","required":true,"schema":{"type":"integer","title":"Follow Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FollowToken"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/wxagg__api__v1__storm_events__FollowState"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/storm-events/follows/{follow_id}/stop":{"post":{"tags":["Storm Events"],"summary":"Stop Follow","description":"The stop link in every update email.","operationId":"stop_follow_api_v1_storm_events_follows__follow_id__stop_post","parameters":[{"name":"follow_id","in":"path","required":true,"schema":{"type":"integer","title":"Follow Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FollowToken"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/wxagg__api__v1__storm_events__FollowState"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-warnings":{"post":{"tags":["Email warnings"],"summary":"Sign Up","description":"Ask for warnings at a place by email. Sends a confirm link.","operationId":"sign_up_api_v1_email_warnings_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/wxagg__api__v1__email_warnings__SignupRequest"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupAccepted"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-warnings/{warning_id}/confirm":{"post":{"tags":["Email warnings"],"summary":"Confirm","description":"The confirm link. Idempotent: a second click reports the same state.","operationId":"confirm_api_v1_email_warnings__warning_id__confirm_post","parameters":[{"name":"warning_id","in":"path","required":true,"schema":{"type":"integer","title":"Warning Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkToken"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WarningState"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/email-warnings/{warning_id}/stop":{"post":{"tags":["Email warnings"],"summary":"Stop","description":"The stop link in every email.","operationId":"stop_api_v1_email_warnings__warning_id__stop_post","parameters":[{"name":"warning_id","in":"path","required":true,"schema":{"type":"integer","title":"Warning Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkToken"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WarningState"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/live":{"get":{"tags":["Live channel"],"summary":"Get Live Status","operationId":"get_live_status_api_v1_live_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LiveStatus"}}}}}}},"/api/v1/live/highlights/{highlight_id}.mp4":{"get":{"tags":["Live channel"],"summary":"Live Highlight Clip","description":"A highlight clip cut from a broadcast (live/highlights.py) — the URL the\nYouTube publisher fetches when posting it as a Short.","operationId":"live_highlight_clip_api_v1_live_highlights__highlight_id__mp4_get","parameters":[{"name":"highlight_id","in":"path","required":true,"schema":{"type":"integer","title":"Highlight Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/postmortems/{vtec}/video.mp4":{"get":{"tags":["Post-mortems"],"summary":"Postmortem Video","operationId":"postmortem_video_api_v1_postmortems__vtec__video_mp4_get","parameters":[{"name":"vtec","in":"path","required":true,"schema":{"type":"string","title":"Vtec"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/postmortems/{vtec}/thumb.jpg":{"get":{"tags":["Post-mortems"],"summary":"Postmortem Thumb","operationId":"postmortem_thumb_api_v1_postmortems__vtec__thumb_jpg_get","parameters":[{"name":"vtec","in":"path","required":true,"schema":{"type":"string","title":"Vtec"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/explainers":{"get":{"tags":["Explainers"],"summary":"List Published","operationId":"list_published_api_v1_explainers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/PublishedExplainer"},"type":"array","title":"Response List Published Api V1 Explainers Get"}}}}}}},"/api/v1/explainers/{slug}/video.mp4":{"get":{"tags":["Explainers"],"summary":"Explainer Video","operationId":"explainer_video_api_v1_explainers__slug__video_mp4_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/explainers/{slug}/thumb.jpg":{"get":{"tags":["Explainers"],"summary":"Explainer Thumb","operationId":"explainer_thumb_api_v1_explainers__slug__thumb_jpg_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/recaps/monthly/{month}/data":{"get":{"tags":["Recaps"],"summary":"Monthly Recap Data","operationId":"monthly_recap_data_api_v1_recaps_monthly__month__data_get","parameters":[{"name":"month","in":"path","required":true,"schema":{"type":"string","title":"Month"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Monthly Recap Data Api V1 Recaps Monthly  Month  Data Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/recaps/monthly/{month}/video.mp4":{"get":{"tags":["Recaps"],"summary":"Monthly Recap Video","operationId":"monthly_recap_video_api_v1_recaps_monthly__month__video_mp4_get","parameters":[{"name":"month","in":"path","required":true,"schema":{"type":"string","title":"Month"}},{"name":"warm","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Warm"}},{"name":"seconds","in":"query","required":false,"schema":{"type":"number","maximum":600,"minimum":0,"default":60,"title":"Seconds"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/recaps/monthly/{month}/thumb.jpg":{"get":{"tags":["Recaps"],"summary":"Monthly Recap Thumb","operationId":"monthly_recap_thumb_api_v1_recaps_monthly__month__thumb_jpg_get","parameters":[{"name":"month","in":"path","required":true,"schema":{"type":"string","title":"Month"}},{"name":"warm","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Warm"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/meta/healthz":{"get":{"tags":["Meta"],"summary":"Get Healthz","description":"Liveness probe for external uptime monitoring (UptimeRobot / Better Stack).\n\nReturns 200 when the API can serve and the database is reachable, 503\notherwise — a clean status code an uptime monitor keys on directly. Cheap (one\n`SELECT 1`) and uncached, unlike /health, which is the rich, cached\ndata-freshness view for the status page and always returns 200 even when a\nsource is down. Rate-limit-exempt. DB is the hard dependency; Redis blips\nleave the app serving, so they're not a liveness failure here.\n\nHandles HEAD as well as GET — UptimeRobot (and many monitors) probe with HEAD\nby default, and a GET-only route 405s on HEAD, which reads as a false outage.","operationId":"get_healthz_api_v1_meta_healthz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"head":{"tags":["Meta"],"summary":"Get Healthz","description":"Liveness probe for external uptime monitoring (UptimeRobot / Better Stack).\n\nReturns 200 when the API can serve and the database is reachable, 503\notherwise — a clean status code an uptime monitor keys on directly. Cheap (one\n`SELECT 1`) and uncached, unlike /health, which is the rich, cached\ndata-freshness view for the status page and always returns 200 even when a\nsource is down. Rate-limit-exempt. DB is the hard dependency; Redis blips\nleave the app serving, so they're not a liveness failure here.\n\nHandles HEAD as well as GET — UptimeRobot (and many monitors) probe with HEAD\nby default, and a GET-only route 405s on HEAD, which reads as a false outage.","operationId":"get_healthz_api_v1_meta_healthz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/meta/readyz":{"get":{"tags":["Meta"],"summary":"Get Readyz","description":"Ingest-pipeline liveness for an external uptime monitor — the out-of-process\ndead-man's-switch. Returns 503 when the ingest heartbeat is stale, i.e. the\nscheduler has stopped writing it (crashed, hung, OOM-looping, or a deploy left\nit down). Unlike /healthz (API + DB liveness) this catches a *total ingest*\nfailure, which /healthz can't see because the API and DB stay up while the\npipeline silently stops producing data. Point UptimeRobot / Better Stack here.\n\nA never-written heartbeat (fresh DB, or ingest not yet booted) is also 503:\nthe pipeline isn't ready. Rate-limit-exempt; HEAD supported (monitors probe\nwith HEAD).","operationId":"get_readyz_api_v1_meta_readyz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"head":{"tags":["Meta"],"summary":"Get Readyz","description":"Ingest-pipeline liveness for an external uptime monitor — the out-of-process\ndead-man's-switch. Returns 503 when the ingest heartbeat is stale, i.e. the\nscheduler has stopped writing it (crashed, hung, OOM-looping, or a deploy left\nit down). Unlike /healthz (API + DB liveness) this catches a *total ingest*\nfailure, which /healthz can't see because the API and DB stay up while the\npipeline silently stops producing data. Point UptimeRobot / Better Stack here.\n\nA never-written heartbeat (fresh DB, or ingest not yet booted) is also 503:\nthe pipeline isn't ready. Rate-limit-exempt; HEAD supported (monitors probe\nwith HEAD).","operationId":"get_readyz_api_v1_meta_readyz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/meta/health":{"get":{"tags":["Meta"],"summary":"Get Health","description":"System health, per-source ingest status, and data freshness.\n\nPublic endpoint — safe to consume from a status page.","operationId":"get_health_api_v1_meta_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/meta/status":{"get":{"tags":["Meta"],"summary":"Get Public Status","description":"The public status page's view: capability groups, not ingestor names.\n\n`/health` is the full per-source picture and stays that way — it is what\nthe admin console and external monitors read. This is the same data curated\nfor a visitor (services/status_page.py): the social posters and the\nretention job are not shown, `hrrr_srh_03km` reads as \"HRRR storm-relative\nhelicity\", and the overall banner is rolled up from exactly the groups\nrendered beneath it.\n\nThat last part is the point. `/health` computes `overall` across all 74\nsources while the page listed 16, so on 2026-09-08 a visitor saw a red\n\"degraded\" banner above an all-green list with nothing to explain it.","operationId":"get_public_status_api_v1_meta_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/meta/media-kit":{"get":{"tags":["Meta"],"summary":"Media Kit","description":"Reach numbers for the media kit (/media): each platform's views over the\nlatest full 28 days and its follower count, from the channel analytics rows.\nA platform without a full window is null, and the page keeps its fallback.","operationId":"media_kit_api_v1_meta_media_kit_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/meta/sources":{"get":{"tags":["Meta"],"summary":"Get Sources","description":"List all available data sources and their capabilities.","operationId":"get_sources_api_v1_meta_sources_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcesResponse"}}}}}}},"/api/v1/afd":{"get":{"tags":["Area Forecast Discussion"],"summary":"Get Afd","description":"Latest AFD for a point (lat+lon) or a named office.\n\nReturns ``{office, afd}`` with ``afd`` null when nothing is stored for\nthat office yet — a clean empty state rather than a 404.","operationId":"get_afd_api_v1_afd_get","parameters":[{"name":"lat","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Lat"}},{"name":"lon","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Lon"}},{"name":"office","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"3- or 4-letter office code","title":"Office"},"description":"3- or 4-letter office code"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/hwo":{"get":{"tags":["Hazardous Weather Outlook"],"summary":"Get Hwo","description":"Latest Hazardous Weather Outlook for a point (lat+lon) or office.\n\nReturns ``{office, hwo}`` with ``hwo`` null when nothing is stored.","operationId":"get_hwo_api_v1_hwo_get","parameters":[{"name":"lat","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Lat"}},{"name":"lon","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Lon"}},{"name":"office","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"3- or 4-letter office code","title":"Office"},"description":"3- or 4-letter office code"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/share/image":{"get":{"tags":["Social Share"],"summary":"Share Image","operationId":"share_image_api_v1_share_image_get","parameters":[{"name":"lat","in":"query","required":true,"schema":{"type":"number","maximum":85,"minimum":-85,"title":"Lat"}},{"name":"lon","in":"query","required":true,"schema":{"type":"number","maximum":180,"minimum":-180,"title":"Lon"}},{"name":"span","in":"query","required":false,"schema":{"type":"number","maximum":12.0,"minimum":0.5,"default":3.5,"title":"Span"}},{"name":"palette","in":"query","required":false,"schema":{"type":"string","default":"nws","title":"Palette"}},{"name":"alert_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Alert Id"}},{"name":"card","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Card"}},{"name":"shape","in":"query","required":false,"schema":{"type":"string","default":"landscape","title":"Shape"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/share/loop.gif":{"get":{"tags":["Social Share"],"summary":"Share Loop","description":"Animated radar loop around a point, as a GIF — the moving version of\n/image. Same branded card + warned-polygon outline over the last `frames`\nreflectivity frames.","operationId":"share_loop_api_v1_share_loop_gif_get","parameters":[{"name":"lat","in":"query","required":true,"schema":{"type":"number","maximum":85,"minimum":-85,"title":"Lat"}},{"name":"lon","in":"query","required":true,"schema":{"type":"number","maximum":180,"minimum":-180,"title":"Lon"}},{"name":"span","in":"query","required":false,"schema":{"type":"number","maximum":12.0,"minimum":0.5,"default":3.5,"title":"Span"}},{"name":"palette","in":"query","required":false,"schema":{"type":"string","default":"nws","title":"Palette"}},{"name":"alert_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Alert Id"}},{"name":"card","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Card"}},{"name":"shape","in":"query","required":false,"schema":{"type":"string","default":"landscape","title":"Shape"}},{"name":"frames","in":"query","required":false,"schema":{"type":"integer","maximum":12,"minimum":2,"default":8,"title":"Frames"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/share/loop.mp4":{"get":{"tags":["Social Share"],"summary":"Share Loop Mp4","description":"Animated radar loop as an H.264 MP4 — the format Facebook needs for a real\nanimated (video) post; its /photos API flattens a GIF to one static frame.\nSame branded card + warned-polygon outline over the last `frames` frames.\n\n`audio=1` adds short TTS narration built from the alert (needs `alert_id`),\nrendering a loop long enough to cover it and muxing the voice on; if TTS is\noff/unavailable it silently serves the plain loop. `warm=1` renders + caches\nwithout streaming the bytes back (a 200 with no body) — the poster calls this\nonce so each platform's later fetch is a cache hit instead of a cold render.","operationId":"share_loop_mp4_api_v1_share_loop_mp4_get","parameters":[{"name":"lat","in":"query","required":true,"schema":{"type":"number","maximum":85,"minimum":-85,"title":"Lat"}},{"name":"lon","in":"query","required":true,"schema":{"type":"number","maximum":180,"minimum":-180,"title":"Lon"}},{"name":"span","in":"query","required":false,"schema":{"type":"number","maximum":12.0,"minimum":0.5,"default":3.5,"title":"Span"}},{"name":"palette","in":"query","required":false,"schema":{"type":"string","default":"nws","title":"Palette"}},{"name":"alert_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Alert Id"}},{"name":"card","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Card"}},{"name":"shape","in":"query","required":false,"schema":{"type":"string","default":"landscape","title":"Shape"}},{"name":"frames","in":"query","required":false,"schema":{"type":"integer","maximum":12,"minimum":2,"default":8,"title":"Frames"}},{"name":"audio","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Audio"}},{"name":"warm","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Warm"}},{"name":"seconds","in":"query","required":false,"schema":{"type":"number","maximum":90,"minimum":0,"default":0,"title":"Seconds"}},{"name":"hook","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Hook"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/share/satellite_loop.mp4":{"get":{"tags":["Social Share"],"summary":"Share Satellite Loop","description":"An animated GOES enhanced-IR loop (the colorized satellite view) around a\npoint, as an MP4 — for tropical Shorts/Reels. Same brand card as the radar\nloop, over the last ~hour of IR frames. GOES-East or GOES-West by coverage.\n\n`narrate=1` adds short TTS narration built from the tropical system\n(`system_id`) — name, category, winds, movement, any coastal watch/warning —\nrendering a loop long enough to cover it and muxing the voice on with timed\ncaptions; if TTS is off/unavailable it silently serves the plain loop.\n`since` is the advisory number of our last post about the storm: the voice\nthen says what changed since it (services.tropical_changes). `ai=1` asks\nfor the Claude-edited script (services.ai_scripts), honoured only while\nthat flag is on, and checked against the template's facts.\nNarration is derived server-side from the system (never free text) so the\npublic endpoint can't be used to synthesize arbitrary TTS.","operationId":"share_satellite_loop_api_v1_share_satellite_loop_mp4_get","parameters":[{"name":"lat","in":"query","required":true,"schema":{"type":"number","maximum":85,"minimum":-85,"title":"Lat"}},{"name":"lon","in":"query","required":true,"schema":{"type":"number","maximum":180,"minimum":-180,"title":"Lon"}},{"name":"span","in":"query","required":false,"schema":{"type":"number","maximum":30.0,"minimum":0.5,"default":6.0,"title":"Span"}},{"name":"shape","in":"query","required":false,"schema":{"type":"string","default":"shorts","title":"Shape"}},{"name":"seconds","in":"query","required":false,"schema":{"type":"number","maximum":60,"minimum":0,"default":0,"title":"Seconds"}},{"name":"card","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Card"}},{"name":"headline","in":"query","required":false,"schema":{"type":"string","maxLength":120,"default":"","title":"Headline"}},{"name":"location","in":"query","required":false,"schema":{"type":"string","maxLength":120,"default":"","title":"Location"}},{"name":"narrate","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Narrate"}},{"name":"system_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"System Id"}},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":6,"pattern":"^\\d{1,3}[A-Za-z]?$"},{"type":"null"}],"title":"Since"}},{"name":"ai","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Ai"}},{"name":"warm","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Warm"}},{"name":"style","in":"query","required":false,"schema":{"type":"string","pattern":"^(classic|map)$","default":"classic","title":"Style"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/share/outlook_video.mp4":{"get":{"tags":["Social Share"],"summary":"Share Outlook Video","description":"The daily severe-weather **outlook video** — a ≥60s narrated 9:16 clip\n(title → summary → threats → where → CTA) from the same `build_briefing`\nspine the /outlook page uses. This is the monetizing long-form-ish piece\n(TikTok Creator Rewards needs ≥60s); the poster warms it, then YouTube /\nTikTok / Facebook consume the one render. Cached per outlook day.\n\n`warm=1` renders + caches without streaming the bytes (the poster's\nwarm-once-fetch-thrice pattern). If TTS is unavailable the video is silent.","operationId":"share_outlook_video_api_v1_share_outlook_video_mp4_get","parameters":[{"name":"seconds","in":"query","required":false,"schema":{"type":"number","maximum":90,"minimum":3,"default":64,"title":"Seconds"}},{"name":"warm","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Warm"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/share/rating_video.mp4":{"get":{"tags":["Social Share"],"summary":"Share Rating Video","description":"The **EF-rating follow-up Short** for a rated tornado: a typographic EF\ncard (badge + survey stats) narrated + captioned. 404 until an NWS damage\nsurvey has actually rated the storm (`build_storm_event` status == \"rated\"\nwith an EF number). Cached per (vtec, ef); `warm=1` renders + caches without\nstreaming (the poster's warm-once-fetch-thrice pattern).","operationId":"share_rating_video_api_v1_share_rating_video_mp4_get","parameters":[{"name":"vtec","in":"query","required":true,"schema":{"type":"string","description":"storm VTEC, e.g. KILN.TO.W.0043","title":"Vtec"},"description":"storm VTEC, e.g. KILN.TO.W.0043"},{"name":"seconds","in":"query","required":false,"schema":{"type":"number","maximum":90,"minimum":0,"default":0,"title":"Seconds"}},{"name":"warm","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Warm"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/share/rating_card.jpg":{"get":{"tags":["Social Share"],"summary":"Share Rating Card","description":"The static EF card — the rating video's first frame as a JPEG — for a\nplatform that can't take the video (Bluesky's fallback when video upload is\nunavailable). Same 404 gate + per-(vtec, ef) cache as rating_video.mp4.","operationId":"share_rating_card_api_v1_share_rating_card_jpg_get","parameters":[{"name":"vtec","in":"query","required":true,"schema":{"type":"string","description":"storm VTEC, e.g. KILN.TO.W.0043","title":"Vtec"},"description":"storm VTEC, e.g. KILN.TO.W.0043"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/share/outlook_video_poster.jpg":{"get":{"tags":["Social Share"],"summary":"Share Outlook Video Poster","description":"A branded JPEG poster (the outlook video's title card) for the /outlook\npage's lazy video facade — so the box shows a real preview, not an empty\ngradient, with **zero external request** (self-hosted, can't 404, no YouTube\nShort-thumbnail letterboxing). Pure PIL (no ffmpeg); cached per outlook day.","operationId":"share_outlook_video_poster_api_v1_share_outlook_video_poster_jpg_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/share/briefing_thumb.jpg":{"get":{"tags":["Social Share"],"summary":"Share Briefing Thumb","description":"The 1280×720 YouTube thumbnail for the daily briefing video — a bold,\ncolor-coded risk word + region + date, readable in the browse feed. `d`/`mode`\ndistinguish the cached asset; the card always renders the current briefing.\nThe briefing poster warms this and sets it via YouTube `thumbnails.set`.","operationId":"share_briefing_thumb_api_v1_share_briefing_thumb_jpg_get","parameters":[{"name":"mode","in":"query","required":false,"schema":{"type":"string","default":"morning","title":"Mode"}},{"name":"d","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"D"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/share/outlook_photo.jpg":{"get":{"tags":["Social Share"],"summary":"Share Outlook Photo","description":"The 4:5 daily-outlook feed-photo card (badge + summary + hazards + heat)\nfor the native Facebook feed post. `d` (outlook date) just distinguishes the\ncached asset per day; the card always renders the current briefing.","operationId":"share_outlook_photo_api_v1_share_outlook_photo_jpg_get","parameters":[{"name":"d","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"D"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/share/earthquake_card.jpg":{"get":{"tags":["Social Share"],"summary":"Share Earthquake Card","description":"A branded 1:1 card for one significant earthquake — the image the\nearthquake poster attaches to its Facebook post. Cached per event.","operationId":"share_earthquake_card_api_v1_share_earthquake_card_jpg_get","parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"integer","description":"Earthquake row id","title":"Id"},"description":"Earthquake row id"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/share/declaration_card.jpg":{"get":{"tags":["Social Share"],"summary":"Share Declaration Card","description":"The declared counties on a map of their state, for the declaration\nposter's Facebook and Bluesky posts. Cached per declaration and county\ncount (an amendment that adds counties draws a new card).","operationId":"share_declaration_card_api_v1_share_declaration_card_jpg_get","parameters":[{"name":"declaration","in":"query","required":true,"schema":{"type":"string","pattern":"^[A-Z]{2}-\\d{3,5}-[A-Z]{2}$","title":"Declaration"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/share/zone_card.jpg":{"get":{"tags":["Social Share"],"summary":"Share Zone Card","description":"A warning issued by zone, without a polygon of its own (Red Flag, winter\nstorm, extreme heat...), drawn from its zones' shapes (ingest/zone_shapes.py):\nthe image the social poster attaches where a polygon warning gets radar.\n404 until the shapes are stored. Cached per alert.","operationId":"share_zone_card_api_v1_share_zone_card_jpg_get","parameters":[{"name":"alert_id","in":"query","required":true,"schema":{"type":"integer","title":"Alert Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/share/anniversary_card.jpg":{"get":{"tags":["Social Share"],"summary":"Share Anniversary Card","description":"A branded 1:1 'on this day' remembrance card — the image the anniversary\nposter attaches. `years_ago` is computed from the current (Central-time)\nyear, so the cache is keyed per year.\n\nResolves curated slugs first, then the SPC-backed ones\n(docs/SLOW_DAY_CONTENT.md Phase 2). Both render through the same card, which\nis the point of generated entries being `HistoricEvent`s.","operationId":"share_anniversary_card_api_v1_share_anniversary_card_jpg_get","parameters":[{"name":"slug","in":"query","required":true,"schema":{"type":"string","description":"Historic event slug","title":"Slug"},"description":"Historic event slug"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/share/tropical_card.jpg":{"get":{"tags":["Social Share"],"summary":"Share Tropical Card","description":"A branded 1:1 card for one NHC tropical system — the image the tropical\nposter attaches. Cached per (system, advisory), so a new advisory re-renders.","operationId":"share_tropical_card_api_v1_share_tropical_card_jpg_get","parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"integer","description":"Tropical system id","title":"Id"},"description":"Tropical system id"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/share/outlook_map.jpg":{"get":{"tags":["Social Share"],"summary":"Share Outlook Map","description":"The national SPC Day-1 categorical risk map (CONUS choropleth) as a JPEG —\nthe outlook \"hero\" for the /outlook page, OG cards, and the briefing video's\nopening scene. `d` (outlook date) just distinguishes the cached asset per day;\nit always renders the current outlook. Cached per (day, width) in MinIO.","operationId":"share_outlook_map_api_v1_share_outlook_map_jpg_get","parameters":[{"name":"d","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"D"}},{"name":"w","in":"query","required":false,"schema":{"type":"integer","maximum":2048,"minimum":480,"description":"Width px; height from CONUS aspect","default":1200,"title":"W"},"description":"Width px; height from CONUS aspect"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/share/briefing_video.mp4":{"get":{"tags":["Social Share"],"summary":"Share Briefing Video","description":"The combined **briefing video** — a narrated 9:16 clip pairing a storm\nrecap with the outlook. `mode=morning` recaps yesterday's storms then reads\ntoday's outlook (the monetizing morning piece); `mode=evening` recaps today\nso far then looks ahead through tonight. Both draw the recap from the same\npreliminary `storm_reports` that drive the Facebook confirmation posts (so it\nsays \"reported\", never \"confirmed\") and the outlook from `build_briefing`.\nCached per outlook day + mode.\n\n`warm=1` renders + caches without streaming the bytes (the poster's\nwarm-once-fetch-thrice pattern). If TTS is unavailable the video is silent.\n`ai=1`: the Claude-edited script (services.ai_scripts), only while that\nflag is on.","operationId":"share_briefing_video_api_v1_share_briefing_video_mp4_get","parameters":[{"name":"mode","in":"query","required":false,"schema":{"type":"string","pattern":"^(morning|evening)$","default":"morning","title":"Mode"}},{"name":"seconds","in":"query","required":false,"schema":{"type":"number","maximum":90,"minimum":3,"default":70,"title":"Seconds"}},{"name":"ai","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Ai"}},{"name":"warm","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Warm"}},{"name":"style","in":"query","required":false,"schema":{"type":"string","pattern":"^(classic|map)$","default":"classic","title":"Style"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/share/briefing_video_wide.mp4":{"get":{"tags":["Social Share"],"summary":"Share Briefing Video Wide","description":"The **long-form 16:9** morning briefing (YouTube) — an extended, landscape\ncut for watch-hours, run alongside the short 9:16 briefing. Richer content: a\ndeeper recap + a Day-2 'tomorrow's setup' beat. Cached per outlook day.","operationId":"share_briefing_video_wide_api_v1_share_briefing_video_wide_mp4_get","parameters":[{"name":"seconds","in":"query","required":false,"schema":{"type":"number","maximum":600,"minimum":3,"default":150,"title":"Seconds"}},{"name":"warm","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Warm"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/share/meta":{"get":{"tags":["Social Share"],"summary":"Share Meta","description":"Live conditions at a point for the share card's title/copy — checked at\nscrape time so it reflects reality (and never implies severe weather when\nthere isn't any). Returns {headline, kind}: kind=\"alert\" only for genuine\nwarnings/emergencies, else \"location\".","operationId":"share_meta_api_v1_share_meta_get","parameters":[{"name":"lat","in":"query","required":true,"schema":{"type":"number","maximum":90,"minimum":-90,"title":"Lat"}},{"name":"lon","in":"query","required":true,"schema":{"type":"number","maximum":180,"minimum":-180,"title":"Lon"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/share-events":{"post":{"tags":["Share Instrumentation"],"summary":"Record Share Event","description":"Record one share/visit event. Fire-and-forget from the client's view.","operationId":"record_share_event_api_v1_share_events_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareEventIn"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"boolean"},"type":"object","title":"Response Record Share Event Api V1 Share Events Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/referral-events":{"post":{"tags":["Share Instrumentation"],"summary":"Record Referral","description":"Record one funnel step. Fire-and-forget from the client's view.\n\nCrawlers that execute JavaScript fire this beacon exactly as a person's\nbrowser does — 42% of the arrivals recorded in the first 48 hours were\nautomated (see `services/bots.py`). They are dropped here rather than\nfiltered at read time so every consumer of the table, now and later, gets\nthe same honest denominator without having to remember to ask.","operationId":"record_referral_api_v1_referral_events_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReferralEventIn"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"boolean"},"type":"object","title":"Response Record Referral Api V1 Referral Events Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/ux-events":{"post":{"tags":["UX Instrumentation"],"summary":"Record Ux Events","description":"Record a batch of UX events for one session. Fire-and-forget from the\nclient's view — the response is only an ack with the row count.\n\nSame crawler filter as the referral beacon: this table is the other half of\nthe funnel join, so a bot counted here would be a bot counted there.","operationId":"record_ux_events_api_v1_ux_events_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UxBatchIn"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"integer"},"type":"object","title":"Response Record Ux Events Api V1 Ux Events Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tiktok/share/status":{"get":{"tags":["TikTok User Share"],"summary":"Share Status","description":"Whether the storm page shows the button: configured, and the viewer is an\nadmin (see _is_admin). Everyone else gets false.","operationId":"share_status_api_v1_tiktok_share_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"boolean"},"type":"object","title":"Response Share Status Api V1 Tiktok Share Status Get"}}}}}}},"/api/v1/tiktok/share/start":{"get":{"tags":["TikTok User Share"],"summary":"Share Start","description":"Begin the OAuth handoff for posting storm ``vtec`` to the user's TikTok.","operationId":"share_start_api_v1_tiktok_share_start_get","parameters":[{"name":"vtec","in":"query","required":true,"schema":{"type":"string","maxLength":64,"title":"Vtec"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tiktok/share/callback":{"get":{"tags":["TikTok User Share"],"summary":"Share Callback","description":"Exchange the code, query creator_info, and hand off to the confirm screen.\nNothing is posted here — the user confirms on the next screen.","operationId":"share_callback_api_v1_tiktok_share_callback_get","parameters":[{"name":"state","in":"query","required":false,"schema":{"type":"string","maxLength":128,"default":"","title":"State"}},{"name":"code","in":"query","required":false,"schema":{"type":"string","maxLength":2048,"default":"","title":"Code"}},{"name":"error","in":"query","required":false,"schema":{"type":"string","maxLength":256,"default":"","title":"Error"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tiktok/share/pending":{"get":{"tags":["TikTok User Share"],"summary":"Share Pending","description":"The confirm screen's data for a pending post — creator, privacy options,\nprefilled caption, and the video preview URL. NEVER returns the access token.","operationId":"share_pending_api_v1_tiktok_share_pending_get","parameters":[{"name":"sid","in":"query","required":true,"schema":{"type":"string","maxLength":128,"title":"Sid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tiktok/share/publish":{"post":{"tags":["TikTok User Share"],"summary":"Share Publish","description":"Direct Post the storm to the user's TikTok with their chosen settings, then\ndiscard the token. Returns the publish_id (or an error the UI can show).","operationId":"share_publish_api_v1_tiktok_share_publish_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishIn"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Share Publish Api V1 Tiktok Share Publish Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/facilities":{"get":{"tags":["Critical Facilities"],"summary":"List Facilities","description":"Facilities in a viewport (`bbox`) or a whole county (`fips`). The county\nform is what the Emergency Management persona uses: its jurisdiction's\nfacilities load regardless of zoom, so an EM zoomed out to the county never\nsees an empty map with a \"zoom in\" hint.","operationId":"list_facilities_api_v1_facilities_get","parameters":[{"name":"bbox","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"west,south,east,north (lon/lat degrees)","title":"Bbox"},"description":"west,south,east,north (lon/lat degrees)"},{"name":"fips","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"5-digit county FIPS — every facility inside that county (no bbox needed)","title":"Fips"},"description":"5-digit county FIPS — every facility inside that county (no bbox needed)"},{"name":"kind","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated facility kinds","title":"Kind"},"description":"Comma-separated facility kinds"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":3000,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/facilities/impact":{"get":{"tags":["Critical Facilities"],"summary":"Alert Impact","description":"Critical facilities a warning polygon covers — the EM impact summary.\n\nAnswers \"who & what is in the path?\" for a single alert: counts (exact, per\nkind) plus an enumerated list (capped) of the hospitals / nursing homes /\nmobile-home parks / schools / dialysis centers / correctional facilities inside\nthe warning polygon, via geography-native\n`ST_Covers(alert.polygon, facility.geom)`. Decision-support only — facility\ncoverage is incomplete for some kinds (see docs/VISION.md).\n\nReturns ``has_polygon: false`` (with empty counts) for zone-only alerts that\ncarry no polygon, rather than erroring — a clean \"can't compute\" state.","operationId":"alert_impact_api_v1_facilities_impact_get","parameters":[{"name":"alert_id","in":"query","required":true,"schema":{"type":"integer","description":"Alert whose polygon to test for coverage","title":"Alert Id"},"description":"Alert whose polygon to test for coverage"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/jurisdiction/search":{"get":{"tags":["EM Jurisdiction"],"summary":"Search Jurisdictions","description":"Find counties by (partial) name for the jurisdiction picker.","operationId":"search_jurisdictions_api_v1_jurisdiction_search_get","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":2,"description":"County name (partial)","title":"Q"},"description":"County name (partial)"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":20,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/jurisdiction/counties":{"get":{"tags":["EM Jurisdiction"],"summary":"List Counties","description":"Every county as {fips, state, slug, name} — powers the SEO weather-page\nsitemap and slug routing. Static-ish (boundaries rarely change); cache\nupstream. Territories without a USPS mapping are skipped.","operationId":"list_counties_api_v1_jurisdiction_counties_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/jurisdiction/boundary":{"get":{"tags":["EM Jurisdiction"],"summary":"Jurisdiction Boundary","description":"A county's boundary polygon as a GeoJSON Feature. Identify it by `fips`,\nor by `state` + `slug` (the SEO weather-page route).\n\nReturns the full, unsimplified MULTIPOLYGON — a developer clipping `/alerts`\n(or anything else) against a county needs the exact boundary, not a\ndisplay-simplified one. The data is static, so it's cached hard.","operationId":"jurisdiction_boundary_api_v1_jurisdiction_boundary_get","parameters":[{"name":"fips","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"5-digit county FIPS code","title":"Fips"},"description":"5-digit county FIPS code"},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"USPS state code (with slug)","title":"State"},"description":"USPS state code (with slug)"},{"name":"slug","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"County name slug (with state)","title":"Slug"},"description":"County name slug (with state)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/jurisdiction/summary":{"get":{"tags":["EM Jurisdiction"],"summary":"Jurisdiction Summary","description":"Active-warning + exposure roll-up for one county. Identify it by `fips`,\nor by `state` + `slug` (the SEO weather-page route). `forecast` adds the\ncached 3-day forecast the SEO page shows; the EM map leaves it off.","operationId":"jurisdiction_summary_api_v1_jurisdiction_summary_get","parameters":[{"name":"fips","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"5-digit county FIPS code","title":"Fips"},"description":"5-digit county FIPS code"},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"USPS state code (with slug)","title":"State"},"description":"USPS state code (with slug)"},{"name":"slug","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"County name slug (with state)","title":"Slug"},"description":"County name slug (with state)"},{"name":"forecast","in":"query","required":false,"schema":{"type":"boolean","description":"Include the 3-day forecast (SEO page)","default":false,"title":"Forecast"},"description":"Include the 3-day forecast (SEO page)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/jurisdiction/briefing":{"get":{"tags":["EM Jurisdiction"],"summary":"Jurisdiction Briefing","description":"The county command picture as a copy-able plain-text block — every active\nwarning with its expiry, people and critical facilities in the path, the\ncounty baseline, a live-map link, and the decision-support line. The\ncounty-level sibling of the per-warning briefing on /facilities/impact\n(MAP_UX_PLAN.md Phase 2.4).","operationId":"jurisdiction_briefing_api_v1_jurisdiction_briefing_get","parameters":[{"name":"fips","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"5-digit county FIPS code","title":"Fips"},"description":"5-digit county FIPS code"},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"USPS state code (with slug)","title":"State"},"description":"USPS state code (with slug)"},{"name":"slug","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"County name slug (with state)","title":"Slug"},"description":"County name slug (with state)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/jurisdiction/recent-damage":{"get":{"tags":["EM Jurisdiction"],"summary":"Recent Damage","description":"Post-event \"was it in the path?\" reconstruction for one county.\n\nReconstructs a damage swath from the storm reports (tornado / wind / hail)\nnear the county over the lookback window — each report buffered by\n``buffer_km`` (location imprecision + real damage width), unioned into a\nswath — and reports the critical facilities and population that swath\ncrossed *within the county*. The swath geometry is returned (full extent,\nfor map context) alongside county-clipped counts, mirroring the dashboard's\n\"what's at stake in my jurisdiction\" framing.\n\nCoverage is computed with index-friendly ``ST_DWithin`` against the reports\n(equivalent to the buffered-union swath, but lets PostGIS use the GiST\nindexes) rather than a polygon intersection. Decision-support only:\nreports are sparse ground-truth samples, the swath is an estimate of the\naffected area, and population is a census-tract-centroid count — not a\nsurvey of what was actually struck.\n\n404 if the county isn't found (boundaries not loaded). Requires storm\nreports (+ facilities / census tracts) loaded to return non-zero numbers.","operationId":"recent_damage_api_v1_jurisdiction_recent_damage_get","parameters":[{"name":"fips","in":"query","required":true,"schema":{"type":"string","description":"5-digit county FIPS code","title":"Fips"},"description":"5-digit county FIPS code"},{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":168,"minimum":1,"description":"Lookback window (hours)","default":24,"title":"Hours"},"description":"Lookback window (hours)"},{"name":"buffer_km","in":"query","required":false,"schema":{"type":"number","maximum":15.0,"minimum":0.25,"description":"Damage-swath half-width (km)","default":2.0,"title":"Buffer Km"},"description":"Damage-swath half-width (km)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/jurisdiction/rotation-swath":{"get":{"tags":["EM Jurisdiction"],"summary":"Rotation Swath","description":"Radar-ESTIMATED damage swath, reconstructed from MRMS low-level rotation\ndetections near the county over the lookback window — each strong-rotation\nsignal buffered by ``buffer_km`` and unioned into a swath — plus the critical\nfacilities and population that swath crossed *within the county*.\n\nComplements ``/recent-damage`` (which reconstructs from storm REPORTS): reports\nare sparse, delayed ground-truth samples, whereas rotation is radar-derived,\ndense, and available in real time — so this catches a rural track with no\nspotter reports, right after it happens rather than after a survey.\n\nDecision-support only, and the weakest-confidence of the three damage views:\nradar-inferred rotation is NOT confirmed damage (rotation aloft doesn't always\nreach the ground), the swath is an estimate, and population is a\ncensus-tract-centroid count. The authoritative path is the surveyed NWS DAT\ntrack (``/damage``); reports are the ground-truth samples (``/recent-damage``).\n404 if the county isn't found.","operationId":"rotation_swath_api_v1_jurisdiction_rotation_swath_get","parameters":[{"name":"fips","in":"query","required":true,"schema":{"type":"string","description":"County FIPS (5-digit)","title":"Fips"},"description":"County FIPS (5-digit)"},{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":72,"minimum":1,"description":"Lookback window (hours)","default":6,"title":"Hours"},"description":"Lookback window (hours)"},{"name":"buffer_km","in":"query","required":false,"schema":{"type":"number","maximum":15.0,"minimum":0.25,"description":"Swath half-width (km)","default":2.0,"title":"Buffer Km"},"description":"Swath half-width (km)"},{"name":"min_shear","in":"query","required":false,"schema":{"type":"number","minimum":0.0,"description":"Minimum peak azimuthal shear (0 = all stored detections)","default":0.0,"title":"Min Shear"},"description":"Minimum peak azimuthal shear (0 = all stored detections)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/briefing/point":{"get":{"tags":["Point Briefing"],"summary":"Point Briefing","description":"Structured briefing + a templated plain-language `text` for the point.","operationId":"point_briefing_api_v1_briefing_point_get","parameters":[{"name":"lat","in":"query","required":true,"schema":{"type":"number","maximum":90,"minimum":-90,"title":"Lat"}},{"name":"lon","in":"query","required":true,"schema":{"type":"number","maximum":180,"minimum":-180,"title":"Lon"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/briefing/timeline":{"get":{"tags":["Point Briefing"],"summary":"Point Timeline","description":"The next N hours at a point: the HRRR convective spine (CAPE/CIN/LI/wind-\ngust/precip) joined hourly to the SPC categorical outlook, WPC excessive-\nrainfall outlook, and any active alerts/watches covering the point — the\ninterpretation layer extended through time. Interpolated/aggregated risk is\ndecision-support only, not an official forecast.","operationId":"point_timeline_api_v1_briefing_timeline_get","parameters":[{"name":"lat","in":"query","required":true,"schema":{"type":"number","maximum":90,"minimum":-90,"title":"Lat"}},{"name":"lon","in":"query","required":true,"schema":{"type":"number","maximum":180,"minimum":-180,"title":"Lon"}},{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":48,"minimum":1,"description":"Forecast hours ahead","default":12,"title":"Hours"},"description":"Forecast hours ahead"},{"name":"hrrr_grids","in":"query","required":false,"schema":{"type":"boolean","description":"Also sample SRH / 0-6km shear / STP from our stored HRRR grids at each hour. Richer severe-weather signal, but slower (one grid read per hour per field), so it's opt-in.","default":false,"title":"Hrrr Grids"},"description":"Also sample SRH / 0-6km shear / STP from our stored HRRR grids at each hour. Richer severe-weather signal, but slower (one grid read per hour per field), so it's opt-in."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/briefing/history":{"get":{"tags":["Point Briefing"],"summary":"Point Tornado History","description":"Tornadoes recorded near a point, from the SPC archive since 1950.\n\nThe calm-Tuesday answer: on the ~95% of days with nothing to warn about, an\nalert-only app has nothing to say, and an app with nothing to say between\nstorms is uninstalled before the next one\n(docs/ANDROID_APP_PLAN.md Phase 4).\n\nDeliberately local, which is not a style preference. The slow-day\nmeasurement on 2026-09-13 found national ambient content produced 2 arrivals\nfrom 177 posts while every arrival that converted landed on a page about one\nspecific place.\n\n`passed_within` distinguishes two different claims. Rows carrying both ends\nof the track support \"a tornado passed within N miles\"; rows that predate the\nloader reading end coordinates only support \"began within\", and saying the\nstronger thing on the weaker data would be wrong in the direction that\nflatters us. Re-running `load_tornado_history` turns the second into the\nfirst.","operationId":"point_tornado_history_api_v1_briefing_history_get","parameters":[{"name":"lat","in":"query","required":true,"schema":{"type":"number","maximum":90,"minimum":-90,"title":"Lat"}},{"name":"lon","in":"query","required":true,"schema":{"type":"number","maximum":180,"minimum":-180,"title":"Lon"}},{"name":"radius_mi","in":"query","required":false,"schema":{"type":"number","maximum":100,"exclusiveMinimum":0,"default":10.0,"title":"Radius Mi"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/forecast/point":{"get":{"tags":["NWS Forecast"],"summary":"Nws Point Forecast","description":"The official NWS gridpoint hourly forecast for a point.","operationId":"nws_point_forecast_api_v1_forecast_point_get","parameters":[{"name":"lat","in":"query","required":true,"schema":{"type":"number","maximum":90,"minimum":-90,"title":"Lat"}},{"name":"lon","in":"query","required":true,"schema":{"type":"number","maximum":180,"minimum":-180,"title":"Lon"}},{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":156,"minimum":1,"description":"Forecast hours to return","default":24,"title":"Hours"},"description":"Forecast hours to return"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/assistant/status":{"get":{"tags":["AI Assistant"],"summary":"Status","description":"Whether the assistant is live (enabled AND keyed) — lets the UI show or\nhide the Ask box in sync with the backend flag, no frontend rebuild. Reveals\na single boolean, so no auth.","operationId":"status_api_v1_assistant_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/assistant/ask":{"post":{"tags":["AI Assistant"],"summary":"Ask","operationId":"ask_api_v1_assistant_ask_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AskRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/places":{"get":{"tags":["Places (SEO city pages)"],"summary":"List Places","description":"Every place as {state, slug, name} — powers the city sitemap + slug\nrouting. Static-ish; cache upstream.","operationId":"list_places_api_v1_places_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/places/nearby":{"get":{"tags":["Places (SEO city pages)"],"summary":"Nearby Places","description":"Nearest cities to a point, for the \"nearby areas\" cross-links on the SEO\nweather pages — turns each leaf page into a crawlable web instead of a\ndead end, and gives readers a real reason to click deeper. A ~1.5deg box\n(~100 mi) prefilters before the exact great-circle sort so the GiST-less\nlat/lon columns stay cheap. Excludes the current place so it never links to\nitself (a county page passes no exclude, which matches nothing — fine).","operationId":"nearby_places_api_v1_places_nearby_get","parameters":[{"name":"lat","in":"query","required":true,"schema":{"type":"number","maximum":90,"minimum":-90,"title":"Lat"}},{"name":"lon","in":"query","required":true,"schema":{"type":"number","maximum":180,"minimum":-180,"title":"Lon"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":20,"minimum":1,"default":8,"title":"Limit"}},{"name":"exclude_state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"USPS code of the current place","title":"Exclude State"},"description":"USPS code of the current place"},{"name":"exclude_slug","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"slug of the current place","title":"Exclude Slug"},"description":"slug of the current place"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/places/summary":{"get":{"tags":["Places (SEO city pages)"],"summary":"Place Summary","description":"A city + the active warnings covering it. 404 if the slug isn't a known\nplace (the frontend then falls back to trying it as a county).","operationId":"place_summary_api_v1_places_summary_get","parameters":[{"name":"state","in":"query","required":true,"schema":{"type":"string","description":"USPS state code","title":"State"},"description":"USPS state code"},{"name":"slug","in":"query","required":true,"schema":{"type":"string","description":"City slug","title":"Slug"},"description":"City slug"},{"name":"forecast","in":"query","required":false,"schema":{"type":"boolean","description":"Include the 3-day forecast (SEO page)","default":false,"title":"Forecast"},"description":"Include the 3-day forecast (SEO page)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/damage-tracks":{"get":{"tags":["Damage Tracks (NWS DAT)"],"summary":"List Damage Tracks","description":"Recent surveyed damage tracks as a GeoJSON FeatureCollection.\n\nGeometry is returned in lon/lat (WGS84); lengths/widths are SI (meters) per\nthe project convention — the client converts to miles/yards for display.","operationId":"list_damage_tracks_api_v1_damage_tracks_get","parameters":[{"name":"bbox","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"west,south,east,north (lon/lat degrees)","title":"Bbox"},"description":"west,south,east,north (lon/lat degrees)"},{"name":"fips","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"County FIPS — tracks intersecting it","title":"Fips"},"description":"County FIPS — tracks intersecting it"},{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"description":"Lookback window by storm date","default":60,"title":"Days"},"description":"Lookback window by storm date"},{"name":"min_ef","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":5,"minimum":0},{"type":"null"}],"description":"Minimum EF number","title":"Min Ef"},"description":"Minimum EF number"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1000,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoJSONFeatureCollection"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/cwa":{"get":{"tags":["NWS Office Boundaries (CWA)"],"summary":"List Cwa Boundaries","description":"Every NWS office jurisdiction as a GeoJSON FeatureCollection.\n\nGeometry is lon/lat (WGS84). Properties carry the office code and a\nready-made label (city/state) for popups and boundary labels.","operationId":"list_cwa_boundaries_api_v1_cwa_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoJSONFeatureCollection"}}}}}}},"/api/v1/stream":{"get":{"tags":["Live Stream (SSE)"],"summary":"Stream","description":"SSE stream of ingest-change pings for the curated channels.","operationId":"stream_api_v1_stream_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"APIKeyCreatedResponse":{"properties":{"id":{"type":"integer","title":"Id"},"name":{"type":"string","title":"Name"},"prefix":{"type":"string","title":"Prefix"},"rate_limit":{"type":"integer","title":"Rate Limit"},"is_active":{"type":"boolean","title":"Is Active"},"created_at":{"type":"string","title":"Created At"},"last_used_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Used At"},"key":{"type":"string","title":"Key"}},"type":"object","required":["id","name","prefix","rate_limit","is_active","created_at","last_used_at","key"],"title":"APIKeyCreatedResponse"},"APIKeyResponse":{"properties":{"id":{"type":"integer","title":"Id"},"name":{"type":"string","title":"Name"},"prefix":{"type":"string","title":"Prefix"},"rate_limit":{"type":"integer","title":"Rate Limit"},"is_active":{"type":"boolean","title":"Is Active"},"created_at":{"type":"string","title":"Created At"},"last_used_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Used At"}},"type":"object","required":["id","name","prefix","rate_limit","is_active","created_at","last_used_at"],"title":"APIKeyResponse"},"AdminEmail":{"properties":{"by_kind":{"items":{"$ref":"#/components/schemas/EmailKindRow"},"type":"array","title":"By Kind"},"recent_failures":{"items":{"$ref":"#/components/schemas/EmailFailureRow"},"type":"array","title":"Recent Failures"},"transactional_logged":{"type":"boolean","title":"Transactional Logged","default":false}},"type":"object","required":["by_kind","recent_failures"],"title":"AdminEmail"},"AdminSocial":{"properties":{"connections":{"items":{"$ref":"#/components/schemas/SocialConnection"},"type":"array","title":"Connections"},"content_flags":{"items":{"$ref":"#/components/schemas/ContentFlagRow"},"type":"array","title":"Content Flags"},"settings":{"items":{"$ref":"#/components/schemas/SettingRow"},"type":"array","title":"Settings"},"ledger":{"items":{"$ref":"#/components/schemas/SocialLedgerRow"},"type":"array","title":"Ledger"},"recent":{"items":{"$ref":"#/components/schemas/SocialPostRow"},"type":"array","title":"Recent"},"daily_ledger":{"items":{"$ref":"#/components/schemas/DailyContentLedgerRow"},"type":"array","title":"Daily Ledger"},"daily_recent":{"items":{"$ref":"#/components/schemas/DailyContentPostRow"},"type":"array","title":"Daily Recent"},"engagement":{"items":{"$ref":"#/components/schemas/EngagementRow"},"type":"array","title":"Engagement"},"briefing_performance":{"$ref":"#/components/schemas/BriefingPerformance"},"share_performance":{"$ref":"#/components/schemas/SharePerformance"},"video_gates":{"$ref":"#/components/schemas/VideoGates"},"live_broadcasts":{"items":{"$ref":"#/components/schemas/LiveBroadcastRow"},"type":"array","title":"Live Broadcasts"},"video_ab":{"$ref":"#/components/schemas/VideoAB"},"tokens":{"items":{"$ref":"#/components/schemas/SocialTokenRow"},"type":"array","title":"Tokens"}},"type":"object","required":["connections","content_flags","settings","ledger","recent","daily_ledger","daily_recent","engagement","briefing_performance","share_performance","video_gates","live_broadcasts","video_ab","tokens"],"title":"AdminSocial"},"AdminStats":{"properties":{"users_total":{"type":"integer","title":"Users Total"},"users_active":{"type":"integer","title":"Users Active"},"users_verified":{"type":"integer","title":"Users Verified"},"users_admin":{"type":"integer","title":"Users Admin"},"signups_last_7d":{"type":"integer","title":"Signups Last 7D"},"signups_last_30d":{"type":"integer","title":"Signups Last 30D"},"saved_locations":{"type":"integer","title":"Saved Locations"},"api_keys_active":{"type":"integer","title":"Api Keys Active"},"alerts_active":{"type":"integer","title":"Alerts Active"},"alerts_total":{"type":"integer","title":"Alerts Total"},"reports_24h":{"type":"integer","title":"Reports 24H"},"observations_total":{"type":"integer","title":"Observations Total"},"push_sent_7d":{"type":"integer","title":"Push Sent 7D"},"push_acked_7d":{"type":"integer","title":"Push Acked 7D"},"devices_live":{"type":"integer","title":"Devices Live"}},"type":"object","required":["users_total","users_active","users_verified","users_admin","signups_last_7d","signups_last_30d","saved_locations","api_keys_active","alerts_active","alerts_total","reports_24h","observations_total","push_sent_7d","push_acked_7d","devices_live"],"title":"AdminStats"},"AdminUserListResponse":{"properties":{"total":{"type":"integer","title":"Total"},"rows":{"items":{"$ref":"#/components/schemas/AdminUserRow"},"type":"array","title":"Rows"}},"type":"object","required":["total","rows"],"title":"AdminUserListResponse"},"AdminUserRow":{"properties":{"id":{"type":"integer","title":"Id"},"email":{"type":"string","title":"Email"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"role":{"type":"string","title":"Role"},"is_active":{"type":"boolean","title":"Is Active"},"email_verified":{"type":"boolean","title":"Email Verified"},"saved_locations":{"type":"integer","title":"Saved Locations"},"api_keys":{"type":"integer","title":"Api Keys"},"created_at":{"type":"string","title":"Created At"},"last_login_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Login At"}},"type":"object","required":["id","email","name","role","is_active","email_verified","saved_locations","api_keys","created_at","last_login_at"],"title":"AdminUserRow"},"AnonDeviceLocationRequest":{"properties":{"endpoint":{"type":"string","maxLength":2048,"minLength":1,"title":"Endpoint"},"lat":{"type":"number","maximum":90.0,"minimum":-90.0,"title":"Lat"},"lon":{"type":"number","maximum":180.0,"minimum":-180.0,"title":"Lon"}},"type":"object","required":["endpoint","lat","lon"],"title":"AnonDeviceLocationRequest"},"AnonSubscribeRequest":{"properties":{"endpoint":{"type":"string","maxLength":2048,"minLength":1,"title":"Endpoint"},"p256dh":{"type":"string","maxLength":255,"minLength":1,"title":"P256Dh"},"auth":{"type":"string","maxLength":255,"minLength":1,"title":"Auth"},"user_agent":{"anyOf":[{"type":"string","maxLength":512},{"type":"null"}],"title":"User Agent"}},"type":"object","required":["endpoint","p256dh","auth"],"title":"AnonSubscribeRequest"},"AnonUnsubscribeRequest":{"properties":{"endpoint":{"type":"string","maxLength":2048,"minLength":1,"title":"Endpoint"}},"type":"object","required":["endpoint"],"title":"AnonUnsubscribeRequest"},"AskRequest":{"properties":{"question":{"type":"string","maxLength":2000,"minLength":1,"title":"Question"},"lat":{"type":"number","maximum":90.0,"minimum":-90.0,"title":"Lat"},"lon":{"type":"number","maximum":180.0,"minimum":-180.0,"title":"Lon"},"mode":{"type":"string","title":"Mode","default":"general"}},"type":"object","required":["question","lat","lon"],"title":"AskRequest"},"BriefingEpisode":{"properties":{"content_date":{"type":"string","title":"Content Date"},"kind":{"type":"string","title":"Kind"},"views":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Views"},"avg_view_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Avg View Seconds"},"watch_hours":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Watch Hours"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"}},"type":"object","required":["content_date","kind","views","avg_view_seconds","watch_hours","url"],"title":"BriefingEpisode","description":"One daily YouTube briefing episode's read-back — the discovery-work signal:\nis views/episode climbing, and are watch-hours compounding toward the 4,000-\nhour monetization gate? Metrics null until YouTube Analytics lands (~2-3 days).\n(Impressions-CTR would be the ideal packaging metric but the API doesn't\nexpose it — that's the weekly YouTube Studio glance.)"},"BriefingPerformance":{"properties":{"episodes":{"items":{"$ref":"#/components/schemas/BriefingEpisode"},"type":"array","title":"Episodes"},"watch_hours_28d":{"type":"number","title":"Watch Hours 28D"},"avg_views_28d":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg Views 28D"},"episode_count_28d":{"type":"integer","title":"Episode Count 28D"}},"type":"object","required":["episodes","watch_hours_28d","avg_views_28d","episode_count_28d"],"title":"BriefingPerformance","description":"Recent daily-briefing episodes + rolling 28-day aggregates. Target (see\nYOUTUBE_LONGFORM_PLAN.md §4): views/episode >100, watch-hours bending up."},"CompositeAlertHistoryItem":{"properties":{"id":{"type":"integer","title":"Id"},"saved_location_id":{"type":"integer","title":"Saved Location Id"},"location_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Location Name"},"index_name":{"type":"string","title":"Index Name"},"peak_value":{"type":"number","title":"Peak Value"},"threshold":{"type":"number","title":"Threshold"},"hour_offset":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Hour Offset"},"device_count":{"type":"integer","title":"Device Count"},"delivered_at":{"type":"string","title":"Delivered At"}},"type":"object","required":["id","saved_location_id","location_name","index_name","peak_value","threshold","hour_offset","device_count","delivered_at"],"title":"CompositeAlertHistoryItem"},"ConeAlertHistoryItem":{"properties":{"id":{"type":"integer","title":"Id"},"saved_location_id":{"type":"integer","title":"Saved Location Id"},"location_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Location Name"},"atcf_id":{"type":"string","title":"Atcf Id"},"storm_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Storm Name"},"classification":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Classification"},"max_wind_kt":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Wind Kt"},"advisory_num":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Advisory Num"},"covered":{"type":"boolean","title":"Covered"},"delivered_at":{"type":"string","title":"Delivered At"},"cleared_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cleared At"}},"type":"object","required":["id","saved_location_id","location_name","atcf_id","storm_name","classification","max_wind_kt","advisory_num","covered","delivered_at","cleared_at"],"title":"ConeAlertHistoryItem"},"ContentFlagRow":{"properties":{"flag":{"type":"string","title":"Flag"},"label":{"type":"string","title":"Label"},"group":{"type":"string","title":"Group"},"enabled":{"type":"boolean","title":"Enabled"},"env_default":{"type":"boolean","title":"Env Default"},"overridden":{"type":"boolean","title":"Overridden"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"},"updated_by":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Updated By"}},"type":"object","required":["flag","label","group","enabled","env_default","overridden"],"title":"ContentFlagRow","description":"A content-type gate + its effective state. `enabled` is what's actually in\nforce (runtime override if set, else the env default). `overridden` says the\nvalue comes from a UI toggle rather than env; `env_default` is the baseline a\nclear() would revert to."},"CreateAPIKeyRequest":{"properties":{"name":{"type":"string","maxLength":128,"minLength":1,"title":"Name"}},"type":"object","required":["name"],"title":"CreateAPIKeyRequest"},"CreateLocationRequest":{"properties":{"name":{"type":"string","maxLength":128,"minLength":1,"title":"Name"},"lat":{"type":"number","maximum":90.0,"minimum":-90.0,"title":"Lat"},"lon":{"type":"number","maximum":180.0,"minimum":-180.0,"title":"Lon"},"label":{"anyOf":[{"type":"string","maxLength":256},{"type":"null"}],"title":"Label"},"alerts_enabled":{"type":"boolean","title":"Alerts Enabled","default":true},"min_severity":{"type":"string","title":"Min Severity","default":"Severe"},"show_impact":{"type":"boolean","title":"Show Impact","default":false},"nearby_alerts":{"type":"boolean","title":"Nearby Alerts","default":true},"watch_alerts":{"type":"boolean","title":"Watch Alerts","default":true},"scp_alert_threshold":{"anyOf":[{"type":"number","maximum":100.0,"minimum":0.0},{"type":"null"}],"title":"Scp Alert Threshold"},"ship_alert_threshold":{"anyOf":[{"type":"number","maximum":100.0,"minimum":0.0},{"type":"null"}],"title":"Ship Alert Threshold"}},"type":"object","required":["name","lat","lon"],"title":"CreateLocationRequest"},"CreateWebhookRequest":{"properties":{"url":{"type":"string","maxLength":2048,"title":"Url"},"bbox":{"anyOf":[{"items":{"type":"number"},"type":"array"},{"type":"null"}],"title":"Bbox","description":"[west, south, east, north]"},"geometry":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Geometry","description":"GeoJSON Polygon/MultiPolygon"},"event_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Event Types"},"min_severity":{"type":"string","title":"Min Severity","default":"Severe"},"description":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Description"}},"type":"object","required":["url"],"title":"CreateWebhookRequest"},"CtaRow":{"properties":{"detail":{"type":"string","title":"Detail"},"sessions":{"type":"integer","title":"Sessions"}},"type":"object","required":["detail","sessions"],"title":"CtaRow","description":"One landing-page action, and how many sessions took it."},"DailyContentLedgerRow":{"properties":{"kind":{"type":"string","title":"Kind"},"platform":{"type":"string","title":"Platform"},"sent":{"type":"integer","title":"Sent"},"failed":{"type":"integer","title":"Failed"},"last_success_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Success At"},"last_failure_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Failure At"},"publish_confirmed":{"type":"integer","title":"Publish Confirmed","default":0},"publish_failed":{"type":"integer","title":"Publish Failed","default":0},"publish_pending":{"type":"integer","title":"Publish Pending","default":0}},"type":"object","required":["kind","platform","sent","failed","last_success_at","last_failure_at"],"title":"DailyContentLedgerRow","description":"Send/fail tally for scheduled daily content, per kind+platform, plus the\npost-ship verification the publish reconciler writes (did the upload actually\nland publicly — same publish_status vocabulary as the storm-feed ledger)."},"DailyContentPostRow":{"properties":{"id":{"type":"integer","title":"Id"},"content_date":{"type":"string","title":"Content Date"},"kind":{"type":"string","title":"Kind"},"platform":{"type":"string","title":"Platform"},"status":{"type":"string","title":"Status"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"platform_post_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Platform Post Id"},"posted_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Posted At"},"publish_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Publish Status"}},"type":"object","required":["id","content_date","kind","platform","status","error","platform_post_id","posted_at","publish_status"],"title":"DailyContentPostRow"},"DataSource":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"description":{"type":"string","title":"Description"},"endpoints":{"items":{"type":"string"},"type":"array","title":"Endpoints"},"update_interval_seconds":{"type":"integer","title":"Update Interval Seconds"},"phase":{"type":"integer","title":"Phase"},"status":{"type":"string","title":"Status"}},"type":"object","required":["id","name","description","endpoints","update_interval_seconds","phase","status"],"title":"DataSource","description":"One entry in the /meta/sources catalog."},"DeviceLocationRequest":{"properties":{"endpoint":{"type":"string","maxLength":2048,"minLength":1,"title":"Endpoint"},"lat":{"type":"number","maximum":90.0,"minimum":-90.0,"title":"Lat"},"lon":{"type":"number","maximum":180.0,"minimum":-180.0,"title":"Lon"}},"type":"object","required":["endpoint","lat","lon"],"title":"DeviceLocationRequest"},"DeviceLocationState":{"properties":{"enabled":{"type":"boolean","title":"Enabled"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"}},"type":"object","required":["enabled","updated_at"],"title":"DeviceLocationState"},"DeviceLocationStateResponse":{"properties":{"enabled":{"type":"boolean","title":"Enabled"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"}},"type":"object","required":["enabled","updated_at"],"title":"DeviceLocationStateResponse"},"DeviceMessageOut":{"properties":{"kind":{"type":"string","title":"Kind"},"level":{"type":"string","title":"Level"},"title":{"type":"string","title":"Title"},"body":{"type":"string","title":"Body"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"},"alert_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Alert Id"},"replaces_msg_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Replaces Msg Id"}},"type":"object","required":["kind","level","title","body"],"title":"DeviceMessageOut","description":"An approach alert or all-clear's text (models/device_message.py). The push\ncarried only its kind and this message's id."},"DevicePlacePoint":{"properties":{"lat":{"type":"number","maximum":90.0,"minimum":-90.0,"title":"Lat"},"lon":{"type":"number","maximum":180.0,"minimum":-180.0,"title":"Lon"}},"type":"object","required":["lat","lon"],"title":"DevicePlacePoint"},"DevicePlacesRequest":{"properties":{"token":{"type":"string","maxLength":4096,"minLength":16,"title":"Token"},"places":{"items":{"$ref":"#/components/schemas/DevicePlacePoint"},"type":"array","maxItems":5,"title":"Places"}},"type":"object","required":["token"],"title":"DevicePlacesRequest"},"DevicePlacesState":{"properties":{"count":{"type":"integer","title":"Count"}},"type":"object","required":["count"],"title":"DevicePlacesState"},"DeviceTokenAckRequest":{"properties":{"token":{"type":"string","maxLength":4096,"minLength":16,"title":"Token"},"msg_id":{"type":"string","maxLength":36,"minLength":8,"title":"Msg Id"}},"type":"object","required":["token","msg_id"],"title":"DeviceTokenAckRequest"},"DeviceTokenHeartbeatRequest":{"properties":{"token":{"type":"string","maxLength":4096,"minLength":16,"title":"Token"},"stationary":{"type":"boolean","title":"Stationary","default":false}},"type":"object","required":["token"],"title":"DeviceTokenHeartbeatRequest"},"DeviceTokenLocationRequest":{"properties":{"token":{"type":"string","maxLength":4096,"minLength":16,"title":"Token"},"lat":{"type":"number","maximum":90.0,"minimum":-90.0,"title":"Lat"},"lon":{"type":"number","maximum":180.0,"minimum":-180.0,"title":"Lon"}},"type":"object","required":["token","lat","lon"],"title":"DeviceTokenLocationRequest"},"DeviceTokenRequest":{"properties":{"token":{"type":"string","maxLength":4096,"minLength":16,"title":"Token"},"platform":{"type":"string","pattern":"^(android|ios)$","title":"Platform","default":"android"},"app_version":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"App Version"}},"type":"object","required":["token"],"title":"DeviceTokenRequest"},"DiscussLink":{"properties":{"platform":{"type":"string","title":"Platform"},"url":{"type":"string","title":"Url"}},"type":"object","required":["platform","url"],"title":"DiscussLink"},"EmInterestIn":{"properties":{"agency":{"type":"string","maxLength":200,"minLength":1,"title":"Agency"},"name":{"type":"string","maxLength":200,"minLength":1,"title":"Name"},"email":{"type":"string","maxLength":320,"minLength":3,"title":"Email"},"jurisdiction":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Jurisdiction"},"state":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"State"},"role":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Role"},"needs":{"items":{"type":"string"},"type":"array","maxItems":12,"title":"Needs"},"current_process":{"anyOf":[{"type":"string","maxLength":4000},{"type":"null"}],"title":"Current Process"},"message":{"anyOf":[{"type":"string","maxLength":4000},{"type":"null"}],"title":"Message"},"source":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Source"}},"type":"object","required":["agency","name","email"],"title":"EmInterestIn"},"EmInterestRow":{"properties":{"id":{"type":"integer","title":"Id"},"agency":{"type":"string","title":"Agency"},"name":{"type":"string","title":"Name"},"email":{"type":"string","title":"Email"},"jurisdiction":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Jurisdiction"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"role":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Role"},"needs":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Needs"},"current_process":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Process"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"}},"type":"object","required":["id","agency","name","email","jurisdiction","state","role","needs","current_process","message","source","created_at"],"title":"EmInterestRow"},"EmailFailureRow":{"properties":{"kind":{"type":"string","title":"Kind"},"user_id":{"type":"integer","title":"User Id"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"sent_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sent At"}},"type":"object","required":["kind","user_id","error","sent_at"],"title":"EmailFailureRow"},"EmailKindRow":{"properties":{"kind":{"type":"string","title":"Kind"},"sent":{"type":"integer","title":"Sent"},"failed":{"type":"integer","title":"Failed"},"last_sent_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Sent At"}},"type":"object","required":["kind","sent","failed","last_sent_at"],"title":"EmailKindRow"},"EngagementRow":{"properties":{"platform":{"type":"string","title":"Platform"},"platform_post_id":{"type":"string","title":"Platform Post Id"},"kind":{"type":"string","title":"Kind"},"event":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Event"},"views":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Views"},"likes":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Likes"},"comments":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Comments"},"shares":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Shares"},"avg_view_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Avg View Seconds"},"est_minutes_watched":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Est Minutes Watched"},"fetched_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fetched At"}},"type":"object","required":["platform","platform_post_id","kind","event","views","likes","comments","shares","avg_view_seconds","est_minutes_watched","fetched_at"],"title":"EngagementRow","description":"One post's read-back engagement (from social_post_stats), labeled with\nwhich ledger it came from. `kind` is the daily-content kind (outlook_video,\n…) or \"warning\" for the storm feed; `event` is the warning's event name when\nit's a warning post. Any count is null when the platform doesn't report it."},"ExplainerRow":{"properties":{"slug":{"type":"string","title":"Slug"},"title":{"type":"string","title":"Title"},"youtube_title":{"type":"string","title":"Youtube Title"},"trailer":{"type":"boolean","title":"Trailer"},"learn_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Learn Path"},"chapters":{"items":{"type":"string"},"type":"array","title":"Chapters"},"narration":{"type":"string","title":"Narration"},"catalog_version":{"type":"integer","title":"Catalog Version"},"status":{"type":"string","title":"Status"},"version":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Version"},"duration_s":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Duration S"},"video_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Video Url"},"thumb_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Thumb Url"},"youtube_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Youtube Id"},"youtube_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Youtube Url"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"rendered_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rendered At"},"published_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Published At"}},"type":"object","required":["slug","title","youtube_title","trailer","learn_path","chapters","narration","catalog_version","status","version","duration_s","video_url","thumb_url","youtube_id","youtube_url","error","rendered_at","published_at"],"title":"ExplainerRow"},"FollowAccepted":{"properties":{"status":{"type":"string","title":"Status","default":"check_email"}},"type":"object","title":"FollowAccepted"},"FollowRequest":{"properties":{"email":{"type":"string","maxLength":254,"title":"Email"}},"type":"object","required":["email"],"title":"FollowRequest"},"FollowToken":{"properties":{"t":{"type":"string","maxLength":128,"title":"T"}},"type":"object","required":["t"],"title":"FollowToken"},"ForgotPasswordRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"}},"type":"object","required":["email"],"title":"ForgotPasswordRequest"},"Funnel":{"properties":{"days":{"type":"integer","title":"Days"},"views":{"type":"integer","title":"Views"},"stages":{"items":{"$ref":"#/components/schemas/FunnelStage"},"type":"array","title":"Stages"},"direct_cta_sessions":{"type":"integer","title":"Direct Cta Sessions"}},"type":"object","required":["days","views","stages","direct_cta_sessions"],"title":"Funnel","description":"The YouTube-Shorts-shaped question: does the reach become anything?\n\nViews come from the engagement read-back; everything after it from the\nreferral ledger and the map beacon, joined on the per-tab session id.\n`direct_cta_sessions` is the control — a referred conversion rate means\nnothing without the untagged one beside it."},"FunnelStage":{"properties":{"key":{"type":"string","title":"Key"},"label":{"type":"string","title":"Label"},"sessions":{"type":"integer","title":"Sessions"},"from_previous_pct":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"From Previous Pct"}},"type":"object","required":["key","label","sessions","from_previous_pct"],"title":"FunnelStage","description":"One stage of the post → site funnel, for a window."},"GeoJSONFeature":{"properties":{"type":{"type":"string","title":"Type","default":"Feature"},"geometry":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Geometry"},"properties":{"additionalProperties":true,"type":"object","title":"Properties"}},"type":"object","title":"GeoJSONFeature","description":"A GeoJSON Feature. `geometry` may be null for an unlocated feature; the\n`properties` object varies by endpoint (see each endpoint's description)."},"GeoJSONFeatureCollection":{"properties":{"type":{"type":"string","title":"Type","default":"FeatureCollection"},"features":{"items":{"$ref":"#/components/schemas/GeoJSONFeature"},"type":"array","title":"Features"},"metadata":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Metadata"}},"type":"object","required":["features"],"title":"GeoJSONFeatureCollection","description":"The standard spatial response: a GeoJSON FeatureCollection, optionally\nwith a `metadata` block (counts, source, generated_at)."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"KeyUsageResponse":{"properties":{"days":{"items":{"$ref":"#/components/schemas/UsageDay"},"type":"array","title":"Days"},"total_requests":{"type":"integer","title":"Total Requests"},"total_throttled":{"type":"integer","title":"Total Throttled"},"limit_per_min":{"type":"integer","title":"Limit Per Min"}},"type":"object","required":["days","total_requests","total_throttled","limit_per_min"],"title":"KeyUsageResponse"},"LinkToken":{"properties":{"t":{"type":"string","maxLength":128,"title":"T"}},"type":"object","required":["t"],"title":"LinkToken"},"LiveBroadcastRow":{"properties":{"broadcast_id":{"type":"string","title":"Broadcast Id"},"url":{"type":"string","title":"Url"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At"},"ended_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ended At"},"duration_min":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Duration Min"},"went_live":{"type":"boolean","title":"Went Live"},"end_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"End Reason"},"warnings":{"type":"integer","title":"Warnings"},"cuts":{"type":"integer","title":"Cuts"},"highlights":{"type":"integer","title":"Highlights"},"highlights_posted":{"type":"integer","title":"Highlights Posted"},"fb_permalink":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fb Permalink"},"peak_concurrent":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Peak Concurrent"},"avg_concurrent":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg Concurrent"},"vod_views":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Vod Views"},"vod_finalized":{"type":"boolean","title":"Vod Finalized"}},"type":"object","required":["broadcast_id","url","title","started_at","ended_at","duration_min","went_live","end_reason","warnings","cuts","highlights","highlights_posted","fb_permalink","peak_concurrent","avg_concurrent","vod_views","vod_finalized"],"title":"LiveBroadcastRow","description":"One live broadcast from the live_broadcasts ledger (newest first)."},"LiveNow":{"properties":{"broadcast_id":{"type":"string","title":"Broadcast Id"},"url":{"type":"string","title":"Url"},"embed_url":{"type":"string","title":"Embed Url"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At"}},"type":"object","required":["broadcast_id","url","embed_url","title","started_at"],"title":"LiveNow"},"LiveStatus":{"properties":{"live":{"anyOf":[{"$ref":"#/components/schemas/LiveNow"},{"type":"null"}]},"scheduled":{"anyOf":[{"$ref":"#/components/schemas/ScheduledNow"},{"type":"null"}]},"recent":{"items":{"$ref":"#/components/schemas/RecentBroadcast"},"type":"array","title":"Recent"}},"type":"object","required":["live","scheduled","recent"],"title":"LiveStatus"},"LoginRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","title":"Password"}},"type":"object","required":["email","password"],"title":"LoginRequest"},"PaletteUpload":{"properties":{"pal":{"type":"string","title":"Pal","description":"RadarScope/GR .pal color-table text"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name","description":"Display name for the picker"}},"type":"object","required":["pal"],"title":"PaletteUpload"},"PostmortemRow":{"properties":{"vtec":{"type":"string","title":"Vtec"},"status":{"type":"string","title":"Status"},"trigger":{"type":"string","title":"Trigger"},"title":{"type":"string","title":"Title"},"description":{"type":"string","title":"Description"},"script":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Script"},"duration_s":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Duration S"},"video_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Video Url"},"thumb_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Thumb Url"},"youtube_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Youtube Id"},"youtube_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Youtube Url"},"storm_url":{"type":"string","title":"Storm Url"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"published_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Published At"}},"type":"object","required":["vtec","status","trigger","title","description","script","duration_s","video_url","thumb_url","youtube_id","youtube_url","storm_url","error","created_at","published_at"],"title":"PostmortemRow"},"PublishIn":{"properties":{"sid":{"type":"string","maxLength":128,"minLength":8,"title":"Sid"},"caption":{"type":"string","maxLength":2200,"title":"Caption","default":""},"privacy_level":{"type":"string","title":"Privacy Level"},"allow_comment":{"type":"boolean","title":"Allow Comment","default":true},"allow_duet":{"type":"boolean","title":"Allow Duet","default":true},"allow_stitch":{"type":"boolean","title":"Allow Stitch","default":true},"disclose":{"type":"boolean","title":"Disclose","default":false},"your_brand":{"type":"boolean","title":"Your Brand","default":false},"branded_content":{"type":"boolean","title":"Branded Content","default":false}},"type":"object","required":["sid","privacy_level"],"title":"PublishIn"},"PublishedExplainer":{"properties":{"slug":{"type":"string","title":"Slug"},"youtube_id":{"type":"string","title":"Youtube Id"},"published_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Published At"}},"type":"object","required":["slug","youtube_id","published_at"],"title":"PublishedExplainer"},"PushSubscribeRequest":{"properties":{"endpoint":{"type":"string","maxLength":2048,"minLength":1,"title":"Endpoint"},"p256dh":{"type":"string","maxLength":255,"minLength":1,"title":"P256Dh"},"auth":{"type":"string","maxLength":255,"minLength":1,"title":"Auth"},"user_agent":{"anyOf":[{"type":"string","maxLength":512},{"type":"null"}],"title":"User Agent"}},"type":"object","required":["endpoint","p256dh","auth"],"title":"PushSubscribeRequest"},"PushSubscriptionResponse":{"properties":{"id":{"type":"integer","title":"Id"},"user_agent":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Agent"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","user_agent","created_at"],"title":"PushSubscriptionResponse"},"PushUnsubscribeRequest":{"properties":{"endpoint":{"type":"string","maxLength":2048,"minLength":1,"title":"Endpoint"}},"type":"object","required":["endpoint"],"title":"PushUnsubscribeRequest"},"Rating":{"properties":{"ef_scale":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ef Scale"},"ef_number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ef Number"},"length_mi":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Length Mi"},"width_yd":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Width Yd"},"fatalities":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Fatalities"},"injuries":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Injuries"},"track_count":{"type":"integer","title":"Track Count"},"qc":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Qc"}},"type":"object","required":["ef_scale","ef_number","length_mi","width_yd","fatalities","injuries","track_count","qc"],"title":"Rating"},"RecentBroadcast":{"properties":{"broadcast_id":{"type":"string","title":"Broadcast Id"},"url":{"type":"string","title":"Url"},"embed_url":{"type":"string","title":"Embed Url"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At"},"ended_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ended At"},"duration_min":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Duration Min"},"vod_views":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Vod Views"}},"type":"object","required":["broadcast_id","url","embed_url","title","started_at","ended_at","duration_min","vod_views"],"title":"RecentBroadcast"},"ReferralEventIn":{"properties":{"step":{"type":"string","enum":["arrive","cta"],"title":"Step","default":"arrive"},"detail":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Detail"},"sid":{"type":"string","maxLength":64,"minLength":8,"title":"Sid"},"source":{"type":"string","maxLength":32,"minLength":1,"title":"Source"},"campaign":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Campaign"},"medium":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Medium"},"path":{"type":"string","maxLength":128,"minLength":1,"title":"Path"}},"type":"object","required":["sid","source","path"],"title":"ReferralEventIn"},"ReferralRow":{"properties":{"source":{"type":"string","title":"Source"},"campaign":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign"},"visits":{"type":"integer","title":"Visits"},"sessions":{"type":"integer","title":"Sessions"}},"type":"object","required":["source","campaign","visits","sessions"],"title":"ReferralRow","description":"Arrivals on one of our own tagged links, by where they came from."},"ReportSummary":{"properties":{"tornado_count":{"type":"integer","title":"Tornado Count"},"hail_count":{"type":"integer","title":"Hail Count"},"wind_count":{"type":"integer","title":"Wind Count"},"flood_count":{"type":"integer","title":"Flood Count"},"hail_max_in":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Hail Max In"},"wind_max_mph":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Wind Max Mph"}},"type":"object","required":["tornado_count","hail_count","wind_count","flood_count","hail_max_in","wind_max_mph"],"title":"ReportSummary"},"ResetPasswordRequest":{"properties":{"token":{"type":"string","title":"Token"},"password":{"type":"string","maxLength":128,"minLength":8,"title":"Password"}},"type":"object","required":["token","password"],"title":"ResetPasswordRequest"},"SavedLocationResponse":{"properties":{"id":{"type":"integer","title":"Id"},"name":{"type":"string","title":"Name"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label"},"lat":{"type":"number","title":"Lat"},"lon":{"type":"number","title":"Lon"},"alerts_enabled":{"type":"boolean","title":"Alerts Enabled"},"min_severity":{"type":"string","title":"Min Severity"},"show_impact":{"type":"boolean","title":"Show Impact","default":false},"nearby_alerts":{"type":"boolean","title":"Nearby Alerts","default":true},"watch_alerts":{"type":"boolean","title":"Watch Alerts","default":true},"scp_alert_threshold":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Scp Alert Threshold"},"ship_alert_threshold":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Ship Alert Threshold"},"last_composite_alert_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Composite Alert At"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","name","label","lat","lon","alerts_enabled","min_severity","created_at"],"title":"SavedLocationResponse"},"ScheduledNow":{"properties":{"broadcast_id":{"type":"string","title":"Broadcast Id"},"url":{"type":"string","title":"Url"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"headline":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Headline"},"scheduled_for":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scheduled For"}},"type":"object","required":["broadcast_id","url","title","headline","scheduled_for"],"title":"ScheduledNow","description":"A pre-scheduled broadcast for today (MDT/HIGH outlook) that hasn't gone\nlive yet — the waiting room to link / set a reminder on."},"SettingRow":{"properties":{"key":{"type":"string","title":"Key"},"label":{"type":"string","title":"Label"},"value":{"type":"integer","title":"Value"},"env_default":{"type":"integer","title":"Env Default"},"overridden":{"type":"boolean","title":"Overridden"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"},"updated_by":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Updated By"}},"type":"object","required":["key","label","value","env_default","overridden"],"title":"SettingRow","description":"A typed operational knob (app_settings) + its effective value — the numeric\nsibling of ContentFlagRow. `value` is what's in force (override else env);\n`env_default` is what a reset reverts to. 0 means unlimited for the caps."},"ShareByType":{"properties":{"object_type":{"type":"string","title":"Object Type"},"shares":{"type":"integer","title":"Shares"},"visits":{"type":"integer","title":"Visits"}},"type":"object","required":["object_type","shares","visits"],"title":"ShareByType","description":"Share/visit counts for one object type (storm / place / em / map) — which\nsurface people actually share from."},"ShareEventIn":{"properties":{"share_id":{"type":"string","maxLength":64,"minLength":8,"title":"Share Id"},"object_type":{"type":"string","enum":["storm","place","em","map","tropical"],"title":"Object Type"},"object_ref":{"type":"string","maxLength":256,"minLength":1,"title":"Object Ref"},"kind":{"type":"string","enum":["shared","visit"],"title":"Kind"},"platform":{"anyOf":[{"type":"string","maxLength":32},{"type":"null"}],"title":"Platform"}},"type":"object","required":["share_id","object_type","object_ref","kind"],"title":"ShareEventIn"},"SharePerformance":{"properties":{"total_shares":{"type":"integer","title":"Total Shares"},"total_visits":{"type":"integer","title":"Total Visits"},"shares_7d":{"type":"integer","title":"Shares 7D"},"visits_7d":{"type":"integer","title":"Visits 7D"},"by_type":{"items":{"$ref":"#/components/schemas/ShareByType"},"type":"array","title":"By Type"},"top_objects":{"items":{"$ref":"#/components/schemas/ShareTopObject"},"type":"array","title":"Top Objects"}},"type":"object","required":["total_shares","total_visits","shares_7d","visits_7d","by_type","top_objects"],"title":"SharePerformance","description":"Roll-up of the share_events ledger (distribution flywheel bet #1, see\ndocs/VISION.md). visits/shares is the reshare→session viral coefficient the\nstrategy gates ad spend on."},"ShareTopObject":{"properties":{"object_type":{"type":"string","title":"Object Type"},"object_ref":{"type":"string","title":"Object Ref"},"shares":{"type":"integer","title":"Shares"},"visits":{"type":"integer","title":"Visits"},"path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path"},"last_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last At"}},"type":"object","required":["object_type","object_ref","shares","visits","path","last_at"],"title":"ShareTopObject","description":"One shared object's read-back: how many times it was shared, how many\nvisits those shares drove back, a deep link, and when it last moved. The\ntop of this list is the answer to 'which storms travel'."},"SignupAccepted":{"properties":{"status":{"type":"string","title":"Status","default":"check_email"}},"type":"object","title":"SignupAccepted"},"SocialConnection":{"properties":{"platform":{"type":"string","title":"Platform"},"enabled":{"type":"boolean","title":"Enabled"},"blocked_until":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Blocked Until"},"block_kind":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Block Kind"}},"type":"object","required":["platform","enabled"],"title":"SocialConnection"},"SocialLedgerRow":{"properties":{"platform":{"type":"string","title":"Platform"},"sent":{"type":"integer","title":"Sent"},"failed":{"type":"integer","title":"Failed"},"last_success_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Success At"},"last_failure_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Failure At"},"publish_confirmed":{"type":"integer","title":"Publish Confirmed","default":0},"publish_failed":{"type":"integer","title":"Publish Failed","default":0},"publish_pending":{"type":"integer","title":"Publish Pending","default":0}},"type":"object","required":["platform","sent","failed","last_success_at","last_failure_at"],"title":"SocialLedgerRow"},"SocialPostRow":{"properties":{"id":{"type":"integer","title":"Id"},"platform":{"type":"string","title":"Platform"},"event":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Event"},"dedup_key":{"type":"string","title":"Dedup Key"},"nws_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nws Id"},"status":{"type":"string","title":"Status"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"platform_post_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Platform Post Id"},"posted_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Posted At"},"expired_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Expired At"},"confirmed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Confirmed At"}},"type":"object","required":["id","platform","event","dedup_key","nws_id","status","error","platform_post_id","posted_at","expired_at","confirmed_at"],"title":"SocialPostRow"},"SocialTokenRow":{"properties":{"service":{"type":"string","title":"Service"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"},"age_days":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Age Days"}},"type":"object","required":["service","updated_at","age_days"],"title":"SocialTokenRow"},"SourcesResponse":{"properties":{"sources":{"items":{"$ref":"#/components/schemas/DataSource"},"type":"array","title":"Sources"}},"type":"object","required":["sources"],"title":"SourcesResponse","description":"Response of /meta/sources — the data-source catalog."},"SponsorshipCreate":{"properties":{"kind":{"type":"string","pattern":"^(recap|briefing|day)$","title":"Kind","default":"recap"},"period":{"type":"string","pattern":"^\\d{4}-\\d{2}(-\\d{2})?$","title":"Period"},"name":{"type":"string","maxLength":80,"minLength":2,"title":"Name"}},"type":"object","required":["period","name"],"title":"SponsorshipCreate"},"SponsorshipRow":{"properties":{"id":{"type":"integer","title":"Id"},"kind":{"type":"string","title":"Kind"},"period":{"type":"string","title":"Period"},"name":{"type":"string","title":"Name"},"status":{"type":"string","title":"Status"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"}},"type":"object","required":["id","kind","period","name","status","created_at"],"title":"SponsorshipRow"},"StatusLadder":{"properties":{"level":{"type":"string","title":"Level"},"label":{"type":"string","title":"Label"},"rated":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rated"}},"type":"object","required":["level","label","rated"],"title":"StatusLadder"},"StormEvent":{"properties":{"vtec":{"type":"string","title":"Vtec"},"event":{"type":"string","title":"Event"},"alert_id":{"type":"integer","title":"Alert Id"},"office":{"type":"string","title":"Office"},"phenomenon":{"type":"string","title":"Phenomenon"},"headline":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Headline"},"area":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Area"},"lat":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Lat"},"lon":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Lon"},"issued_at":{"type":"string","format":"date-time","title":"Issued At"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"},"is_active":{"type":"boolean","title":"Is Active"},"damage_threat":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Damage Threat"},"detection":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detection"},"status":{"$ref":"#/components/schemas/StatusLadder"},"reports":{"$ref":"#/components/schemas/ReportSummary"},"rating":{"anyOf":[{"$ref":"#/components/schemas/Rating"},{"type":"null"}]},"update_count":{"type":"integer","title":"Update Count"},"timeline":{"items":{"$ref":"#/components/schemas/TimelineItem"},"type":"array","title":"Timeline"},"discuss":{"items":{"$ref":"#/components/schemas/DiscussLink"},"type":"array","title":"Discuss"},"followable":{"type":"boolean","title":"Followable","default":false}},"type":"object","required":["vtec","event","alert_id","office","phenomenon","headline","area","lat","lon","issued_at","expires_at","is_active","damage_threat","detection","status","reports","rating","update_count","timeline","discuss"],"title":"StormEvent"},"StormEventSummary":{"properties":{"vtec":{"type":"string","title":"Vtec"},"event":{"type":"string","title":"Event"},"area":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Area"},"issued_at":{"type":"string","format":"date-time","title":"Issued At"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"is_active":{"type":"boolean","title":"Is Active"},"damage_threat":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Damage Threat"},"alert_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Alert Id"},"lat":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Lat"},"lon":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Lon"}},"type":"object","required":["vtec","event","area","issued_at","expires_at","is_active","damage_threat","alert_id","lat","lon"],"title":"StormEventSummary"},"StormRelative":{"properties":{"motion":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Motion"},"distance_mi":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Distance Mi"},"direction_from_point":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Direction From Point"},"eta_minutes":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Eta Minutes"}},"type":"object","title":"StormRelative","description":"A warned storm, seen from one point. See `get_storm_relative`."},"TestWarningRequest":{"properties":{"token":{"type":"string","maxLength":4096,"minLength":16,"title":"Token"}},"type":"object","required":["token"],"title":"TestWarningRequest"},"TestWarningResult":{"properties":{"msg_id":{"type":"string","title":"Msg Id"}},"type":"object","required":["msg_id"],"title":"TestWarningResult"},"TikTokTestPostResponse":{"properties":{"ok":{"type":"boolean","title":"Ok"},"publish_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Publish Id"},"privacy_level":{"type":"string","title":"Privacy Level"},"alert":{"additionalProperties":true,"type":"object","title":"Alert"},"video_url":{"type":"string","title":"Video Url"},"note":{"type":"string","title":"Note"}},"type":"object","required":["ok","privacy_level","alert","video_url","note"],"title":"TikTokTestPostResponse"},"TimelineItem":{"properties":{"at":{"type":"string","format":"date-time","title":"At"},"kind":{"type":"string","title":"Kind"},"text":{"type":"string","title":"Text"},"report_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Report Type"}},"type":"object","required":["at","kind","text"],"title":"TimelineItem"},"UnitSystem":{"type":"string","enum":["imperial","metric","si"],"title":"UnitSystem"},"UnsubscribeRequest":{"properties":{"u":{"type":"integer","title":"U"},"t":{"type":"string","title":"T"}},"type":"object","required":["u","t"],"title":"UnsubscribeRequest"},"UpdateFlagRequest":{"properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Enabled"},"reset":{"type":"boolean","title":"Reset","default":false}},"type":"object","title":"UpdateFlagRequest"},"UpdateLocationRequest":{"properties":{"name":{"anyOf":[{"type":"string","maxLength":128,"minLength":1},{"type":"null"}],"title":"Name"},"alerts_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Alerts Enabled"},"min_severity":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Min Severity"},"show_impact":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Show Impact"},"nearby_alerts":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Nearby Alerts"},"watch_alerts":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Watch Alerts"},"scp_alert_threshold":{"anyOf":[{"type":"number","maximum":100.0,"minimum":0.0},{"type":"null"}],"title":"Scp Alert Threshold"},"ship_alert_threshold":{"anyOf":[{"type":"number","maximum":100.0,"minimum":0.0},{"type":"null"}],"title":"Ship Alert Threshold"},"clear_scp_threshold":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Clear Scp Threshold"},"clear_ship_threshold":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Clear Ship Threshold"}},"type":"object","title":"UpdateLocationRequest"},"UpdateSettingRequest":{"properties":{"value":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Value"},"reset":{"type":"boolean","title":"Reset","default":false}},"type":"object","title":"UpdateSettingRequest"},"UpdateUserRequest":{"properties":{"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"},"role":{"anyOf":[{"type":"string","pattern":"^(user|admin)$"},{"type":"null"}],"title":"Role"}},"type":"object","title":"UpdateUserRequest"},"UpdateWebhookRequest":{"properties":{"url":{"anyOf":[{"type":"string","maxLength":2048},{"type":"null"}],"title":"Url"},"event_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Event Types"},"min_severity":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Min Severity"},"description":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Description"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"}},"type":"object","title":"UpdateWebhookRequest"},"UsageDay":{"properties":{"date":{"type":"string","title":"Date"},"requests":{"type":"integer","title":"Requests"},"throttled":{"type":"integer","title":"Throttled"}},"type":"object","required":["date","requests","throttled"],"title":"UsageDay"},"UserResponse":{"properties":{"id":{"type":"integer","title":"Id"},"email":{"type":"string","title":"Email"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"role":{"type":"string","title":"Role"},"email_verified":{"type":"boolean","title":"Email Verified"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","email","name","role","email_verified","created_at"],"title":"UserResponse"},"UxBatchIn":{"properties":{"sid":{"type":"string","maxLength":64,"minLength":8,"title":"Sid"},"events":{"items":{"$ref":"#/components/schemas/UxEventIn"},"type":"array","maxItems":50,"minItems":1,"title":"Events"}},"type":"object","required":["sid","events"],"title":"UxBatchIn"},"UxCount":{"properties":{"detail":{"type":"string","title":"Detail"},"count":{"type":"integer","title":"Count"}},"type":"object","required":["detail","count"],"title":"UxCount"},"UxEventCount":{"properties":{"event":{"type":"string","title":"Event"},"count":{"type":"integer","title":"Count"}},"type":"object","required":["event","count"],"title":"UxEventCount"},"UxEventIn":{"properties":{"event":{"type":"string","enum":["mode_set","layer_on","layer_off","scene_apply","share","radar_play","location_set"],"title":"Event"},"mode":{"type":"string","enum":["everyday","tracker","meteorologist","emergency"],"title":"Mode"},"detail":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Detail"}},"type":"object","required":["event","mode"],"title":"UxEventIn"},"UxModePick":{"properties":{"mode":{"type":"string","title":"Mode"},"picks":{"type":"integer","title":"Picks"},"sessions":{"type":"integer","title":"Sessions"}},"type":"object","required":["mode","picks","sessions"],"title":"UxModePick"},"UxSummary":{"properties":{"days":{"type":"integer","title":"Days"},"sessions":{"type":"integer","title":"Sessions"},"events":{"type":"integer","title":"Events"},"mode_picks":{"items":{"$ref":"#/components/schemas/UxModePick"},"type":"array","title":"Mode Picks"},"by_event":{"items":{"$ref":"#/components/schemas/UxEventCount"},"type":"array","title":"By Event"},"top_layers":{"items":{"$ref":"#/components/schemas/UxCount"},"type":"array","title":"Top Layers"},"top_scenes":{"items":{"$ref":"#/components/schemas/UxCount"},"type":"array","title":"Top Scenes"},"shares":{"type":"integer","title":"Shares"},"radar_plays":{"type":"integer","title":"Radar Plays"},"locations_set":{"type":"integer","title":"Locations Set"},"referrals":{"items":{"$ref":"#/components/schemas/ReferralRow"},"type":"array","title":"Referrals"},"referral_sessions":{"type":"integer","title":"Referral Sessions"},"funnel":{"$ref":"#/components/schemas/Funnel"},"ctas":{"items":{"$ref":"#/components/schemas/CtaRow"},"type":"array","title":"Ctas"}},"type":"object","required":["days","sessions","events","mode_picks","by_event","top_layers","top_scenes","shares","radar_plays","locations_set","referrals","referral_sessions","funnel","ctas"],"title":"UxSummary","description":"Last-`days` rollup of the ux_events beacon: how many map sessions sent\nanything, which persona they picked, what they toggled and applied."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VerifyEmailRequest":{"properties":{"token":{"type":"string","title":"Token"}},"type":"object","required":["token"],"title":"VerifyEmailRequest"},"VideoAB":{"properties":{"arms":{"items":{"$ref":"#/components/schemas/VideoABArm"},"type":"array","title":"Arms"},"since":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Since"}},"type":"object","required":["arms","since"],"title":"VideoAB"},"VideoABArm":{"properties":{"variant":{"type":"string","title":"Variant"},"dimension":{"type":"string","title":"Dimension"},"seconds":{"type":"integer","title":"Seconds"},"event":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Event"},"n":{"type":"integer","title":"N"},"n_with_watch":{"type":"integer","title":"N With Watch"},"avg_views":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg Views"},"median_views":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Median Views"},"avg_view_seconds":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg View Seconds"},"completion_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Completion Pct"}},"type":"object","required":["variant","dimension","seconds","event","n","n_with_watch","avg_views","median_views","avg_view_seconds","completion_pct"],"title":"VideoABArm","description":"One arm of the clip-length A/B (YouTube read-back). `completion_pct` =\navg seconds watched / clip seconds — the number the experiment is about."},"VideoGateTypeRow":{"properties":{"content_type":{"type":"string","title":"Content Type"},"views":{"type":"integer","title":"Views"},"minutes_watched":{"type":"integer","title":"Minutes Watched"},"avg_view_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Avg View Seconds"},"subscribers_gained":{"type":"integer","title":"Subscribers Gained"}},"type":"object","required":["content_type","views","minutes_watched","avg_view_seconds","subscribers_gained"],"title":"VideoGateTypeRow","description":"One content type's last-28-day totals (YouTube creatorContentType)."},"VideoGates":{"properties":{"as_of":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"As Of"},"days_covered":{"type":"integer","title":"Days Covered"},"subscribers":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Subscribers"},"subscribers_gained_28d":{"type":"integer","title":"Subscribers Gained 28D"},"subscribers_lost_28d":{"type":"integer","title":"Subscribers Lost 28D"},"views_28d":{"type":"integer","title":"Views 28D"},"shorts_views_28d":{"type":"integer","title":"Shorts Views 28D"},"shorts_views_90d":{"type":"integer","title":"Shorts Views 90D"},"shorts_views_90d_runrate":{"type":"integer","title":"Shorts Views 90D Runrate"},"public_watch_hours_28d":{"type":"number","title":"Public Watch Hours 28D"},"public_watch_hours_365d":{"type":"number","title":"Public Watch Hours 365D"},"public_watch_hours_365d_runrate":{"type":"number","title":"Public Watch Hours 365D Runrate"},"live_hours_28d":{"type":"number","title":"Live Hours 28D"},"vod_hours_28d":{"type":"number","title":"Vod Hours 28D"},"shorts_watch_hours_28d":{"type":"number","title":"Shorts Watch Hours 28D"},"by_type_28d":{"items":{"$ref":"#/components/schemas/VideoGateTypeRow"},"type":"array","title":"By Type 28D"},"thresholds":{"additionalProperties":{"additionalProperties":{"type":"integer"},"type":"object"},"type":"object","title":"Thresholds"}},"type":"object","required":["as_of","days_covered","subscribers","subscribers_gained_28d","subscribers_lost_28d","views_28d","shorts_views_28d","shorts_views_90d","shorts_views_90d_runrate","public_watch_hours_28d","public_watch_hours_365d","public_watch_hours_365d_runrate","live_hours_28d","vod_hours_28d","shorts_watch_hours_28d","by_type_28d","thresholds"],"title":"VideoGates","description":"Progress against the YouTube monetization gates, from channel-level\nanalytics (channel_analytics_daily). The split that matters: YouTube\nEXCLUDES Shorts watch time from the public watch-hour bar, so\n`public_watch_hours_*` = VOD + live only, and Shorts views are their own\nbar. Thresholds per docs/REVENUE_STRATEGY.md §1 (they double 2027-02-01).\nRun-rates project the trailing 28 days onto the gate's window."},"WarningState":{"properties":{"label":{"type":"string","title":"Label"},"state":{"type":"string","title":"State"}},"type":"object","required":["label","state"],"title":"WarningState"},"WebhookCreatedResponse":{"properties":{"id":{"type":"integer","title":"Id"},"url":{"type":"string","title":"Url"},"secret":{"type":"string","title":"Secret"},"geometry":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Geometry"},"event_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Event Types"},"min_severity":{"type":"string","title":"Min Severity"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"is_active":{"type":"boolean","title":"Is Active"},"consecutive_failures":{"type":"integer","title":"Consecutive Failures"},"last_delivery_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Delivery At"},"last_success_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Success At"},"last_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error"},"disabled_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Disabled At"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","url","secret","geometry","event_types","min_severity","description","is_active","consecutive_failures","last_delivery_at","last_success_at","last_error","disabled_at","created_at"],"title":"WebhookCreatedResponse"},"WebhookResponse":{"properties":{"id":{"type":"integer","title":"Id"},"url":{"type":"string","title":"Url"},"secret":{"type":"string","title":"Secret"},"geometry":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Geometry"},"event_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Event Types"},"min_severity":{"type":"string","title":"Min Severity"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"is_active":{"type":"boolean","title":"Is Active"},"consecutive_failures":{"type":"integer","title":"Consecutive Failures"},"last_delivery_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Delivery At"},"last_success_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Success At"},"last_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error"},"disabled_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Disabled At"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","url","secret","geometry","event_types","min_severity","description","is_active","consecutive_failures","last_delivery_at","last_success_at","last_error","disabled_at","created_at"],"title":"WebhookResponse"},"wxagg__api__v1__auth__SignupRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","maxLength":128,"minLength":8,"title":"Password"},"name":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Name"}},"type":"object","required":["email","password"],"title":"SignupRequest"},"wxagg__api__v1__email_warnings__SignupRequest":{"properties":{"email":{"type":"string","maxLength":254,"title":"Email"},"lat":{"type":"number","maximum":90.0,"minimum":-90.0,"title":"Lat"},"lon":{"type":"number","maximum":180.0,"minimum":-180.0,"title":"Lon"},"label":{"type":"string","maxLength":128,"minLength":1,"title":"Label"}},"type":"object","required":["email","lat","lon","label"],"title":"SignupRequest"},"wxagg__api__v1__storm_events__FollowState":{"properties":{"vtec":{"type":"string","title":"Vtec"},"event":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Event"},"place":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Place"},"state":{"type":"string","title":"State"}},"type":"object","required":["vtec","event","place","state"],"title":"FollowState"},"wxagg__api__v1__tropical__FollowState":{"properties":{"slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Slug"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"state":{"type":"string","title":"State"}},"type":"object","required":["slug","name","state"],"title":"FollowState"}},"securitySchemes":{"ApiKeyHeader":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Optional. Raises your rate limit from the anonymous per-IP budget to the registered per-key one. Create a key at /account/api-keys."}}}}