{"openapi":"3.0.1","info":{"title":"Atmosoar Multi-Model API","description":"Weather forecast data and flight-safety impact analysis for drone operations, mission planning systems, and aviation analytics dashboards.\n\nThe API aggregates forecast output from **DWD ICON (`icon-global`, `icon-eu`, `icon-d2`, `icon-d2-ruc`), ECMWF IFS (`ecmwf-ifs`), NOAA GFS (`gfs`), and Météo-France ARPEGE (`arpege`)** and returns normalized, unit-converted results. Pick a model with `source=` (or `modelSource` on `POST /forecast/v2`) to get values from exactly that model; a cell the model cannot serve (outside its domain or forecast range) is `null` with an error rather than filled from another model. When omitted, each cell is served by the best available model for its location and lead time.\n\n## Authentication\nThis service runs behind the **Atmosoar Gateway**. External clients send their bearer token to the gateway, which enforces OAuth2/OIDC authentication and quota, then forwards the request with trusted identity headers. **Direct calls to MMA without going through the gateway return `401 AUTHENTICATION_FAILED`.**\n\n```\nAuthorization: Bearer \u003cjwt-from-keycloak\u003e\n```\n\n## Quickstart — single-point forecast\n```bash\ncurl -G https://api.atmosoar.io/forecast/v1 \\\n -H 'Authorization: Bearer \u003cjwt\u003e' \\\n --data-urlencode 'parameter=temperature_2m' \\\n --data-urlencode 'location=point:55.75,37.6' \\\n --data-urlencode 'time=2026-04-26T12:00:00Z'\n```\n\n## Quickstart — GO / NO-GO schedule for a drone flight\n```bash\ncurl -G https://api.atmosoar.io/impact/v1 \\\n -H 'Authorization: Bearer \u003cjwt\u003e' \\\n --data-urlencode 'location=point:55.75,37.6' \\\n --data-urlencode 'time=2026-04-26T00:00:00Z/2026-04-27T00:00:00Z/PT1H' \\\n --data-urlencode 'cruise_altitude=120' \\\n --data-urlencode 'max_wind_speed=10' --data-urlencode 'max_wind_gust=13' \\\n --data-urlencode 'max_precipitation=0.2' \\\n --data-urlencode 'min_temperature=-5' --data-urlencode 'max_temperature=40' \\\n --data-urlencode 'min_visibility=5' --data-urlencode 'min_cloud_base=300'\n```\n\n## Common request shapes\n- **Locations:** `point:lat,lon`, `polyline:lat1,lon1;lat2,lon2;...`, `rectangle:s,w,n,e`, `bbox:west,south,east,north` (map overlay), `station:\u003cicao-or-wmo\u003e`, or a configured shortcut (e.g. `moscow`).\n- **Times:** single instant `2026-04-26T12:00:00Z`, range with resolution `2026-04-26T00:00:00Z/2026-04-27T00:00:00Z/PT1H`, comma list, or a shortcut (e.g. `next_24h`).\n- **Output formats:** `json` (default), `csv`, `html`, `geojson`, `png`, `grid_png`, `grid_json`, `xml`, plus the chart formats `chart_png`, `chart_svg` and `chartspec` (a rendered time series or meteogram, and the chart definition a frontend can draw itself; `theme=light|dark`).\n\n## Available forecast parameters\n`temperature_2m`, `dew_point_2m`, `relative_humidity_2m`, `wind_speed_10m`, `wind_gusts_10m`, `wind_direction_10m`, `surface_pressure`, `mean_sea_level_pressure`, `total_precipitation` (accumulated mm since model run), `hourly_precipitation` (mm/h, precipitation in the hour ending at the valid time; used by impact analysis; physically-impossible values are returned as `null`), `snow_fall`, `snow_height`, `visibility`, `cloud_cover_total`, `cloud_cover_high`, `cloud_cover_mid`, `cloud_cover_low`, `cloud_ceiling`, `convective_cloud_base`, `convective_cloud_top`, `cape`, `freezing_level`, `shortwave_radiation` (W/m², downward global horizontal irradiance, mean over the hour ending at the valid time), `direct_radiation`, `diffuse_radiation`, `soil_moisture`, `evapotranspiration` (mm/h, reference evapotranspiration ET0, FAO-56 Penman-Monteith, short grass reference surface), `icing_risk` (aviation icing-risk index, %, altitude-aware). Per-endpoint descriptions list which parameters are derived (computed in MMA) versus native (read from the model GRIB).\n\n## Error envelope\nAll `4xx` and `5xx` responses use the same JSON shape:\n\n```json\n{\n \"httpStatus\": 400,\n \"code\": \"VALIDATION_ERROR\",\n \"message\": \"request validation failed\",\n \"errors\": [\n { \"code\": \"MISSING_REQUIRED_FIELD\", \"field\": \"parameter\", \"message\": \"parameter is required\" }\n ]\n}\n```\n\nCommon `code` values: `VALIDATION_ERROR`, `MISSING_REQUIRED_FIELD`, `INVALID_PARAMETER`, `INVALID_LOCATION`, `INVALID_TIME_FORMAT`, `INVALID_TIME_RANGE`, `INVALID_OUTPUT_FORMAT`, `INVALID_THRESHOLD`, `AUTHENTICATION_FAILED`, `FORBIDDEN`, `QUOTA_EXCEEDED`, `NOT_FOUND`, `UPSTREAM_ERROR`, `DATA_SOURCE_UNAVAILABLE`, `INTERNAL_ERROR`.","contact":{"name":"Atmosoar Support","url":"https://atmosoar.io"},"version":"1.37.0","x-logo":{"url":"https://atmosoar.io/logo.png","backgroundColor":"#003b48","altText":"atmosoar"}},"paths":{"/forecast/v1":{"get":{"tags":["Forecast"],"summary":"Run a forecast","description":"Executes a forecast request using query parameters and returns model output for one or more locations and timestamps. Parameter keys and enum values are **case-insensitive**.\n\n### Authentication\nSend your bearer token to the **Atmosoar Gateway**, not directly to this service:\n\n```\nAuthorization: Bearer \u003cjwt\u003e\n```\n\nThe gateway validates the JWT, applies your quota tier, and forwards the call to MMA with trusted identity headers. Calling MMA directly without going through the gateway will return `401 AUTHENTICATION_FAILED`.\n\n### Required query parameters\n| Param | Description | Examples |\n|-------|-------------|----------|\n| `parameter` | Forecast variable, or a comma-separated list for one multi-parameter call (see list below) | `temperature_2m` · `temperature_2m,wind_speed_10m,visibility` |\n| `location` | Where to sample | `point:55.75,37.6` · `polyline:55.7,37.6;55.8,37.7` · `rectangle:55.0,37.0,56.0,38.0` · `bbox:37.0,55.0,38.0,56.0` · `station:UUEE` · shortcut e.g. `moscow` |\n| `time` | When to sample (RFC 3339 UTC) | `2026-04-26T12:00:00Z` · `2026-04-26T00:00:00Z/2026-04-27T00:00:00Z/PT1H` · shortcut e.g. `next_24h` |\n\n### Optional query parameters\n| Param | Default | Notes |\n|-------|---------|-------|\n| `output` | `json` | One of `json`, `csv`, `html`, `geojson`, `png`, `grid_png`, `grid_json`, `xml`, `chart_png`, `chart_svg`, `chartspec` |\n| `theme` | `light` | Colour mode for the chart formats: `light` or `dark`. Ignored by every other format |\n| `source` | `icon-eu` | Force a model: `icon-global`, `icon-eu`, `icon-d2`, `icon-d2-ruc`, `ecmwf-ifs`, `gfs`, `arpege` |\n| `altitude` | surface | Meters above ground; applies wind power-law and ISA temperature lapse |\n| `density` | `10` | For `bbox` map overlays — grid points per degree |\n\n### Available forecast parameters\n`temperature_2m`, `dew_point_2m`, `relative_humidity_2m`, `wind_speed_10m`, `wind_gusts_10m`, `wind_direction_10m`, `surface_pressure`, `mean_sea_level_pressure`, `total_precipitation` (accumulated mm since model run), `hourly_precipitation` (mm/h, precipitation in the hour ending at the valid time; used by impact analysis; physically-impossible values are returned as `null`), `snow_fall`, `snow_height`, `visibility`, `cloud_cover_total`, `cloud_cover_high`, `cloud_cover_mid`, `cloud_cover_low`, `cloud_ceiling`, `convective_cloud_base`, `convective_cloud_top`, `cape`, `freezing_level`, `shortwave_radiation` (W/m², downward global horizontal irradiance, mean over the hour ending at the valid time), `direct_radiation`, `diffuse_radiation` (W/m², same averaging; ICON models only), `soil_moisture`, `evapotranspiration` (mm/h, **reference evapotranspiration ET0, FAO-56 Penman-Monteith**, for a well-watered short grass reference surface — atmospheric evaporative demand, not the actual evapotranspiration of a real surface), `icing_risk` (aviation icing-risk index, %, altitude-aware).\n\nRadiation, `hourly_precipitation` and `evapotranspiration` are hourly means where the serving model is hourly (ICON-D2 to 48 h, ICON-EU to 78 h). Beyond that, and for ECMWF and GFS, radiation and precipitation are the mean of the model's native 3- or 6-hour interval containing the hour, and `evapotranspiration` uses those radiation means with interpolated temperature, humidity and wind.\n\n### Multiple parameters in one call\nPass a comma-separated `parameter` list to retrieve several variables in a single request — one round-trip, **one quota unit** (the parameter count does not multiply quota). Up to 10 distinct parameters per call (`max_parameters_per_request`); duplicates are de-duplicated and surrounding whitespace is trimmed. Each response cell then carries a `parameter` field naming the variable it belongs to. A single-parameter request is unchanged — no `parameter` field is added. The map/grid formats (`png`, `grid_png`, `grid_json`) accept a single parameter only; the chart formats take several and stack one panel per parameter.\n\n```bash\ncurl -G https://api.atmosoar.io/forecast/v1 \\\n -H 'Authorization: Bearer \u003cjwt\u003e' \\\n --data-urlencode 'parameter=temperature_2m,wind_speed_10m' \\\n --data-urlencode 'location=point:55.75,37.6' \\\n --data-urlencode 'time=2026-04-26T12:00:00Z'\n```\n\n```json\n{\n \"data\": {\n \"Status\": \"ok\",\n \"Payload\": [ [\n { \"parameter\": \"temperature_2m\", \"latitude\": 55.75, \"longitude\": 37.6, \"timestamp\": \"2026-04-26T12:00:00Z\", \"value\": 8.42, \"status\": \"ok\" },\n { \"parameter\": \"wind_speed_10m\", \"latitude\": 55.75, \"longitude\": 37.6, \"timestamp\": \"2026-04-26T12:00:00Z\", \"value\": 3.10, \"status\": \"ok\" }\n ] ]\n }\n}\n```\n\n### Bad path — multi-parameter with a single-parameter format (`400 OUTPUT_FORMAT_SINGLE_PARAM_ONLY`)\n```json\n{\n \"httpStatus\": 400,\n \"code\": \"VALIDATION_ERROR\",\n \"errors\": [\n { \"code\": \"OUTPUT_FORMAT_SINGLE_PARAM_ONLY\", \"field\": \"output\", \"message\": \"Output format \\\"grid_png\\\" supports a single parameter only; request one parameter or use json/csv/xml/geojson.\" }\n ]\n}\n```\n\n### Happy path — single point\n```bash\ncurl -G https://api.atmosoar.io/forecast/v1 \\\n -H 'Authorization: Bearer \u003cjwt\u003e' \\\n --data-urlencode 'parameter=temperature_2m' \\\n --data-urlencode 'location=point:55.75,37.6' \\\n --data-urlencode 'time=2026-04-26T12:00:00Z'\n```\n\n```json\n{\n \"httpStatus\": 200,\n \"message\": \"Forecast request received\",\n \"data\": {\n \"Status\": \"ok\",\n \"Payload\": [\n [\n {\n \"latitude\": 55.75,\n \"longitude\": 37.6,\n \"timestamp\": \"2026-04-26T12:00:00Z\",\n \"value\": 8.42,\n \"status\": \"ok\"\n }\n ]\n ]\n }\n}\n```\n\n### Happy path — time range and CSV output\n```bash\ncurl -G https://api.atmosoar.io/forecast/v1 \\\n -H 'Authorization: Bearer \u003cjwt\u003e' \\\n --data-urlencode 'parameter=wind_speed_10m' \\\n --data-urlencode 'location=point:55.75,37.6' \\\n --data-urlencode 'time=2026-04-26T00:00:00Z/2026-04-27T00:00:00Z/PT1H' \\\n --data-urlencode 'output=csv'\n```\n\n### Happy path — map overlay (bbox + grid_png)\n`grid_png` returns a raw PNG body whose RGBA channels encode the forecast value; the `X-Grid-Meta` response header carries the value range and grid spacing needed to colorize it. Use `output=grid_json` if you prefer base64-encoded tiles in a JSON array (multi-timestep).\n\n```bash\ncurl -G https://api.atmosoar.io/forecast/v1 \\\n -H 'Authorization: Bearer \u003cjwt\u003e' \\\n --data-urlencode 'parameter=temperature_2m' \\\n --data-urlencode 'location=bbox:37.0,55.0,38.0,56.0' \\\n --data-urlencode 'time=2026-04-26T12:00:00Z' \\\n --data-urlencode 'output=grid_png' \\\n --data-urlencode 'density=10' \\\n -o tile.png -D headers.txt\n```\n\n### Happy path — a rendered chart (`chart_png`, `chart_svg`, `chartspec`)\nThe chart formats render the forecast itself, not a map. All three come from one chart definition, so a PNG, an SVG and a browser-drawn chart of the same request carry identical colours, units, axis ticks and threshold bands — the same visual language every Atmosoar service uses.\n\nThe form follows the request:\n\n| Request | Chart |\n|---------|-------|\n| one location, one parameter | a time series, with the parameter's operational bands shaded |\n| one location, several parameters | a meteogram — one panel per parameter over a shared time axis |\n| several locations, one parameter | a time series with one line per location (first 8; the subtitle says so) |\n| several locations, several parameters | a meteogram for the first location, named in the subtitle |\n\nUnreported values are drawn as gaps, never as zero. `chartspec` returns the chart as JSON — series, axes, ticks, threshold bands and the resolved palette — so a frontend can draw the identical chart with its own charting library. Add `theme=dark` for the dark palette (its own colour steps, not an inversion).\n\n```bash\ncurl -G https://api.atmosoar.io/forecast/v1 \\\n -H 'Authorization: Bearer \u003cjwt\u003e' \\\n --data-urlencode 'parameter=temperature_2m,wind_speed_10m,total_precipitation' \\\n --data-urlencode 'location=point:47.26,11.39' \\\n --data-urlencode 'time=2026-04-26T00:00:00Z/2026-04-27T00:00:00Z/PT1H' \\\n --data-urlencode 'output=chart_png' \\\n --data-urlencode 'theme=dark' \\\n -o meteogram.png\n```\n\n### Bad path — missing required parameter (`400 VALIDATION_ERROR`)\n```bash\ncurl -G https://api.atmosoar.io/forecast/v1 \\\n -H 'Authorization: Bearer \u003cjwt\u003e' \\\n --data-urlencode 'location=point:55.75,37.6'\n```\n\n```json\n{\n \"httpStatus\": 400,\n \"code\": \"VALIDATION_ERROR\",\n \"message\": \"request validation failed\",\n \"errors\": [\n { \"code\": \"MISSING_REQUIRED_FIELD\", \"field\": \"parameter\", \"message\": \"parameter is required\" },\n { \"code\": \"MISSING_REQUIRED_FIELD\", \"field\": \"time\", \"message\": \"time is required\" }\n ]\n}\n```\n\n### Bad path — unknown parameter (`400 INVALID_PARAMETER`)\n```json\n{\n \"httpStatus\": 400,\n \"code\": \"INVALID_PARAMETER\",\n \"message\": \"unknown forecast parameter: temperature\",\n \"errors\": [\n { \"code\": \"INVALID_PARAMETER\", \"field\": \"parameter\", \"message\": \"unknown forecast parameter: temperature\" }\n ]\n}\n```\n\n### Bad path — malformed location (`400 INVALID_LOCATION`)\n```json\n{\n \"httpStatus\": 400,\n \"code\": \"INVALID_LOCATION\",\n \"message\": \"location must be one of: point:lat,lon · polyline:... · rectangle:... · bbox:... · station:\u003cicao\u003e · \u003cshortcut\u003e\",\n \"errors\": [\n { \"code\": \"INVALID_LOCATION\", \"field\": \"location\", \"message\": \"could not parse 'point:nope'\" }\n ]\n}\n```\n\n### Bad path — calling MMA without going through the gateway (`401 AUTHENTICATION_FAILED`)\n```json\n{\n \"httpStatus\": 401,\n \"code\": \"AUTHENTICATION_FAILED\",\n \"message\": \"gateway identity headers missing\"\n}\n```\n\n### Bad path — upstream weather data ingestor unreachable (`502 UPSTREAM_ERROR`)\n```json\n{\n \"httpStatus\": 502,\n \"code\": \"UPSTREAM_ERROR\",\n \"message\": \"weather data ingestor unavailable\"\n}\n```","operationId":"runForecast","parameters":[{"name":"altitude","in":"query","description":"Altitude in meters. Key is case-insensitive.","schema":{"type":"integer","description":"Altitude in meters. Key is case-insensitive.","format":"int32","example":10}},{"name":"density","in":"query","description":"Grid density in points per degree for bbox locations. Overrides the server default. Must be an integer between 1 and 100.","schema":{"type":"integer","description":"Grid density in points per degree for bbox locations. Overrides the server default. Must be an integer between 1 and 100.","format":"int32","example":10}},{"name":"location","in":"query","description":"Location selector (case-insensitive keys and shortcut/station values).\n\nSupported forms:\n- Point: lat,lon (e.g. 50.1109,8.6821)\n- Point list: lat1,lon1|lat2,lon2\n- Polyline: startLat,startLon,endLat,endLon,resolution\n- Rectangle: neLat,neLon|swLat,swLon (NE must be truly northeast of SW). Rectangles are automatically expanded into an adaptive-resolution grid of evenly-spaced points (max 250). The response contains one entry per grid point in a flat list, each labeled lat,lon.\n- Bbox: bbox:west,south,east,north (WGS84, for map overlay grid output).\n- Station ID: e.g. WMO10381 (case-insensitive)\n- Shortcut: predefined, e.g. germany (case-insensitive).","required":true,"schema":{"type":"string","description":"Location selector (case-insensitive keys and shortcut/station values).\n\nSupported forms:\n- Point: lat,lon (e.g. 50.1109,8.6821)\n- Point list: lat1,lon1|lat2,lon2\n- Polyline: startLat,startLon,endLat,endLon,resolution\n- Rectangle: neLat,neLon|swLat,swLon (NE must be truly northeast of SW). Rectangles are automatically expanded into an adaptive-resolution grid of evenly-spaced points (max 250). The response contains one entry per grid point in a flat list, each labeled lat,lon.\n- Bbox: bbox:west,south,east,north (WGS84, for map overlay grid output).\n- Station ID: e.g. WMO10381 (case-insensitive)\n- Shortcut: predefined, e.g. germany (case-insensitive).","example":"50.1109,8.6821"}},{"name":"output","in":"query","description":"Response format (case-insensitive). Defaults to json when omitted.\n\nChart formats render the forecast as a time series (one location, one parameter), a meteogram (several parameters) or one line per location:\n- chart_png: rendered chart as PNG\n- chart_svg: the same chart as SVG\n- chartspec: the chart definition as JSON, including the resolved palette, for a browser to draw","schema":{"type":"string","description":"Response format (case-insensitive). Defaults to json when omitted.\n\nChart formats render the forecast as a time series (one location, one parameter), a meteogram (several parameters) or one line per location:\n- chart_png: rendered chart as PNG\n- chart_svg: the same chart as SVG\n- chartspec: the chart definition as JSON, including the resolved palette, for a browser to draw","default":"json","example":"json","enum":["json","csv","xml","geojson","html","png","grid_json","grid_png","chart_png","chart_svg","chartspec"]}},{"name":"parameter","in":"query","description":"Forecast variable, or a comma-separated list of variables for a single multi-parameter call (e.g. temperature_2m,wind_speed_10m,visibility). Keys and values are case-insensitive; duplicates are de-duplicated. Up to max_parameters_per_request (default 10) distinct parameters per call.","required":true,"schema":{"type":"string","description":"Forecast variable, or a comma-separated list of variables for a single multi-parameter call (e.g. temperature_2m,wind_speed_10m,visibility). Keys and values are case-insensitive; duplicates are de-duplicated. Up to max_parameters_per_request (default 10) distinct parameters per call.","example":"temperature_2m"}},{"name":"radar_nowcast","in":"query","description":"/nowcast only (ignored by /forecast). Radar-motion nowcast blend, on or off (case-insensitive): on (default) blends the radar service's rain-motion nowcast into precipitation_rate and hourly_precipitation for the next 90 minutes at locations inside a configured radar site window and adds the radar_nowcast block to JSON responses; off serves the precipitation values without the blend. precipitation_rate (mm/h) is a /nowcast-only parameter: its model baseline is the hour's precipitation, refined by station and radar rates. Any other value returns 400 INVALID_PARAMETER.","schema":{"type":"string","description":"/nowcast only (ignored by /forecast). Radar-motion nowcast blend, on or off (case-insensitive): on (default) blends the radar service's rain-motion nowcast into precipitation_rate and hourly_precipitation for the next 90 minutes at locations inside a configured radar site window and adds the radar_nowcast block to JSON responses; off serves the precipitation values without the blend. precipitation_rate (mm/h) is a /nowcast-only parameter: its model baseline is the hour's precipitation, refined by station and radar rates. Any other value returns 400 INVALID_PARAMETER.","default":"on","example":"off"}},{"name":"source","in":"query","description":"Forecast model to serve from (case-insensitive, optional). When set, every value comes from exactly this model; a cell the model cannot serve (outside its domain, beyond its forecast range, or no data yet) is returned with a null value and an error instead of falling back to another model. When omitted, each cell is served by the best available model for its location and lead time.","schema":{"type":"string","description":"Forecast model to serve from (case-insensitive, optional). When set, every value comes from exactly this model; a cell the model cannot serve (outside its domain, beyond its forecast range, or no data yet) is returned with a null value and an error instead of falling back to another model. When omitted, each cell is served by the best available model for its location and lead time.","example":"icon-eu","enum":["icon-global","icon-eu","icon-d2","icon-d2-ruc","ecmwf-ifs","gfs","arpege"]}},{"name":"theme","in":"query","description":"Colour mode for the chart formats (case-insensitive): light (default) or dark. Ignored by every other output format.","schema":{"type":"string","description":"Colour mode for the chart formats (case-insensitive): light (default) or dark. Ignored by every other output format.","default":"light","example":"dark","enum":["light","dark"]}},{"name":"time","in":"query","description":"Time selection in UTC, RFC 3339 (case-insensitive keys and shortcut values).\n\nSupported forms:\n- Single: 2025-07-23T17:56:48Z\n- Range: start,end,resolutionHours (e.g. 2025-07-23T00:00:00Z,2025-07-23T12:00:00Z,3)\n- List: t1|t2|t3\n- Shortcut: now (current model time), 3day (72h forecast window), 7day (168h forecast window), 10day (240h forecast window).","required":true,"schema":{"type":"string","description":"Time selection in UTC, RFC 3339 (case-insensitive keys and shortcut values).\n\nSupported forms:\n- Single: 2025-07-23T17:56:48Z\n- Range: start,end,resolutionHours (e.g. 2025-07-23T00:00:00Z,2025-07-23T12:00:00Z,3)\n- List: t1|t2|t3\n- Shortcut: now (current model time), 3day (72h forecast window), 7day (168h forecast window), 10day (240h forecast window).","example":"2025-07-23T17:56:48Z"}}],"responses":{"200":{"description":"Forecast request received","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsResponseServer"},"example":{"data":{"Payload":[[{"latitude":55.75,"longitude":37.6,"status":"ok","timestamp":"2025-10-11T17:56:48Z","value":1.9548537638760257}]],"Status":"ok"},"httpStatus":200,"message":"Forecast request received"}}}},"202":{"description":"Accepted"},"400":{"description":"Bad request (validation error)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"401":{"description":"Gateway identity missing or invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"413":{"description":"Request too large (over the server cap)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"502":{"description":"Upstream data source error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}}}}},"/forecast/v2":{"post":{"tags":["Forecast"],"summary":"Run a forecast (POST)","description":"Same forecast engine as `GET /forecast/v1`, accepting **POST** with a JSON body. Use this from SDKs, backend services, or anywhere URL length / shareability is not a concern. The query-string form (`v1`) is intended for browser-based URL builders; the JSON body form (`v2`) is the conventional choice for programmatic clients.\n\nField names are camelCase (`timeRange`, `outputFormat`, `modelSource`). `parameter`, `location`, `timeRange`, and `outputFormat` are required. `modelSource` is optional: when set, every value comes from exactly that model and a cell it cannot serve is `null` with an error; when omitted, each cell is served by the best available model.\n\n`parameter` accepts a single variable or a comma-separated list for one multi-parameter call (e.g. `\"temperature_2m,wind_speed_10m\"`) — one quota unit regardless of parameter count, up to 10 distinct parameters. Each response cell then carries a `parameter` field; single-parameter responses are unchanged. The `png`/`grid_png`/`grid_json` formats accept a single parameter only; the chart formats (`chart_png`, `chart_svg`, `chartspec`) take several and stack one panel per parameter. `theme` (`light`/`dark`) selects the chart colour mode.\n\n### `location` and `timeRange` formats\nBoth fields accept **either** a compact string (identical to the `GET /v1` query-string grammar) **or** a structured `{type,payload}` object — pick whichever suits your client:\n\n| Field | Compact string | Structured object |\n|-------|----------------|-------------------|\n| `location` | `point:55.75,37.6` · `bbox:w,s,e,n` · `polyline:lat,lon;lat,lon` · `rectangle:south,west,north,east` · `station:\u003cicao-or-wmo\u003e` · a shortcut | `{\"type\":\"point\",\"payload\":{\"lat\":55.75,\"lon\":37.6}}` |\n| `timeRange` | `2026-04-26T12:00:00Z` · `start/end/PT1H` (ISO-8601 step) · `start/end/6` (integer hours) · a shortcut e.g. `now` | `{\"type\":\"single\",\"payload\":{\"timestamp\":\"2026-04-26T12:00:00Z\"}}` |\n\n### Happy path\n```bash\ncurl -X POST https://api.atmosoar.io/forecast/v2 \\\n -H 'Authorization: Bearer \u003cjwt\u003e' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"parameter\": \"wind_speed_10m\",\n \"location\": \"point:55.75,37.6\",\n \"timeRange\": \"2026-04-26T00:00:00Z/2026-04-27T00:00:00Z/PT1H\",\n \"outputFormat\": \"json\",\n \"modelSource\": \"icon-eu\",\n \"altitude\": 120\n }'\n```\n\n```json\n{\n \"httpStatus\": 200,\n \"message\": \"Forecast request received\",\n \"data\": {\n \"Status\": \"ok\",\n \"Payload\": [\n [\n { \"latitude\": 55.75, \"longitude\": 37.6, \"timestamp\": \"2026-04-26T00:00:00Z\", \"value\": 4.21, \"status\": \"ok\" },\n { \"latitude\": 55.75, \"longitude\": 37.6, \"timestamp\": \"2026-04-26T01:00:00Z\", \"value\": 4.55, \"status\": \"ok\" }\n ]\n ]\n }\n}\n```\n\n### Bad path — malformed JSON (`400 VALIDATION_ERROR`)\n```json\n{\n \"httpStatus\": 400,\n \"code\": \"VALIDATION_ERROR\",\n \"message\": \"could not decode request body: invalid character '}' looking for beginning of object key string\"\n}\n```\n\n### Bad path — unsupported output format (`400 INVALID_OUTPUT_FORMAT`)\n```json\n{\n \"httpStatus\": 400,\n \"code\": \"INVALID_OUTPUT_FORMAT\",\n \"message\": \"outputFormat must be one of: json, csv, html, geojson, png, grid_png, grid_json, xml, chart_png, chart_svg, chartspec\",\n \"errors\": [\n { \"code\": \"INVALID_OUTPUT_FORMAT\", \"field\": \"outputFormat\", \"message\": \"got 'pdf'\" }\n ]\n}\n```\n\nAll other status codes (`401`, `502`, `500`) match `GET /forecast/v1`.","operationId":"runForecastPost","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunForecastPostInput"}}}},"responses":{"200":{"description":"Forecast request received","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsResponseServer"}}}},"202":{"description":"Accepted"},"400":{"description":"Bad request (validation error)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"401":{"description":"Gateway identity missing or invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"413":{"description":"Request too large (over the server cap)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"502":{"description":"Upstream data source error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}}}}},"/impact/v1":{"get":{"tags":["Impact"],"summary":"Impact analysis (v1)","description":"Evaluates forecast data against the flight safety thresholds you supply and returns a per-hour **GO / Caution / NO-GO** schedule for drone operations. The decision is deterministic: each hour is graded against every threshold, and the worst-grade limiting parameter is reported.\n\nAll parameter keys are **case-insensitive**.\n\n### Required query parameters\n| Param | Description | Example |\n|-------|-------------|---------|\n| `location` | Where the operation is | `point:55.75,37.6` or shortcut |\n| `time` | Time range to evaluate (RFC 3339 UTC) | `2026-04-26T00:00:00Z/2026-04-27T00:00:00Z/PT1H` |\n| `cruise_altitude` | Operating altitude in metres. MSL by default; AGL when `altitude_reference=AGL`. | `120` |\n| `max_wind_speed` | Wind speed limit (m/s) | `10` |\n| `max_wind_gust` | Gust limit (m/s) | `13` |\n| `max_precipitation` | Precip limit (mm/h) | `0.2` |\n| `min_temperature` | Cold-side temp limit (°C) | `-5` |\n| `max_temperature` | Hot-side temp limit (°C) | `40` |\n| `min_visibility` | Visibility floor (km) | `5` |\n| `min_cloud_base` | Cloud base floor (m AGL — lowest cloud ceiling above ground; not affected by `altitude_reference`). Sourced from `cloud_ceiling` (ICON-D2 / ICON-EU only); missing in regions without ICON coverage and degrades to a Caution with `missing_data` instead of NO-GO. | `300` |\n\n### Optional query parameters\n| Param | Default | Notes |\n|-------|---------|-------|\n| `days` | `10` | Number of days to evaluate (1–10) |\n| `altitude_reference` | `MSL` | Altitude datum for `cruise_altitude`. `MSL` treats it as metres above mean sea level (default; matches previous behaviour). `AGL` resolves a per-location ground elevation up-front and converts to MSL before the forecast lookup. Per-location lookup failures degrade that location to MSL and are surfaced in the top-level `warnings` array. |\n| `bias_correction` | `auto` | Nudge forecast values toward the latest ground observation near each impact location at T+0, with linear decay to zero by `BIAS_DECAY_HORIZON_H` hours (default 3 h). `auto` enables it; `off` disables. METAR (nearest airport) supplies `ceiling_height` + `visibility`; the Observations API (nearest station) supplies wind speed/gust, temperature, pressure, precipitation, cloud cover. `wind_direction` is never bias-corrected (circular quantity). Per-location bias state appears in the top-level `bias_corrections` map keyed by `\"lat,lon\"`. |\n| `cruise_speed` | — | m/s; enables endurance check **and** the per-hour `flight_time_minutes` estimate (requires ≥2 waypoints) |\n| `max_endurance` | — | minutes; pairs with `cruise_speed` |\n| `thunderstorm_proximity` | — | km — flag NO-GO if CAPE/lightning within this radius |\n| `risk_signals` | `radar,airspace,gpsjam` | risk-signal sources to evaluate (comma-separated, case-insensitive). `radar` samples the latest RainViewer dBZ at each location and forces NO-GO at/above `RADAR_NO_GO_THRESHOLD_DBZ` (default 40) or Caution at/above `RADAR_CAUTION_THRESHOLD_DBZ` (default 25), with a freshness horizon of `RADAR_FRESHNESS_HORIZON_MINUTES` (default 30). `airspace` queries OpenAIP airspace tiles — zones intersecting the route and cruise altitude (PROHIBITED / RESTRICTED / NO_DRONE_ZONE / DANGER / controlled classes) are surfaced as advisory `hazards[]` entries only and never affect per-hour status. `gpsjam` looks up the H3 cell containing each location in the GPSJam daily aggregate; cells at/above `GPSJAM_CAUTION_THRESHOLD` (default 10 %) force Caution. Send `risk_signals=` (empty) to disable all three; per-source upstream failures degrade to top-level `warnings` (`RADAR_LOOKUP_FAILED`, `AIRSPACE_LOOKUP_FAILED`, `GPSJAM_LOOKUP_FAILED`) without aborting the request. |\n\n### Phase-split mission limits\nTake-off and landing are flown at low airspeed close to the ground, where control authority is weakest and turbulence strongest; cruise is flown at working altitude where the wind is stronger but the airframe far more tolerant of it. Every serious OEM publishes a lower take-off/landing wind limit than its cruise limit, and the ratio is airframe-specific, so the sets are independent values rather than a ratio.\n\nWhen `phase_split=true`, waypoints within `phase_radius` of the FIRST waypoint are judged against the take-off limits, waypoints within `phase_radius` of the LAST against the landing limits, and everything between against the existing (cruise) limits. Both zones are contiguous runs measured outward from their pad, so a route that passes back over its own launch point mid-flight is not reclassified as take-off.\n\nThe seven required threshold params above **are** the cruise set. Each `takeoff_*` / `landing_*` param overrides one of them for that phase only; anything left unset inherits the cruise value, so a request that sends none of them behaves exactly as before. There is deliberately no combined shortcut param — clients resolve their own policy layers and send explicit values.\n\n**Sending any phase parameter without `phase_split=true` is a 400, not a silent no-op.** An ignored limit parameter would leave the operator believing a stricter take-off limit applied when it did not.\n\n| Param | Default | Notes |\n|-------|---------|-------|\n| `phase_split` | `false` | Enable per-phase evaluation. |\n| `phase_radius` | derived | Radius in **metres** defining the take-off and landing zones (\u003e 0). Derived as `max(cruise_altitude, 152)` when `altitude_reference=AGL`, otherwise `152` — SORA 2.5's 1:1 rule floored at the 152 m (500 ft) take-off/landing manoeuvring volume that FAA Engineering Brief 105 and EASA PTS-VPT-DSN independently arrive at. An **MSL** `cruise_altitude` is not a height above ground and is never used to size the zone. |\n| `point_location_phase` | `takeoff_landing` | Which phase applies when the location is not an ordered route (single point, station ID, rectangle, or a route that collapses to one point). One of `takeoff_landing`, `takeoff`, `cruise`, `landing`. The default treats a point check as a question about a pad. |\n| `matrix_semantics` | `fixed_hour` | `launch_window` reinterprets each matrix cell as a LAUNCH time: every waypoint is evaluated at launch + its estimated time of arrival, so the landing pad is judged at launch + flight time rather than at launch. Requires `cruise_speed` and an ordered route; degrades to `fixed_hour` otherwise. Costs no extra upstream calls — the (location, hour) grid is already fetched, so this reads a diagonal of it instead of a column. Hours whose mission would land outside the requested window are returned as `Caution` with `missing_data` and the reason `LAUNCH_WINDOW_TRUNCATED`. **Under `launch_window` the hours map is keyed by launch time while `value`/`lat`/`lon` may describe a later hour — read `eta_minutes` to know which.** |\n| `takeoff_*` / `landing_*` | inherit cruise | One override per threshold: `max_wind_speed`, `max_wind_gust`, `max_precipitation`, `min_temperature`, `max_temperature`, `min_visibility`, `min_cloud_base`. Same units and domain rules as their cruise counterparts, including the `min_temperature \u003c max_temperature` cross-check per phase. |\n\n### Response shape\nFor every day in the window, `hours` is a map keyed by `\"HH:MM\"` whose values are objects with the following fields:\n\n| Field | Type | Notes |\n|-------|------|-------|\n| `status` | string | `GO`, `Caution`, or `NO-GO` |\n| `param` | string | Limiting parameter for non-`GO` hours; omitted for `GO` |\n| `diff` | number | Absolute deviation from the relevant threshold; for `GO`, the worst-case margin to the nearest threshold |\n| `value` | number | The offending parameter's forecast value at the triggering location for non-`GO` hours (native unit: m, m/s, mm/h, °C); omitted for `GO` |\n| `lat` / `lon` | number | The location that drove a non-`GO` verdict (for multi-location requests, which waypoint tripped it); omitted for `GO` |\n| `eta_minutes` | number | **Only present** under `matrix_semantics=launch_window`: minutes after LAUNCH at which the triggering waypoint was evaluated. Without it the hour key and the reported value describe different times. |\n| `phase` | string | **Only present** when `phase_split=true` and the hour is non-`GO`: the mission phase of the waypoint that drove the verdict — `takeoff`, `cruise` or `landing`. Waypoints are examined in route order, so a launch breach out-ranks a simultaneous cruise breach. |\n| `missing_data` | bool | `true` if any impact param was unavailable for the hour |\n| `missing_params` | string[] | Canonical names of unavailable params, matching the requested parameter names (precipitation reports as `hourly_precipitation`) |\n| `missing_details` | object[] | **Additive.** Explains *why* params in `missing_params` have no data, for every param the upstream answered with an explicit error cell. Each entry: `{param, error_code, contract}`. `error_code` is the upstream code (`MISSING_DATA`, `FETCH_FAILED`, `GRIB_READ_FAILED`, `DERIVED_NOT_SUPPORTED`, `DATA_ERROR`); `contract` classifies it: `param_no_global_coverage` (the parameter can **never** be served for this location/time — do not retry), `cascade_exhausted` (transient — all sources failed right now, retry later), or `other`. Params missing without an explicit upstream error appear in `missing_params` only. |\n| `flight_time_minutes` | number | **Only present** when `cruise_speed` is set and the route has ≥2 waypoints. Wind-penalised route flight time using the worst-hour wind across waypoints (capped at +15% at `max_wind_speed`). Omitted otherwise. |\n\nWhen `phase_split=true` the response also carries a top-level `applied_config` object echoing the configuration that was actually applied: the resolved `phase_radius_m` and whether it was `explicit` or `derived`, the resolved limits for all three phases (each with an `inherited_from_cruise` list naming the fields that were not sent explicitly), and the segmentation counts (`takeoff_waypoints` / `cruise_waypoints` / `landing_waypoints`). It is absent when `phase_split` is off, so existing callers see an unchanged response. This is the artefact an operations manual needs: a verdict alone does not show which limits produced it.\n\n### Response caching\nIdentical requests (same locations, hours, thresholds, and options) within `IMPACT_RESPONSE_CACHE_TTL_S` (default 60 s) are served from an in-memory cache — the schedule is computed once per burst. Degraded responses (`UPSTREAM_UNAVAILABLE`) are never cached. The SSE stream variant shares the same cache.\n\n### Happy path\n```bash\ncurl -G https://api.atmosoar.io/impact/v1 \\\n -H 'Authorization: Bearer \u003cjwt\u003e' \\\n --data-urlencode 'location=point:55.75,37.6' \\\n --data-urlencode 'time=2026-04-26T00:00:00Z/2026-04-27T00:00:00Z/PT1H' \\\n --data-urlencode 'cruise_altitude=120' \\\n --data-urlencode 'max_wind_speed=10' \\\n --data-urlencode 'max_wind_gust=13' \\\n --data-urlencode 'max_precipitation=0.2' \\\n --data-urlencode 'min_temperature=-5' \\\n --data-urlencode 'max_temperature=40' \\\n --data-urlencode 'min_visibility=5' \\\n --data-urlencode 'min_cloud_base=300'\n```\n\n```json\n{\n \"httpStatus\": 200,\n \"data\": [\n {\n \"date\": \"2026-04-26\",\n \"hours\": {\n \"00:00\": { \"status\": \"GO\", \"diff\": 4.32, \"flight_time_minutes\": 12.05 },\n \"12:00\": { \"status\": \"Caution\", \"param\": \"wind_gust\", \"diff\": 1.6, \"flight_time_minutes\": 12.83 },\n \"15:00\": { \"status\": \"NO-GO\", \"param\": \"wind_speed\", \"diff\": 2.7, \"flight_time_minutes\": 13.86 },\n \"23:00\": { \"status\": \"GO\", \"diff\": 5.1, \"flight_time_minutes\": 12.20 }\n }\n }\n ]\n}\n```\n\n`flight_time_minutes` is omitted from each hour when the request did not supply `cruise_speed` or the location resolves to fewer than two waypoints.\n\n### Happy path — AGL operating altitude with a per-location elevation warning\n```bash\ncurl -G https://api.atmosoar.io/impact/v1 \\\n -H 'Authorization: Bearer \u003cjwt\u003e' \\\n --data-urlencode 'location=point:55.75,37.6' \\\n --data-urlencode 'time=2026-04-26T00:00:00Z/2026-04-27T00:00:00Z/PT1H' \\\n --data-urlencode 'altitude=120' \\\n --data-urlencode 'altitude_reference=AGL' \\\n --data-urlencode 'max_wind_speed=10' \\\n --data-urlencode 'max_wind_gust=13' \\\n --data-urlencode 'max_precipitation=0.2' \\\n --data-urlencode 'min_temperature=-5' \\\n --data-urlencode 'max_temperature=40' \\\n --data-urlencode 'min_visibility=5' \\\n --data-urlencode 'min_cloud_base=300'\n```\n\n```json\n{\n \"httpStatus\": 200,\n \"data\": [ /* days */ ],\n \"warnings\": [\n {\n \"code\": \"ELEVATION_LOOKUP_FAILED\",\n \"latitude\": 55.75,\n \"longitude\": 37.6,\n \"message\": \"Ground elevation lookup failed; altitude treated as MSL\"\n }\n ]\n}\n```\n\n`warnings` is omitted (or empty) when every location resolved successfully. Only the listed location(s) fall back to MSL — other locations in the same request still use the AGL→MSL conversion.\n\n### Happy path — bias correction with the latest ground observations\n```bash\ncurl -G https://api.atmosoar.io/impact/v1 \\\n -H 'Authorization: Bearer \u003cjwt\u003e' \\\n --data-urlencode 'location=point:48.13,11.58' \\\n --data-urlencode 'time=2026-04-26T00:00:00Z/2026-04-27T00:00:00Z/PT1H' \\\n --data-urlencode 'cruise_altitude=120' \\\n --data-urlencode 'altitude_reference=AGL' \\\n --data-urlencode 'bias_correction=auto' \\\n --data-urlencode 'max_wind_speed=10' \\\n --data-urlencode 'max_wind_gust=13' \\\n --data-urlencode 'min_visibility=5' \\\n --data-urlencode 'min_cloud_base=300'\n```\n\n```json\n{\n \"httpStatus\": 200,\n \"data\": [ /* days */ ],\n \"bias_corrections\": {\n \"48.13,11.58\": {\n \"applied\": true,\n \"decay_horizon_h\": 3,\n \"stations\": {\n \"metar\": { \"source\": \"metar\", \"id\": \"EDDM\", \"age_minutes\": 14, \"fields_used\": [\"visibility\", \"ceiling_height\"] },\n \"observations\": { \"source\": \"observations\", \"id\": \"10866\", \"age_minutes\": 9, \"fields_used\": [\"wind_speed_10m\", \"wind_gusts_10m\", \"temperature_2m\", \"pressure_msl\"] }\n },\n \"skipped_fields\": [\"wind_direction\"]\n }\n }\n}\n```\n\n`bias_corrections` is keyed by `\"lat,lon\"` and always present (one entry per unique impact location) when `bias_correction != off`. `applied:false` carries a `reason` field (`off` / `no_station_in_range` / `stale_observation` / `lookup_failed`). `wind_direction` is always listed under `skipped_fields` because circular bias correction is out of scope for v1.\n\n### Hazard integration — SIGMET / TAF\nImpact runs SIGMET hazard checks against every unique impact location for the flight window. Matched SIGMETs force a status change:\n\n| Hazard | Effect |\n|--------|--------|\n| `CONVECTIVE` / `ICING` / `VOLCANIC_ASH` / `DUST` | Forces **NO-GO** with reason `NO_GO_SIGMET_\u003cHAZARD\u003e` |\n| `TURBULENCE` | Escalates to at least **Caution** with reason `CAUTION_SIGMET_TURBULENCE` |\n\nReason codes accumulate in a new per-hour `reasons[]` field (additive — the existing `param` field stays unchanged). Status is the most severe across numerical thresholds and hazards. Matched SIGMETs appear in a new top-level `hazards[]` array with hazard type, validity window, FIR, altitude band (`from_ft` / `to_ft`), the raw advisory text, and the affected `{lat, lon}` locations.\n\nA best-effort TAF check at the nearest aerodrome emits per-location warnings (`TAF_LOW_CEILING`, `TAF_LOW_VISIBILITY`) when forecast ceiling/visibility drops below configured thresholds during any impact hour. **TAF does not change status in v1 — informational only.** TAF lookup failures are silent. SIGMET lookup failures add a top-level warning `SIGMET_LOOKUP_FAILED` and impact decisions continue on numerical thresholds alone.\n\nAltitude-band filtering uses the AGL-resolved MSL altitude (via `altitude_reference=AGL`) when available. When `cruise_altitude` is omitted, the filter conservatively matches every altitude-restricted SIGMET (safety bias).\n\n```json\n{\n \"httpStatus\": 200,\n \"data\": [\n {\n \"date\": \"2026-04-26\",\n \"hours\": {\n \"12:00\": {\n \"status\": \"NO-GO\",\n \"reasons\": [\"NO_GO_SIGMET_CONVECTIVE\"]\n }\n }\n }\n ],\n \"hazards\": [\n {\n \"type\": \"SIGMET\",\n \"hazard\": \"CONVECTIVE\",\n \"valid_from\": \"2026-04-26T11:00:00Z\",\n \"valid_to\": \"2026-04-26T15:00:00Z\",\n \"fir\": \"EDMM\",\n \"altitude_band\": { \"from_ft\": 0, \"to_ft\": 30000 },\n \"raw\": \"SIGMET 1 VALID 261100/261500 EDMM- EDMM FIR EMBD TS ...\",\n \"affected_locations\": [{ \"lat\": 48.13, \"lon\": 11.58 }]\n },\n {\n \"type\": \"RADAR\",\n \"intensity\": \"heavy\",\n \"frame_time\": \"2026-04-26T11:55:00Z\",\n \"affected_locations\": [{ \"lat\": 48.13, \"lon\": 11.58 }]\n },\n {\n \"type\": \"AIRSPACE\",\n \"class\": \"RESTRICTED\",\n \"name\": \"ED-R 401\",\n \"altitude_band\": { \"from_ft\": 0, \"to_ft\": 2500 },\n \"affected_locations\": [{ \"lat\": 48.13, \"lon\": 11.58 }]\n },\n {\n \"type\": \"GPSJAM\",\n \"level\": \"high\",\n \"percent\": 22,\n \"h3_cell\": \"841f8d7ffffffff\",\n \"affected_locations\": [{ \"lat\": 48.13, \"lon\": 11.58 }]\n }\n ],\n \"warnings\": [\n { \"code\": \"TAF_LOW_CEILING\", \"latitude\": 48.13, \"longitude\": 11.58, \"message\": \"TAF forecast ceiling below 500 ft during impact window\" }\n ]\n}\n```\n\n`hazards` is ALWAYS present in the response (possibly an empty array). Per-hour `reasons[]` is emitted only when at least one hazard matched that hour. Existing clients reading `status` / `param` / `days` continue to work unchanged.\n\n### Bad path — invalid `altitude_reference` (`400 INVALID_PARAMETER`)\n```json\n{\n \"httpStatus\": 400,\n \"code\": \"VALIDATION_ERROR\",\n \"message\": \"request validation failed\",\n \"errors\": [\n { \"code\": \"INVALID_PARAMETER\", \"field\": \"altitude_reference\", \"message\": \"altitude_reference must be one of: AGL, MSL.\" }\n ]\n}\n```\n\n### Bad path — invalid `bias_correction` (`400 INVALID_PARAMETER`)\n```json\n{\n \"httpStatus\": 400,\n \"code\": \"VALIDATION_ERROR\",\n \"errors\": [\n { \"code\": \"INVALID_PARAMETER\", \"field\": \"bias_correction\", \"message\": \"bias_correction must be one of: auto, off.\" }\n ]\n}\n```\n\n### Bad path — threshold below zero where physical sense forbids it (`400 INVALID_THRESHOLD`)\n```json\n{\n \"httpStatus\": 400,\n \"code\": \"INVALID_THRESHOLD\",\n \"message\": \"max_wind_speed must be \u003e 0\",\n \"errors\": [\n { \"code\": \"INVALID_THRESHOLD\", \"field\": \"max_wind_speed\", \"message\": \"got -1\" }\n ]\n}\n```\n\n### Bad path — `days` out of range (`400 VALIDATION_ERROR`)\n```json\n{\n \"httpStatus\": 400,\n \"code\": \"VALIDATION_ERROR\",\n \"message\": \"days must be between 1 and 10\",\n \"errors\": [\n { \"code\": \"VALIDATION_ERROR\", \"field\": \"days\", \"message\": \"got 14\" }\n ]\n}\n```\n\n### Degraded path — weather data ingestor unavailable (`200` + `UPSTREAM_UNAVAILABLE`)\nWhen the ingestor is unhealthy a circuit breaker fails reads fast (rather than each sub-call burning its full timeout) and an overall wall-clock cap bounds the whole request. Instead of hanging for minutes or returning `500`, the schedule degrades gracefully: affected hours are returned as `missing_data` Caution and a single request-scoped `UPSTREAM_UNAVAILABLE` warning is added to the top-level `warnings` array:\n\n```json\n{\n \"httpStatus\": 200,\n \"data\": [ /* days — hours carry \"missing_data\": true */ ],\n \"warnings\": [\n {\n \"code\": \"UPSTREAM_UNAVAILABLE\",\n \"message\": \"Weather data service is temporarily unavailable; results may be incomplete (degraded mode)\"\n }\n ]\n}\n```\n\nThe streaming endpoint surfaces the same code as an extra `event: warnings` and then completes with `event: done`. A `500` is reserved for an all-missing result with no ingestor-availability signal (e.g. every cell returned a hard data error).","operationId":"impactAnalysisV1","parameters":[{"name":"altitude_reference","in":"query","description":"Reference frame for cruise_altitude: 'MSL' (default) or 'AGL'. Case-insensitive. When AGL, MMA looks up ground elevation per location and converts to MSL before evaluating the forecast.","schema":{"type":"string","description":"Reference frame for cruise_altitude: 'MSL' (default) or 'AGL'. Case-insensitive. When AGL, MMA looks up ground elevation per location and converts to MSL before evaluating the forecast.","example":"AGL"}},{"name":"bias_correction","in":"query","description":"Observation-based bias correction mode: 'auto' (default) or 'off'. Case-insensitive. When 'auto', MMA pulls the latest METAR + observations near each impact location and nudges forecast values toward them with linear decay over BIAS_DECAY_HORIZON_H hours.","schema":{"type":"string","description":"Observation-based bias correction mode: 'auto' (default) or 'off'. Case-insensitive. When 'auto', MMA pulls the latest METAR + observations near each impact location and nudges forecast values toward them with linear decay over BIAS_DECAY_HORIZON_H hours.","example":"auto"}},{"name":"cruise_altitude","in":"query","description":"Cruise altitude in meters (required, \u003e= 0, case-insensitive key).","required":true,"schema":{"type":"string","description":"Cruise altitude in meters (required, \u003e= 0, case-insensitive key).","example":"120"}},{"name":"cruise_speed","in":"query","description":"Cruise speed in m/s (optional, case-insensitive key).","schema":{"type":"string","description":"Cruise speed in m/s (optional, case-insensitive key).","example":"15"}},{"name":"days","in":"query","description":"Number of forecast days to analyze (1-10, default 10, case-insensitive key).","schema":{"type":"string","description":"Number of forecast days to analyze (1-10, default 10, case-insensitive key).","example":"3"}},{"name":"landing_max_precipitation","in":"query","description":"Landing phase override for max_precipitation (mm/h). Inherits max_precipitation when unset. Requires phase_split=true.","schema":{"type":"string","description":"Landing phase override for max_precipitation (mm/h). Inherits max_precipitation when unset. Requires phase_split=true.","example":"0.2"}},{"name":"landing_max_temperature","in":"query","description":"Landing phase override for max_temperature (deg C). Inherits max_temperature when unset. Requires phase_split=true.","schema":{"type":"string","description":"Landing phase override for max_temperature (deg C). Inherits max_temperature when unset. Requires phase_split=true.","example":"35"}},{"name":"landing_max_wind_gust","in":"query","description":"Landing phase override for max_wind_gust (m/s, \u003e 0). Inherits max_wind_gust when unset. Requires phase_split=true.","schema":{"type":"string","description":"Landing phase override for max_wind_gust (m/s, \u003e 0). Inherits max_wind_gust when unset. Requires phase_split=true.","example":"10"}},{"name":"landing_max_wind_speed","in":"query","description":"Landing phase override for max_wind_speed (m/s). Inherits max_wind_speed when unset. Requires phase_split=true.","schema":{"type":"string","description":"Landing phase override for max_wind_speed (m/s). Inherits max_wind_speed when unset. Requires phase_split=true.","example":"8"}},{"name":"landing_min_cloud_base","in":"query","description":"Landing phase override for min_cloud_base (m). Inherits min_cloud_base when unset. Requires phase_split=true.","schema":{"type":"string","description":"Landing phase override for min_cloud_base (m). Inherits min_cloud_base when unset. Requires phase_split=true.","example":"400"}},{"name":"landing_min_temperature","in":"query","description":"Landing phase override for min_temperature (deg C). Inherits min_temperature when unset. Requires phase_split=true.","schema":{"type":"string","description":"Landing phase override for min_temperature (deg C). Inherits min_temperature when unset. Requires phase_split=true.","example":"-5"}},{"name":"landing_min_visibility","in":"query","description":"Landing phase override for min_visibility (km). Inherits min_visibility when unset. Requires phase_split=true.","schema":{"type":"string","description":"Landing phase override for min_visibility (km). Inherits min_visibility when unset. Requires phase_split=true.","example":"6"}},{"name":"location","in":"query","description":"Location selector (case-insensitive key and shortcut/station values).\n\nSupported forms:\n- Point: lat,lon (e.g. 50.1109,8.6821)\n- Point list: lat1,lon1|lat2,lon2\n- Polyline: startLat,startLon,endLat,endLon,resolution\n- Rectangle: neLat,neLon|swLat,swLon (NE must be truly northeast of SW)\n- Station ID: e.g. WMO10381 (case-insensitive)\n- Shortcut: predefined, e.g. germany (case-insensitive).","required":true,"schema":{"type":"string","description":"Location selector (case-insensitive key and shortcut/station values).\n\nSupported forms:\n- Point: lat,lon (e.g. 50.1109,8.6821)\n- Point list: lat1,lon1|lat2,lon2\n- Polyline: startLat,startLon,endLat,endLon,resolution\n- Rectangle: neLat,neLon|swLat,swLon (NE must be truly northeast of SW)\n- Station ID: e.g. WMO10381 (case-insensitive)\n- Shortcut: predefined, e.g. germany (case-insensitive).","example":"50.1109,8.6821"}},{"name":"matrix_semantics","in":"query","description":"How a matrix cell is interpreted: 'fixed_hour' (default) evaluates every waypoint at that hour; 'launch_window' treats the hour as the LAUNCH time and evaluates each waypoint at launch + its estimated time of arrival, so the landing pad is judged at launch + flight time. Requires cruise_speed and an ordered route; degrades to fixed_hour with a warning otherwise. Case-insensitive.","schema":{"type":"string","description":"How a matrix cell is interpreted: 'fixed_hour' (default) evaluates every waypoint at that hour; 'launch_window' treats the hour as the LAUNCH time and evaluates each waypoint at launch + its estimated time of arrival, so the landing pad is judged at launch + flight time. Requires cruise_speed and an ordered route; degrades to fixed_hour with a warning otherwise. Case-insensitive.","example":"launch_window"}},{"name":"max_endurance","in":"query","description":"Maximum flight endurance in minutes (optional, case-insensitive key).","schema":{"type":"string","description":"Maximum flight endurance in minutes (optional, case-insensitive key).","example":"45"}},{"name":"max_precipitation","in":"query","description":"Maximum acceptable precipitation in mm/h (required, case-insensitive key).","required":true,"schema":{"type":"string","description":"Maximum acceptable precipitation in mm/h (required, case-insensitive key).","example":"0.5"}},{"name":"max_temperature","in":"query","description":"Maximum acceptable temperature in deg C (required, case-insensitive key).","required":true,"schema":{"type":"string","description":"Maximum acceptable temperature in deg C (required, case-insensitive key).","example":"40"}},{"name":"max_wind_gust","in":"query","description":"Maximum acceptable wind gust speed in m/s (required, \u003e 0, case-insensitive key). Checked against wind_gusts_10m.","required":true,"schema":{"type":"string","description":"Maximum acceptable wind gust speed in m/s (required, \u003e 0, case-insensitive key). Checked against wind_gusts_10m.","example":"18"}},{"name":"max_wind_speed","in":"query","description":"Maximum acceptable sustained wind speed in m/s (required, case-insensitive key). Checked against wind_speed_10m.","required":true,"schema":{"type":"string","description":"Maximum acceptable sustained wind speed in m/s (required, case-insensitive key). Checked against wind_speed_10m.","example":"12"}},{"name":"min_cloud_base","in":"query","description":"Minimum acceptable cloud base height in m (required, case-insensitive key).","required":true,"schema":{"type":"string","description":"Minimum acceptable cloud base height in m (required, case-insensitive key).","example":"500"}},{"name":"min_temperature","in":"query","description":"Minimum acceptable temperature in deg C (required, case-insensitive key).","required":true,"schema":{"type":"string","description":"Minimum acceptable temperature in deg C (required, case-insensitive key).","example":"-5"}},{"name":"min_visibility","in":"query","description":"Minimum acceptable visibility in km (required, case-insensitive key).","required":true,"schema":{"type":"string","description":"Minimum acceptable visibility in km (required, case-insensitive key).","example":"5"}},{"name":"phase_radius","in":"query","description":"Radius in METRES around the first/last waypoint defining the take-off and landing zones (\u003e 0). Default: max(cruise_altitude, 152) when altitude_reference=AGL, otherwise 152 — SORA 2.5's 1:1 rule with the 152 m (500 ft) floor from FAA EB-105 / EASA PTS-VPT-DSN. Requires phase_split=true.","schema":{"type":"string","description":"Radius in METRES around the first/last waypoint defining the take-off and landing zones (\u003e 0). Default: max(cruise_altitude, 152) when altitude_reference=AGL, otherwise 152 — SORA 2.5's 1:1 rule with the 152 m (500 ft) floor from FAA EB-105 / EASA PTS-VPT-DSN. Requires phase_split=true.","example":"152"}},{"name":"phase_split","in":"query","description":"Enable phase-split evaluation: waypoints near the first and last waypoint are judged against take-off/landing limits instead of the cruise set. 'true' or 'false' (default). Case-insensitive.","schema":{"type":"string","description":"Enable phase-split evaluation: waypoints near the first and last waypoint are judged against take-off/landing limits instead of the cruise set. 'true' or 'false' (default). Case-insensitive.","example":"true"}},{"name":"point_location_phase","in":"query","description":"Which phase applies when the location is not an ordered route (single point, station ID, rectangle, or a route that collapses to one point): 'takeoff_landing' (default), 'takeoff', 'cruise' or 'landing'. Case-insensitive. Requires phase_split=true.","schema":{"type":"string","description":"Which phase applies when the location is not an ordered route (single point, station ID, rectangle, or a route that collapses to one point): 'takeoff_landing' (default), 'takeoff', 'cruise' or 'landing'. Case-insensitive. Requires phase_split=true.","example":"takeoff_landing"}},{"name":"risk_signals","in":"query","description":"Comma-separated risk-signal sources to evaluate: 'radar' (live RainViewer precipitation), 'airspace' (OpenAIP regulated airspace), 'gpsjam' (GPS interference cells). Case-insensitive. Omit the parameter for all-on (default). Send 'risk_signals=' (empty) to disable all three.","schema":{"type":"string","description":"Comma-separated risk-signal sources to evaluate: 'radar' (live RainViewer precipitation), 'airspace' (OpenAIP regulated airspace), 'gpsjam' (GPS interference cells). Case-insensitive. Omit the parameter for all-on (default). Send 'risk_signals=' (empty) to disable all three.","example":"radar,airspace,gpsjam","nullable":true}},{"name":"takeoff_max_precipitation","in":"query","description":"Take-off phase override for max_precipitation (mm/h). Inherits max_precipitation when unset. Requires phase_split=true.","schema":{"type":"string","description":"Take-off phase override for max_precipitation (mm/h). Inherits max_precipitation when unset. Requires phase_split=true.","example":"0.2"}},{"name":"takeoff_max_temperature","in":"query","description":"Take-off phase override for max_temperature (deg C). Inherits max_temperature when unset. Requires phase_split=true.","schema":{"type":"string","description":"Take-off phase override for max_temperature (deg C). Inherits max_temperature when unset. Requires phase_split=true.","example":"35"}},{"name":"takeoff_max_wind_gust","in":"query","description":"Take-off phase override for max_wind_gust (m/s, \u003e 0). Inherits max_wind_gust when unset. Requires phase_split=true.","schema":{"type":"string","description":"Take-off phase override for max_wind_gust (m/s, \u003e 0). Inherits max_wind_gust when unset. Requires phase_split=true.","example":"10"}},{"name":"takeoff_max_wind_speed","in":"query","description":"Take-off phase override for max_wind_speed (m/s). Inherits max_wind_speed when unset. Requires phase_split=true.","schema":{"type":"string","description":"Take-off phase override for max_wind_speed (m/s). Inherits max_wind_speed when unset. Requires phase_split=true.","example":"8"}},{"name":"takeoff_min_cloud_base","in":"query","description":"Take-off phase override for min_cloud_base (m). Inherits min_cloud_base when unset. Requires phase_split=true.","schema":{"type":"string","description":"Take-off phase override for min_cloud_base (m). Inherits min_cloud_base when unset. Requires phase_split=true.","example":"400"}},{"name":"takeoff_min_temperature","in":"query","description":"Take-off phase override for min_temperature (deg C). Inherits min_temperature when unset. Requires phase_split=true.","schema":{"type":"string","description":"Take-off phase override for min_temperature (deg C). Inherits min_temperature when unset. Requires phase_split=true.","example":"-5"}},{"name":"takeoff_min_visibility","in":"query","description":"Take-off phase override for min_visibility (km). Inherits min_visibility when unset. Requires phase_split=true.","schema":{"type":"string","description":"Take-off phase override for min_visibility (km). Inherits min_visibility when unset. Requires phase_split=true.","example":"6"}},{"name":"thunderstorm_proximity","in":"query","description":"Minimum distance to thunderstorms in km (optional, case-insensitive key).","schema":{"type":"string","description":"Minimum distance to thunderstorms in km (optional, case-insensitive key).","example":"30"}},{"name":"time","in":"query","description":"Time selection in UTC, RFC 3339 (case-insensitive key and shortcut values).\n\nSupported forms:\n- Single: 2025-07-23T17:56:48Z\n- Range: start,end,resolutionHours\n- List: t1|t2|t3\n- Shortcut: now (current model time).","required":true,"schema":{"type":"string","description":"Time selection in UTC, RFC 3339 (case-insensitive key and shortcut values).\n\nSupported forms:\n- Single: 2025-07-23T17:56:48Z\n- Range: start,end,resolutionHours\n- List: t1|t2|t3\n- Shortcut: now (current model time).","example":"2025-07-23T17:56:48Z"}}],"responses":{"200":{"description":"Impact result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsResponseServer"}}}},"202":{"description":"Impact result"},"400":{"description":"Bad request (validation error)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"401":{"description":"Gateway identity missing or invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"413":{"description":"Request too large (over the server cap)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"502":{"description":"Upstream data source error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}}}}},"/impact/v1/stream":{"get":{"tags":["Impact"],"summary":"Impact analysis — SSE stream","description":"Streams per-hour GO / Caution / NO-GO impact results as **Server-Sent Events (SSE)**. Accepts the same query parameters as `GET /impact/v1`. Each hour is pushed to the client in chronological order as soon as it is evaluated, so dashboards can paint progress instead of waiting for the full schedule.\n\n**Content-Type:** `text/event-stream`\n\n### Event types\n| Event | When | Payload |\n|-------|------|---------|\n| `warnings` | Always emitted **once, before any hour event** | `{ \"warnings\": [ { \"code\":\"ELEVATION_LOOKUP_FAILED\", \"latitude\":55.75, \"longitude\":37.6, \"message\":\"Ground elevation lookup failed; altitude treated as MSL\" } ] }` (the `warnings` array is always present, possibly empty) |\n| `bias_corrections` | Emitted **once, after `warnings` and before `hazards`** | `{ \"bias_corrections\": { \"48.13,11.58\": { \"applied\": true, \"decay_horizon_h\": 3, \"stations\": { /* metar + observations */ }, \"skipped_fields\": [\"wind_direction\"] } } }` (always present; the inner map carries one entry per unique impact location, possibly with `applied:false`). Omitted only when `bias_correction=off`. |\n| `hazards` | Emitted **once, after `bias_corrections` and before any hour event** | `{ \"hazards\": [ { \"type\":\"SIGMET\", \"hazard\":\"CONVECTIVE\", \"valid_from\":\"...\", \"valid_to\":\"...\", \"fir\":\"EDMM\", \"altitude_band\": { \"from_ft\":0, \"to_ft\":30000 }, \"raw\":\"...\", \"affected_locations\": [...] } ] }` (the `hazards` array is always present, possibly empty). Per-hour events on the same stream may carry a `reasons[]` field referencing matched hazards (e.g. `\"reasons\": [\"NO_GO_SIGMET_CONVECTIVE\"]`). |\n| (default) | One per evaluated hour | `{ \"date\":\"2026-04-26\", \"hour\":\"12:00\", \"status\":\"GO\", \"diff\":4.3, \"missing_data\":false, \"flight_time_minutes\":12.05 }` |\n| `done` | Last event when the stream completes successfully | `{ \"start_hour\": \"...\", \"days_completed\": 10 }` |\n| `error` | Terminal — stream closes after this | `{ \"message\":\"...\" }` |\n\nPer-hour fields mirror `GET /impact/v1` — including the optional `flight_time_minutes` (only emitted when `cruise_speed` is supplied and the route has ≥2 waypoints), the optional `reasons[]` array carrying SIGMET hazard codes that affected this hour, and the optional `missing_details[]` array explaining why params in `missing_params` have no data (`{param, error_code, contract}` — see `GET /impact/v1`). The `altitude_reference` and `bias_correction` query parameters behave identically to `GET /impact/v1`. Upfront events are emitted in the order: `warnings` → `bias_corrections` → `hazards`, all before any hour event.\n\nThe stream shares `GET /impact/v1`'s short-TTL response cache: an identical request within `IMPACT_RESPONSE_CACHE_TTL_S` (default 60 s) replays the cached schedule instead of recomputing it.\n\nIf the client disconnects mid-stream, the server cancels remaining in-flight upstream fetches so quota is not consumed for unread hours.\n\n### Happy path\n```bash\ncurl -N -G https://api.atmosoar.io/impact/v1/stream \\\n -H 'Authorization: Bearer \u003cjwt\u003e' \\\n -H 'Accept: text/event-stream' \\\n --data-urlencode 'location=point:55.75,37.6' \\\n --data-urlencode 'time=2026-04-26T00:00:00Z/2026-04-27T00:00:00Z/PT1H' \\\n --data-urlencode 'cruise_altitude=120' \\\n --data-urlencode 'max_wind_speed=10' \\\n --data-urlencode 'max_wind_gust=13' \\\n --data-urlencode 'max_precipitation=0.2' \\\n --data-urlencode 'min_temperature=-5' \\\n --data-urlencode 'max_temperature=40' \\\n --data-urlencode 'min_visibility=5' \\\n --data-urlencode 'min_cloud_base=300'\n```\n\n```\ndata: {\"date\":\"2026-04-26\",\"hour\":\"00:00\",\"status\":\"GO\",\"diff\":4.32,\"missing_data\":false,\"flight_time_minutes\":12.05}\n\ndata: {\"date\":\"2026-04-26\",\"hour\":\"15:00\",\"status\":\"NO-GO\",\"param\":\"wind_speed\",\"diff\":2.7,\"missing_data\":false,\"flight_time_minutes\":13.86}\n\nevent: done\ndata: {\"start_hour\":\"2026-04-26T00:00:00Z\",\"days_completed\":1}\n```\n\n### Bad path — terminal error event\n```\nevent: error\ndata: {\"code\":\"UPSTREAM_ERROR\",\"message\":\"weather data ingestor unavailable\"}\n```\n\nValidation errors are returned as a normal JSON `400` (see `GET /impact/v1`) — the stream is never opened in that case.","operationId":"impactAnalysisV1Stream","parameters":[{"name":"altitude_reference","in":"query","description":"Reference frame for cruise_altitude: 'MSL' (default) or 'AGL'. Case-insensitive. When AGL, MMA looks up ground elevation per location and converts to MSL before evaluating the forecast.","schema":{"type":"string","description":"Reference frame for cruise_altitude: 'MSL' (default) or 'AGL'. Case-insensitive. When AGL, MMA looks up ground elevation per location and converts to MSL before evaluating the forecast.","example":"AGL"}},{"name":"bias_correction","in":"query","description":"Observation-based bias correction mode: 'auto' (default) or 'off'. Case-insensitive. When 'auto', MMA pulls the latest METAR + observations near each impact location and nudges forecast values toward them with linear decay over BIAS_DECAY_HORIZON_H hours.","schema":{"type":"string","description":"Observation-based bias correction mode: 'auto' (default) or 'off'. Case-insensitive. When 'auto', MMA pulls the latest METAR + observations near each impact location and nudges forecast values toward them with linear decay over BIAS_DECAY_HORIZON_H hours.","example":"auto"}},{"name":"cruise_altitude","in":"query","description":"Cruise altitude in meters (required, \u003e= 0, case-insensitive key).","required":true,"schema":{"type":"string","description":"Cruise altitude in meters (required, \u003e= 0, case-insensitive key).","example":"120"}},{"name":"cruise_speed","in":"query","description":"Cruise speed in m/s (optional, case-insensitive key).","schema":{"type":"string","description":"Cruise speed in m/s (optional, case-insensitive key).","example":"15"}},{"name":"days","in":"query","description":"Number of forecast days to analyze (1-10, default 10, case-insensitive key).","schema":{"type":"string","description":"Number of forecast days to analyze (1-10, default 10, case-insensitive key).","example":"3"}},{"name":"landing_max_precipitation","in":"query","description":"Landing phase override for max_precipitation (mm/h). Inherits max_precipitation when unset. Requires phase_split=true.","schema":{"type":"string","description":"Landing phase override for max_precipitation (mm/h). Inherits max_precipitation when unset. Requires phase_split=true.","example":"0.2"}},{"name":"landing_max_temperature","in":"query","description":"Landing phase override for max_temperature (deg C). Inherits max_temperature when unset. Requires phase_split=true.","schema":{"type":"string","description":"Landing phase override for max_temperature (deg C). Inherits max_temperature when unset. Requires phase_split=true.","example":"35"}},{"name":"landing_max_wind_gust","in":"query","description":"Landing phase override for max_wind_gust (m/s, \u003e 0). Inherits max_wind_gust when unset. Requires phase_split=true.","schema":{"type":"string","description":"Landing phase override for max_wind_gust (m/s, \u003e 0). Inherits max_wind_gust when unset. Requires phase_split=true.","example":"10"}},{"name":"landing_max_wind_speed","in":"query","description":"Landing phase override for max_wind_speed (m/s). Inherits max_wind_speed when unset. Requires phase_split=true.","schema":{"type":"string","description":"Landing phase override for max_wind_speed (m/s). Inherits max_wind_speed when unset. Requires phase_split=true.","example":"8"}},{"name":"landing_min_cloud_base","in":"query","description":"Landing phase override for min_cloud_base (m). Inherits min_cloud_base when unset. Requires phase_split=true.","schema":{"type":"string","description":"Landing phase override for min_cloud_base (m). Inherits min_cloud_base when unset. Requires phase_split=true.","example":"400"}},{"name":"landing_min_temperature","in":"query","description":"Landing phase override for min_temperature (deg C). Inherits min_temperature when unset. Requires phase_split=true.","schema":{"type":"string","description":"Landing phase override for min_temperature (deg C). Inherits min_temperature when unset. Requires phase_split=true.","example":"-5"}},{"name":"landing_min_visibility","in":"query","description":"Landing phase override for min_visibility (km). Inherits min_visibility when unset. Requires phase_split=true.","schema":{"type":"string","description":"Landing phase override for min_visibility (km). Inherits min_visibility when unset. Requires phase_split=true.","example":"6"}},{"name":"location","in":"query","description":"Location selector (case-insensitive key and shortcut/station values).\n\nSupported forms:\n- Point: lat,lon (e.g. 50.1109,8.6821)\n- Point list: lat1,lon1|lat2,lon2\n- Polyline: startLat,startLon,endLat,endLon,resolution\n- Rectangle: neLat,neLon|swLat,swLon (NE must be truly northeast of SW)\n- Station ID: e.g. WMO10381 (case-insensitive)\n- Shortcut: predefined, e.g. germany (case-insensitive).","required":true,"schema":{"type":"string","description":"Location selector (case-insensitive key and shortcut/station values).\n\nSupported forms:\n- Point: lat,lon (e.g. 50.1109,8.6821)\n- Point list: lat1,lon1|lat2,lon2\n- Polyline: startLat,startLon,endLat,endLon,resolution\n- Rectangle: neLat,neLon|swLat,swLon (NE must be truly northeast of SW)\n- Station ID: e.g. WMO10381 (case-insensitive)\n- Shortcut: predefined, e.g. germany (case-insensitive).","example":"50.1109,8.6821"}},{"name":"matrix_semantics","in":"query","description":"How a matrix cell is interpreted: 'fixed_hour' (default) evaluates every waypoint at that hour; 'launch_window' treats the hour as the LAUNCH time and evaluates each waypoint at launch + its estimated time of arrival, so the landing pad is judged at launch + flight time. Requires cruise_speed and an ordered route; degrades to fixed_hour with a warning otherwise. Case-insensitive.","schema":{"type":"string","description":"How a matrix cell is interpreted: 'fixed_hour' (default) evaluates every waypoint at that hour; 'launch_window' treats the hour as the LAUNCH time and evaluates each waypoint at launch + its estimated time of arrival, so the landing pad is judged at launch + flight time. Requires cruise_speed and an ordered route; degrades to fixed_hour with a warning otherwise. Case-insensitive.","example":"launch_window"}},{"name":"max_endurance","in":"query","description":"Maximum flight endurance in minutes (optional, case-insensitive key).","schema":{"type":"string","description":"Maximum flight endurance in minutes (optional, case-insensitive key).","example":"45"}},{"name":"max_precipitation","in":"query","description":"Maximum acceptable precipitation in mm/h (required, case-insensitive key).","required":true,"schema":{"type":"string","description":"Maximum acceptable precipitation in mm/h (required, case-insensitive key).","example":"0.5"}},{"name":"max_temperature","in":"query","description":"Maximum acceptable temperature in deg C (required, case-insensitive key).","required":true,"schema":{"type":"string","description":"Maximum acceptable temperature in deg C (required, case-insensitive key).","example":"40"}},{"name":"max_wind_gust","in":"query","description":"Maximum acceptable wind gust speed in m/s (required, \u003e 0, case-insensitive key). Checked against wind_gusts_10m.","required":true,"schema":{"type":"string","description":"Maximum acceptable wind gust speed in m/s (required, \u003e 0, case-insensitive key). Checked against wind_gusts_10m.","example":"18"}},{"name":"max_wind_speed","in":"query","description":"Maximum acceptable sustained wind speed in m/s (required, case-insensitive key). Checked against wind_speed_10m.","required":true,"schema":{"type":"string","description":"Maximum acceptable sustained wind speed in m/s (required, case-insensitive key). Checked against wind_speed_10m.","example":"12"}},{"name":"min_cloud_base","in":"query","description":"Minimum acceptable cloud base height in m (required, case-insensitive key).","required":true,"schema":{"type":"string","description":"Minimum acceptable cloud base height in m (required, case-insensitive key).","example":"500"}},{"name":"min_temperature","in":"query","description":"Minimum acceptable temperature in deg C (required, case-insensitive key).","required":true,"schema":{"type":"string","description":"Minimum acceptable temperature in deg C (required, case-insensitive key).","example":"-5"}},{"name":"min_visibility","in":"query","description":"Minimum acceptable visibility in km (required, case-insensitive key).","required":true,"schema":{"type":"string","description":"Minimum acceptable visibility in km (required, case-insensitive key).","example":"5"}},{"name":"phase_radius","in":"query","description":"Radius in METRES around the first/last waypoint defining the take-off and landing zones (\u003e 0). Default: max(cruise_altitude, 152) when altitude_reference=AGL, otherwise 152 — SORA 2.5's 1:1 rule with the 152 m (500 ft) floor from FAA EB-105 / EASA PTS-VPT-DSN. Requires phase_split=true.","schema":{"type":"string","description":"Radius in METRES around the first/last waypoint defining the take-off and landing zones (\u003e 0). Default: max(cruise_altitude, 152) when altitude_reference=AGL, otherwise 152 — SORA 2.5's 1:1 rule with the 152 m (500 ft) floor from FAA EB-105 / EASA PTS-VPT-DSN. Requires phase_split=true.","example":"152"}},{"name":"phase_split","in":"query","description":"Enable phase-split evaluation: waypoints near the first and last waypoint are judged against take-off/landing limits instead of the cruise set. 'true' or 'false' (default). Case-insensitive.","schema":{"type":"string","description":"Enable phase-split evaluation: waypoints near the first and last waypoint are judged against take-off/landing limits instead of the cruise set. 'true' or 'false' (default). Case-insensitive.","example":"true"}},{"name":"point_location_phase","in":"query","description":"Which phase applies when the location is not an ordered route (single point, station ID, rectangle, or a route that collapses to one point): 'takeoff_landing' (default), 'takeoff', 'cruise' or 'landing'. Case-insensitive. Requires phase_split=true.","schema":{"type":"string","description":"Which phase applies when the location is not an ordered route (single point, station ID, rectangle, or a route that collapses to one point): 'takeoff_landing' (default), 'takeoff', 'cruise' or 'landing'. Case-insensitive. Requires phase_split=true.","example":"takeoff_landing"}},{"name":"risk_signals","in":"query","description":"Comma-separated risk-signal sources to evaluate: 'radar' (live RainViewer precipitation), 'airspace' (OpenAIP regulated airspace), 'gpsjam' (GPS interference cells). Case-insensitive. Omit the parameter for all-on (default). Send 'risk_signals=' (empty) to disable all three.","schema":{"type":"string","description":"Comma-separated risk-signal sources to evaluate: 'radar' (live RainViewer precipitation), 'airspace' (OpenAIP regulated airspace), 'gpsjam' (GPS interference cells). Case-insensitive. Omit the parameter for all-on (default). Send 'risk_signals=' (empty) to disable all three.","example":"radar,airspace,gpsjam","nullable":true}},{"name":"takeoff_max_precipitation","in":"query","description":"Take-off phase override for max_precipitation (mm/h). Inherits max_precipitation when unset. Requires phase_split=true.","schema":{"type":"string","description":"Take-off phase override for max_precipitation (mm/h). Inherits max_precipitation when unset. Requires phase_split=true.","example":"0.2"}},{"name":"takeoff_max_temperature","in":"query","description":"Take-off phase override for max_temperature (deg C). Inherits max_temperature when unset. Requires phase_split=true.","schema":{"type":"string","description":"Take-off phase override for max_temperature (deg C). Inherits max_temperature when unset. Requires phase_split=true.","example":"35"}},{"name":"takeoff_max_wind_gust","in":"query","description":"Take-off phase override for max_wind_gust (m/s, \u003e 0). Inherits max_wind_gust when unset. Requires phase_split=true.","schema":{"type":"string","description":"Take-off phase override for max_wind_gust (m/s, \u003e 0). Inherits max_wind_gust when unset. Requires phase_split=true.","example":"10"}},{"name":"takeoff_max_wind_speed","in":"query","description":"Take-off phase override for max_wind_speed (m/s). Inherits max_wind_speed when unset. Requires phase_split=true.","schema":{"type":"string","description":"Take-off phase override for max_wind_speed (m/s). Inherits max_wind_speed when unset. Requires phase_split=true.","example":"8"}},{"name":"takeoff_min_cloud_base","in":"query","description":"Take-off phase override for min_cloud_base (m). Inherits min_cloud_base when unset. Requires phase_split=true.","schema":{"type":"string","description":"Take-off phase override for min_cloud_base (m). Inherits min_cloud_base when unset. Requires phase_split=true.","example":"400"}},{"name":"takeoff_min_temperature","in":"query","description":"Take-off phase override for min_temperature (deg C). Inherits min_temperature when unset. Requires phase_split=true.","schema":{"type":"string","description":"Take-off phase override for min_temperature (deg C). Inherits min_temperature when unset. Requires phase_split=true.","example":"-5"}},{"name":"takeoff_min_visibility","in":"query","description":"Take-off phase override for min_visibility (km). Inherits min_visibility when unset. Requires phase_split=true.","schema":{"type":"string","description":"Take-off phase override for min_visibility (km). Inherits min_visibility when unset. Requires phase_split=true.","example":"6"}},{"name":"thunderstorm_proximity","in":"query","description":"Minimum distance to thunderstorms in km (optional, case-insensitive key).","schema":{"type":"string","description":"Minimum distance to thunderstorms in km (optional, case-insensitive key).","example":"30"}},{"name":"time","in":"query","description":"Time selection in UTC, RFC 3339 (case-insensitive key and shortcut values).\n\nSupported forms:\n- Single: 2025-07-23T17:56:48Z\n- Range: start,end,resolutionHours\n- List: t1|t2|t3\n- Shortcut: now (current model time).","required":true,"schema":{"type":"string","description":"Time selection in UTC, RFC 3339 (case-insensitive key and shortcut values).\n\nSupported forms:\n- Single: 2025-07-23T17:56:48Z\n- Range: start,end,resolutionHours\n- List: t1|t2|t3\n- Shortcut: now (current model time).","example":"2025-07-23T17:56:48Z"}}],"responses":{"200":{"description":"OK"},"400":{"description":"Bad request (validation error)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"401":{"description":"Gateway identity missing or invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"413":{"description":"Request too large (over the server cap)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}}}}},"/mma/healthz":{"get":{"tags":["Infrastructure"],"summary":"Health check","description":"Liveness probe. Returns `200 {\"status\":\"ok\"}` whenever the HTTP server is accepting connections. Does **not** check upstream gRPC, database, or gateway connectivity — those are surfaced as `502 UPSTREAM_ERROR` on real requests.\n\nNo authentication required.\n\n```bash\ncurl https://api.atmosoar.io/mma/healthz\n```\n\n```json\n{ \"status\": \"ok\" }\n```","operationId":"healthCheck","responses":{"200":{"description":"Service is healthy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerHealthResponse"}}}}}}},"/nowcast/v1":{"get":{"tags":["Nowcast"],"summary":"Run a nowcast (observation-anchored forecast) request","description":"A **nowcast** is a short-range forecast anchored to current real-world observations. `GET /nowcast/v1` returns the same response envelope as `GET /forecast/v1`, with values bias-corrected from ground observations, METAR, and live radar where an observed counterpart exists. Bias is computed as `obs − forecast(T+0)` per location and applied to each cell as `value += bias × decay(lead)`, where `decay` falls linearly to zero at the configured horizon (default 12 h). At long lead times the nowcast converges to the raw forecast.\n\n**Parameter coverage**: every parameter accepted by `GET /forecast/v1` is accepted here. Parameters with an observed counterpart — `temperature_2m`, `wind_speed_10m`, `wind_gusts_10m`, `wind_direction_10m`, `visibility`, `total_precipitation` — receive bias correction. Parameters with no observed counterpart (e.g. `cape`, geopotential heights, upper-air fields) pass through as raw forecast values with no warning — fallback is a contract, not a degradation.\n\n**Bbox**: handled identically to `/forecast` — fine grid expansion, `density` parameter, `MaxGridCells` guard. To keep east-west fan-out bounded, bias is resolved on a coarse 0.5° grid and propagated to every fine cell, so a dense bbox produces at most `(span_deg × 2)²` upstream observation calls rather than one per fine cell.\n\nAll parameter keys and enum values are **case-insensitive**.\n\n### Authentication\nSame as `/forecast/v1`: send your bearer token to the **Atmosoar Gateway**, not directly to MMA. Direct calls return `401 AUTHENTICATION_FAILED`.\n\n### Request parameters\nIdentical to `GET /forecast/v1` (see that route's documentation for the full table). `parameter` also accepts a comma-separated list for one multi-parameter nowcast: each parameter is bias-corrected against its own observed source, parameters with no observed counterpart fall through to raw forecast, and each response cell carries a `parameter` field (single-parameter responses are unchanged). One quota unit regardless of count.\n\n| Param | Description | Examples |\n|-------|-------------|----------|\n| `parameter` | Forecast variable, or a comma-separated list | `temperature_2m` · `temperature_2m,wind_speed_10m,visibility` |\n| `location` | Where to sample | `point:55.75,37.6` · `polyline:...` · `bbox:...` · shortcut |\n| `time` | When to sample (RFC 3339 UTC) | `2026-04-26T12:00:00Z` · range with `/PT1H` resolution |\n| `output` | Response format (optional, defaults to `json`) | `json`, `csv`, `html`, `geojson`, `png`, `grid_png`, `grid_json`, `xml`, `chart_png`, `chart_svg`, `chartspec` |\n| `theme` | Colour mode for the chart formats (optional, defaults to `light`) | `light`, `dark` |\n| `source` | Forecast model (optional; omitted = best available model per cell) | `icon-eu`, `gfs`, etc. |\n| `altitude` | Metres above ground (optional) | `120` |\n| `density` | Grid density for `bbox` (optional) | `10` |\n\n### Happy path — full source coverage\n```bash\ncurl -G https://api.atmosoar.io/nowcast/v1 \\\n -H 'Authorization: Bearer \u003cjwt\u003e' \\\n --data-urlencode 'parameter=temperature_2m' \\\n --data-urlencode 'location=point:55.75,37.6' \\\n --data-urlencode 'time=2026-04-26T12:00:00Z'\n```\n\n```json\n{\n \"httpStatus\": 200,\n \"message\": \"Nowcast request received\",\n \"data\": {\n \"Status\": \"ok\",\n \"Payload\": [\n [\n { \"latitude\": 55.75, \"longitude\": 37.6, \"timestamp\": \"2026-04-26T12:00:00Z\", \"value\": 8.42, \"status\": \"ok\" }\n ]\n ],\n \"warnings\": []\n }\n}\n```\n\n`warnings[]` is always present in the envelope (possibly empty). When every requested cell was satisfied by an observed source, the array is empty.\n\n### Degraded path — far-future timestamp falls back to forecast\n```bash\ncurl -G https://api.atmosoar.io/nowcast/v1 \\\n -H 'Authorization: Bearer \u003cjwt\u003e' \\\n --data-urlencode 'parameter=temperature_2m' \\\n --data-urlencode 'location=point:55.75,37.6' \\\n --data-urlencode 'time=2026-05-04T12:00:00Z'\n```\n\n```json\n{\n \"httpStatus\": 200,\n \"data\": {\n \"Status\": \"ok\",\n \"Payload\": [ [ /* values */ ] ],\n \"warnings\": [\n {\n \"code\": \"NOWCAST_FALLBACK_FORECAST\",\n \"message\": \"Nowcast value unavailable; fell back to forecast\",\n \"lat\": 55.75,\n \"lon\": 37.6\n }\n ]\n }\n}\n```\n\nFallback warnings are deduplicated by ~0.1° coarse cell so a wide bbox or long polyline never floods the warnings array.\n\n### Degraded path — an upstream observation source is unreachable\n```json\n{\n \"httpStatus\": 200,\n \"data\": {\n \"Status\": \"ok\",\n \"Payload\": [ [ /* raw forecast values for affected cells */ ] ],\n \"warnings\": [\n {\n \"code\": \"BIAS_LOOKUP_FAILED_METAR\",\n \"message\": \"METAR observation lookup failed; bias correction skipped for this location\",\n \"lat\": 55.75,\n \"lon\": 37.6\n }\n ]\n }\n}\n```\n\nPer-source codes: `BIAS_LOOKUP_FAILED_METAR`, `BIAS_LOOKUP_FAILED_OBSERVATIONS`, `BIAS_LOOKUP_FAILED_RADAR`. Verbose upstream detail (status codes, response snippets) is logged via Zap only.\n\n### Non-bias-correctable parameter (silent fallback by design)\nParameters like `cape` or geopotential heights have no observed counterpart. `/nowcast` returns raw forecast values with no warning — by contract, that is not a degradation. The five bias-correctable parameters are listed above.\n\n### Bad path — missing required parameter (`400 VALIDATION_ERROR`)\nIdentical to `/forecast/v1`:\n\n```json\n{\n \"httpStatus\": 400,\n \"code\": \"VALIDATION_ERROR\",\n \"errors\": [\n { \"code\": \"MISSING_REQUIRED_FIELD\", \"field\": \"parameter\", \"message\": \"parameter is required\" }\n ]\n}\n```\n\n### Bad path — calling MMA without going through the gateway (`401 AUTHENTICATION_FAILED`)\n```json\n{\n \"httpStatus\": 401,\n \"code\": \"AUTHENTICATION_FAILED\",\n \"message\": \"gateway identity headers missing\"\n}\n```\n\n### Degraded path — weather data ingestor unavailable (`200` + `UPSTREAM_UNAVAILABLE`)\nIf the ingestor is unhealthy, a circuit breaker fails the read fast (rather than hanging on per-call timeouts) and the request is also bounded by an overall wall-clock cap. The same applies when the ingestor sheds load at its concurrent-request cap (\"server at capacity; retry shortly\"). When any of these fire, `/nowcast` returns a valid envelope in degraded mode — an empty payload plus a single request-scoped `UPSTREAM_UNAVAILABLE` warning — instead of a 502 or a multi-minute hang:\n\n```json\n{\n \"httpStatus\": 200,\n \"data\": {\n \"Status\": \"degraded\",\n \"Payload\": [],\n \"warnings\": [\n {\n \"code\": \"UPSTREAM_UNAVAILABLE\",\n \"message\": \"Weather data service is temporarily unavailable; results may be incomplete (degraded mode)\"\n }\n ]\n }\n}\n```\n\nIn degraded mode the response is always `application/json` with HTTP `200`, **regardless of the requested `output` format** (`grid_png`, `grid_json`, `png`, `html`, `chart_png`, `chart_svg`, `chartspec`) — there is no data to render into those formats, and serving JSON guarantees the `UPSTREAM_UNAVAILABLE` warning is delivered (the binary/HTML formatters carry no `warnings[]`).\n\n### Bad path — forecast read fails for a non-availability reason (`502 UPSTREAM_ERROR`)\nErrors that are not an ingestor-availability signal (e.g. a malformed upstream response) are not degraded and still return `502`:\n\n```json\n{\n \"httpStatus\": 502,\n \"code\": \"UPSTREAM_ERROR\",\n \"message\": \"weather data ingestor unavailable\"\n}\n```\n\n### Radar-motion nowcast blend\nAt locations inside a configured radar site window, the radar service's rain-motion nowcast is blended into `precipitation_rate` and `hourly_precipitation` for the next 90 minutes. The radar weight is 1 up to 30 minutes lead, falls linearly to 0 at 90 minutes and is blended with the **raw** model value (`w·radar + (1−w)·model`, rounded to 0.01, never negative). Each timestamp takes the mean radar rate of the hour ending at it, whatever the request's time grid (the current hour, for `time=now`, takes the first hour of the curve); `hourly_precipitation` uses that mean × 1 h. Implausible radar rates are ignored. Within the horizon the radar nowcast replaces the station/radar bias for these two parameters; later timestamps, other parameters and uncovered locations are unchanged. Switch it off per request with `radar_nowcast=off` (v1 query) or `\"radarNowcast\": \"off\"` (v2 body); values are `on` (default) or `off`, case-insensitive, anything else is `400 INVALID_PARAMETER`.\n\nJSON responses carry a top-level `radar_nowcast` array (sibling of `warnings`, absent when no location was blended), one entry per blended location:\n\n```json\n\"radar_nowcast\": [{\n \"latitude\": 43.93, \"longitude\": 10.92,\n \"source\": \"rainviewer\", \"frame_time\": \"2026-10-13T09:50:00Z\", \"frame_age_minutes\": 4,\n \"eta_status\": \"arriving\", \"eta_minutes\": 20,\n \"accumulation_mm\": {\"30\": 0.4, \"60\": 2.1, \"90\": 3.0},\n \"motion\": {\"speed_kmh\": 32.5, \"bearing_deg\": 75, \"confidence\": \"high\"},\n \"fields\": [\"precipitation_rate\", \"hourly_precipitation\"],\n \"warnings\": []\n}]\n```\n\n`eta_minutes`, `motion.speed_kmh` and `motion.bearing_deg` may be `null`; `fields` lists the parameters that were blended; `warnings` passes the radar service's warnings through.\n\nNon-JSON formats (CSV, XML, GeoJSON, HTML, PNG, grid, chart) carry the blended precipitation values but no `radar_nowcast` block.\n\nWarning codes: `RADAR_NOWCAST_UNAVAILABLE` (with `latitude`/`longitude`; the radar nowcast lookup failed, timed out or its breaker is open — values served unblended, HTTP 200) and `RADAR_NOWCAST_SKIPPED_TOO_MANY_LOCATIONS` (request-level, no coordinates; the request expands to more locations than the lookup cap, default 50).","operationId":"runNowcast","parameters":[{"name":"altitude","in":"query","description":"Altitude in meters. Key is case-insensitive.","schema":{"type":"integer","description":"Altitude in meters. Key is case-insensitive.","format":"int32","example":10}},{"name":"density","in":"query","description":"Grid density in points per degree for bbox locations. Overrides the server default. Must be an integer between 1 and 100.","schema":{"type":"integer","description":"Grid density in points per degree for bbox locations. Overrides the server default. Must be an integer between 1 and 100.","format":"int32","example":10}},{"name":"location","in":"query","description":"Location selector (case-insensitive keys and shortcut/station values).\n\nSupported forms:\n- Point: lat,lon (e.g. 50.1109,8.6821)\n- Point list: lat1,lon1|lat2,lon2\n- Polyline: startLat,startLon,endLat,endLon,resolution\n- Rectangle: neLat,neLon|swLat,swLon (NE must be truly northeast of SW). Rectangles are automatically expanded into an adaptive-resolution grid of evenly-spaced points (max 250). The response contains one entry per grid point in a flat list, each labeled lat,lon.\n- Bbox: bbox:west,south,east,north (WGS84, for map overlay grid output).\n- Station ID: e.g. WMO10381 (case-insensitive)\n- Shortcut: predefined, e.g. germany (case-insensitive).","required":true,"schema":{"type":"string","description":"Location selector (case-insensitive keys and shortcut/station values).\n\nSupported forms:\n- Point: lat,lon (e.g. 50.1109,8.6821)\n- Point list: lat1,lon1|lat2,lon2\n- Polyline: startLat,startLon,endLat,endLon,resolution\n- Rectangle: neLat,neLon|swLat,swLon (NE must be truly northeast of SW). Rectangles are automatically expanded into an adaptive-resolution grid of evenly-spaced points (max 250). The response contains one entry per grid point in a flat list, each labeled lat,lon.\n- Bbox: bbox:west,south,east,north (WGS84, for map overlay grid output).\n- Station ID: e.g. WMO10381 (case-insensitive)\n- Shortcut: predefined, e.g. germany (case-insensitive).","example":"50.1109,8.6821"}},{"name":"output","in":"query","description":"Response format (case-insensitive). Defaults to json when omitted.\n\nChart formats render the forecast as a time series (one location, one parameter), a meteogram (several parameters) or one line per location:\n- chart_png: rendered chart as PNG\n- chart_svg: the same chart as SVG\n- chartspec: the chart definition as JSON, including the resolved palette, for a browser to draw","schema":{"type":"string","description":"Response format (case-insensitive). Defaults to json when omitted.\n\nChart formats render the forecast as a time series (one location, one parameter), a meteogram (several parameters) or one line per location:\n- chart_png: rendered chart as PNG\n- chart_svg: the same chart as SVG\n- chartspec: the chart definition as JSON, including the resolved palette, for a browser to draw","default":"json","example":"json","enum":["json","csv","xml","geojson","html","png","grid_json","grid_png","chart_png","chart_svg","chartspec"]}},{"name":"parameter","in":"query","description":"Forecast variable, or a comma-separated list of variables for a single multi-parameter call (e.g. temperature_2m,wind_speed_10m,visibility). Keys and values are case-insensitive; duplicates are de-duplicated. Up to max_parameters_per_request (default 10) distinct parameters per call.","required":true,"schema":{"type":"string","description":"Forecast variable, or a comma-separated list of variables for a single multi-parameter call (e.g. temperature_2m,wind_speed_10m,visibility). Keys and values are case-insensitive; duplicates are de-duplicated. Up to max_parameters_per_request (default 10) distinct parameters per call.","example":"temperature_2m"}},{"name":"radar_nowcast","in":"query","description":"/nowcast only (ignored by /forecast). Radar-motion nowcast blend, on or off (case-insensitive): on (default) blends the radar service's rain-motion nowcast into precipitation_rate and hourly_precipitation for the next 90 minutes at locations inside a configured radar site window and adds the radar_nowcast block to JSON responses; off serves the precipitation values without the blend. precipitation_rate (mm/h) is a /nowcast-only parameter: its model baseline is the hour's precipitation, refined by station and radar rates. Any other value returns 400 INVALID_PARAMETER.","schema":{"type":"string","description":"/nowcast only (ignored by /forecast). Radar-motion nowcast blend, on or off (case-insensitive): on (default) blends the radar service's rain-motion nowcast into precipitation_rate and hourly_precipitation for the next 90 minutes at locations inside a configured radar site window and adds the radar_nowcast block to JSON responses; off serves the precipitation values without the blend. precipitation_rate (mm/h) is a /nowcast-only parameter: its model baseline is the hour's precipitation, refined by station and radar rates. Any other value returns 400 INVALID_PARAMETER.","default":"on","example":"off"}},{"name":"source","in":"query","description":"Forecast model to serve from (case-insensitive, optional). When set, every value comes from exactly this model; a cell the model cannot serve (outside its domain, beyond its forecast range, or no data yet) is returned with a null value and an error instead of falling back to another model. When omitted, each cell is served by the best available model for its location and lead time.","schema":{"type":"string","description":"Forecast model to serve from (case-insensitive, optional). When set, every value comes from exactly this model; a cell the model cannot serve (outside its domain, beyond its forecast range, or no data yet) is returned with a null value and an error instead of falling back to another model. When omitted, each cell is served by the best available model for its location and lead time.","example":"icon-eu","enum":["icon-global","icon-eu","icon-d2","icon-d2-ruc","ecmwf-ifs","gfs","arpege"]}},{"name":"theme","in":"query","description":"Colour mode for the chart formats (case-insensitive): light (default) or dark. Ignored by every other output format.","schema":{"type":"string","description":"Colour mode for the chart formats (case-insensitive): light (default) or dark. Ignored by every other output format.","default":"light","example":"dark","enum":["light","dark"]}},{"name":"time","in":"query","description":"Time selection in UTC, RFC 3339 (case-insensitive keys and shortcut values).\n\nSupported forms:\n- Single: 2025-07-23T17:56:48Z\n- Range: start,end,resolutionHours (e.g. 2025-07-23T00:00:00Z,2025-07-23T12:00:00Z,3)\n- List: t1|t2|t3\n- Shortcut: now (current model time), 3day (72h forecast window), 7day (168h forecast window), 10day (240h forecast window).","required":true,"schema":{"type":"string","description":"Time selection in UTC, RFC 3339 (case-insensitive keys and shortcut values).\n\nSupported forms:\n- Single: 2025-07-23T17:56:48Z\n- Range: start,end,resolutionHours (e.g. 2025-07-23T00:00:00Z,2025-07-23T12:00:00Z,3)\n- List: t1|t2|t3\n- Shortcut: now (current model time), 3day (72h forecast window), 7day (168h forecast window), 10day (240h forecast window).","example":"2025-07-23T17:56:48Z"}}],"responses":{"200":{"description":"Nowcast request received","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsResponseServer"}}}},"202":{"description":"Accepted"},"400":{"description":"Bad request (validation error)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"401":{"description":"Gateway identity missing or invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"413":{"description":"Request too large (over the server cap)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"502":{"description":"Upstream data source error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}}}}},"/nowcast/v2":{"post":{"tags":["Nowcast"],"summary":"Run a nowcast (observation-anchored forecast) request (POST)","description":"Same bias-correction engine as `GET /nowcast/v1`, accepting **POST** with a JSON body. Field names are camelCase (`timeRange`, `outputFormat`, `modelSource`); request schema is byte-identical to `POST /forecast/v2`. Accepts every parameter `POST /forecast/v2` accepts, including a comma-separated `parameter` list for one multi-parameter nowcast (each cell then carries a `parameter` field; one quota unit regardless of count). Bias-correctable parameters are corrected, all others pass through as raw forecast values.\n\nAs with `POST /forecast/v2`, `location` and `timeRange` accept either a compact string (`point:55.75,37.6`, `2026-04-26T00:00:00Z/2026-04-27T00:00:00Z/PT1H`) or a structured `{type,payload}` object — see that route for the full format table.\n\n### Happy path\n```bash\ncurl -X POST https://api.atmosoar.io/nowcast/v2 \\\n -H 'Authorization: Bearer \u003cjwt\u003e' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"parameter\": \"wind_speed_10m\",\n \"location\": \"point:55.75,37.6\",\n \"timeRange\": \"2026-04-26T00:00:00Z/2026-04-27T00:00:00Z/PT1H\",\n \"outputFormat\": \"json\"\n }'\n```\n\n```json\n{\n \"httpStatus\": 200,\n \"data\": {\n \"Status\": \"ok\",\n \"Payload\": [ /* per-timestamp bias-corrected values */ ],\n \"warnings\": []\n }\n}\n```\n\nAll warning codes and degraded paths from `GET /nowcast/v1` apply. See that route's documentation for the full degraded-response examples.\n\n### Radar-motion nowcast blend\nAt locations inside a configured radar site window, the radar service's rain-motion nowcast is blended into `precipitation_rate` and `hourly_precipitation` for the next 90 minutes. The radar weight is 1 up to 30 minutes lead, falls linearly to 0 at 90 minutes and is blended with the **raw** model value (`w·radar + (1−w)·model`, rounded to 0.01, never negative). Each timestamp takes the mean radar rate of the hour ending at it, whatever the request's time grid (the current hour, for `time=now`, takes the first hour of the curve); `hourly_precipitation` uses that mean × 1 h. Implausible radar rates are ignored. Within the horizon the radar nowcast replaces the station/radar bias for these two parameters; later timestamps, other parameters and uncovered locations are unchanged. Switch it off per request with `radar_nowcast=off` (v1 query) or `\"radarNowcast\": \"off\"` (v2 body); values are `on` (default) or `off`, case-insensitive, anything else is `400 INVALID_PARAMETER`.\n\nJSON responses carry a top-level `radar_nowcast` array (sibling of `warnings`, absent when no location was blended), one entry per blended location:\n\n```json\n\"radar_nowcast\": [{\n \"latitude\": 43.93, \"longitude\": 10.92,\n \"source\": \"rainviewer\", \"frame_time\": \"2026-10-13T09:50:00Z\", \"frame_age_minutes\": 4,\n \"eta_status\": \"arriving\", \"eta_minutes\": 20,\n \"accumulation_mm\": {\"30\": 0.4, \"60\": 2.1, \"90\": 3.0},\n \"motion\": {\"speed_kmh\": 32.5, \"bearing_deg\": 75, \"confidence\": \"high\"},\n \"fields\": [\"precipitation_rate\", \"hourly_precipitation\"],\n \"warnings\": []\n}]\n```\n\n`eta_minutes`, `motion.speed_kmh` and `motion.bearing_deg` may be `null`; `fields` lists the parameters that were blended; `warnings` passes the radar service's warnings through.\n\nNon-JSON formats (CSV, XML, GeoJSON, HTML, PNG, grid, chart) carry the blended precipitation values but no `radar_nowcast` block.\n\nWarning codes: `RADAR_NOWCAST_UNAVAILABLE` (with `latitude`/`longitude`; the radar nowcast lookup failed, timed out or its breaker is open — values served unblended, HTTP 200) and `RADAR_NOWCAST_SKIPPED_TOO_MANY_LOCATIONS` (request-level, no coordinates; the request expands to more locations than the lookup cap, default 50).","operationId":"runNowcastPost","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunNowcastPostInput"}}}},"responses":{"200":{"description":"Nowcast request received","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsResponseServer"}}}},"202":{"description":"Accepted"},"400":{"description":"Bad request (validation error)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"401":{"description":"Gateway identity missing or invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"413":{"description":"Request too large (over the server cap)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"502":{"description":"Upstream data source error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}}}}}},"components":{"schemas":{"HttputilsAPIError":{"type":"object","properties":{"code":{"type":"string"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/HttputilsFieldError"}},"httpStatus":{"type":"integer","format":"int32"},"message":{"type":"string"}}},"HttputilsFieldError":{"type":"object","properties":{"code":{"type":"string"},"field":{"type":"string"},"message":{"type":"string"}}},"HttputilsResponseServer":{"type":"object","properties":{"data":{"description":"Value of any type, including null","nullable":true},"httpStatus":{"type":"integer","format":"int32"},"message":{"type":"string"}}},"LocationLocation":{"type":"object","properties":{"payload":{"description":"Value of any type, including null","nullable":true},"type":{"type":"string"}}},"ModelsGridMeta":{"type":"object","properties":{"cols":{"type":"integer","format":"int32"},"east":{"type":"number","format":"double"},"lat_step":{"type":"number","format":"double"},"lon_step":{"type":"number","format":"double"},"north":{"type":"number","format":"double"},"rows":{"type":"integer","format":"int32"},"south":{"type":"number","format":"double"},"west":{"type":"number","format":"double"}}},"RunForecastPostInput":{"type":"object","properties":{"altitude":{"type":"integer","format":"int32"},"density":{"type":"integer","description":"Grid density in points per degree for bbox locations; overrides the bbox payload's own density and the server default. Must be an integer between 1 and 100.","format":"int32"},"grid":{"$ref":"#/components/schemas/ModelsGridMeta"},"location":{"$ref":"#/components/schemas/LocationLocation"},"modelSource":{"type":"string"},"outputFormat":{"type":"string"},"parameter":{"type":"string"},"parameters":{"type":"array","items":{"type":"string"}},"radarNowcast":{"type":"string"},"theme":{"type":"string"},"timeRange":{"$ref":"#/components/schemas/TimeTimeRange"}},"required":["outputFormat","parameter","timeRange"]},"RunNowcastPostInput":{"type":"object","properties":{"altitude":{"type":"integer","format":"int32"},"density":{"type":"integer","description":"Grid density in points per degree for bbox locations; overrides the bbox payload's own density and the server default. Must be an integer between 1 and 100.","format":"int32"},"grid":{"$ref":"#/components/schemas/ModelsGridMeta"},"location":{"$ref":"#/components/schemas/LocationLocation"},"modelSource":{"type":"string"},"outputFormat":{"type":"string"},"parameter":{"type":"string"},"parameters":{"type":"array","items":{"type":"string"}},"radarNowcast":{"type":"string"},"theme":{"type":"string"},"timeRange":{"$ref":"#/components/schemas/TimeTimeRange"}},"required":["outputFormat","parameter","timeRange"]},"ServerHealthResponse":{"type":"object","properties":{"status":{"type":"string","example":"ok"}}},"TimeTimeRange":{"type":"object","properties":{"payload":{"description":"Value of any type, including null","nullable":true},"type":{"type":"string"}}}}},"tags":[{"name":"Forecast","description":"Forecast endpoints"},{"name":"Impact","description":"Impact analysis endpoints"},{"name":"Infrastructure","description":"Infrastructure endpoints (health)"},{"name":"Nowcast","description":"Observation-anchored short-range forecast"}]}