{"openapi":"3.0.1","info":{"title":"Foehn API","description":"Foehn forecast for the Alpine arc. For each axis (a pair of points across the Alps) the service tracks the NOAA GFS mean-sea-level pressure difference `a - b` over the next seven days, sourced from the Atmosoar Multi-Model API. A difference of about 4 hPa brings Foehn to Alpine valleys; beyond 8 hPa it often reaches the lowlands.\n\n## Response envelope\n\nJSON responses use the shared Atmosoar envelope `{httpStatus, message, data}`; errors use `{httpStatus, code, message, errors[]}`. Chart routes return `image/png` with `Last-Modified` set to the render time.","version":"1.6.0"},"paths":{"/foehn/api/axes":{"get":{"tags":["Foehn"],"summary":"List Foehn axes","description":"Returns the enabled axes in display order with both endpoints' names and coordinates.","operationId":"listAxes","responses":{"200":{"description":"Axes"},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}}}}},"/foehn/api/axes/{slug}/chart.png":{"get":{"tags":["Foehn"],"summary":"Rendered chart for an axis","description":"Returns the newest rendered chart as `image/png`. `Last-Modified` is the render time; `X-Foehn-Run-Id` identifies the run it was drawn from. `If-Modified-Since` is answered with `304`.","operationId":"getChart","parameters":[{"name":"slug","in":"path","description":"Axis slug or legacy chart name.","required":true,"schema":{"type":"string","description":"Axis slug or legacy chart name.","example":"innsbruck-bolzano"}},{"name":"variant","in":"query","description":"Chart layout: desktop (2500x1800) or mobile.","schema":{"type":"string","description":"Chart layout: desktop (2500x1800) or mobile.","default":"desktop","enum":["desktop","mobile"]}}],"responses":{"200":{"description":"PNG chart"},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"404":{"description":"Unknown axis, or no chart rendered yet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}}}}},"/foehn/api/axes/{slug}/history":{"get":{"tags":["Foehn"],"summary":"Historical runs for an axis","description":"Returns runs fetched within the requested window, newest first, each with its points.","operationId":"getSeriesHistory","parameters":[{"name":"slug","in":"path","description":"Axis slug or legacy chart name.","required":true,"schema":{"type":"string","description":"Axis slug or legacy chart name.","example":"innsbruck-bolzano"}},{"name":"limit","in":"query","description":"Maximum number of runs (positive integer, capped at 200).","schema":{"type":"string","description":"Maximum number of runs (positive integer, capped at 200).","default":"20"}},{"name":"time","in":"query","description":"Fetch-time window: single RFC 3339 timestamp, range 'start,end', or list 't1|t2'. Defaults to the last 48 hours.","schema":{"type":"string","description":"Fetch-time window: single RFC 3339 timestamp, range 'start,end', or list 't1|t2'. Defaults to the last 48 hours.","example":"2026-09-10T00:00:00Z,2026-09-12T00:00:00Z"}}],"responses":{"200":{"description":"Runs"},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"404":{"description":"Unknown axis.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}}}}},"/foehn/api/axes/{slug}/latest":{"get":{"tags":["Foehn"],"summary":"Latest pressure-difference series for an axis","description":"Returns the newest run: 57 three-hourly points over seven days with both pressures and their difference, plus the live Foehn thresholds used by the rendered charts.\n\n### Charts — `format=chart_png|chart_svg|chartspec`\n\nThe same run can come back as a chart, rendered from the platform's shared chart definition (`atmosoar-api-libraries/chart`). Colours, units, axis ticks and threshold bands match every other Atmosoar chart of the same parameter, so an axis chart and a Multi-Model API pressure chart read as one system. `chartspec` returns the chart definition as JSON — series, axes, ticks, bands and the resolved palette — so a frontend draws the identical chart with its own library. `theme=dark` selects the dark palette.\n\nThe two Foehn thresholds are shaded on **both** sides of zero: a positive difference is north Foehn, a negative one south Foehn, and the y axis is framed symmetrically (at least ±20 hPa) so neither looks like the smaller phenomenon. The bands follow the live runtime thresholds, so they move when an operator retunes them.\n\n**This is not the hotlinked PNG.** `GET chart.png?variant=desktop|mobile` keeps serving the worker-rendered legacy charts that `models.atmosoar.io` publishes and external sites embed; those are unchanged and stay byte-identical in look. Use the chart formats here for dashboards and new consumers.\n\nChart responses carry an `ETag` and `Cache-Control: private, max-age=300`; send the ETag back as `If-None-Match` to get `304 Not Modified` until the run, the thresholds or the axis change.\n\n```bash\n# the shared chart, dark\ncurl -H \"Authorization: Bearer $TOKEN\" -o axis.svg \\\n  'https://api.atmosoar.io/foehn/api/axes/innsbruck-bolzano/latest?format=chart_svg\u0026theme=dark'\n```","operationId":"getLatestSeries","parameters":[{"name":"slug","in":"path","description":"Axis slug (e.g. innsbruck-bolzano) or its legacy chart name (e.g. innsbruckbolzano).","required":true,"schema":{"type":"string","description":"Axis slug (e.g. innsbruck-bolzano) or its legacy chart name (e.g. innsbruckbolzano).","example":"innsbruck-bolzano"}},{"name":"format","in":"query","description":"Render the run as a chart instead of JSON. One of chart_png (PNG), chart_svg (SVG), chartspec (the chart definition as JSON, for a frontend to draw itself). Omitted, the response is the usual JSON payload. This is the shared Atmosoar chart, not the legacy hotlinked PNG — for that, use chart.png.","schema":{"type":"string","description":"Render the run as a chart instead of JSON. One of chart_png (PNG), chart_svg (SVG), chartspec (the chart definition as JSON, for a frontend to draw itself). Omitted, the response is the usual JSON payload. This is the shared Atmosoar chart, not the legacy hotlinked PNG — for that, use chart.png.","example":"chart_svg"}},{"name":"theme","in":"query","description":"Colour mode for the chart formats: light (default) or dark. Ignored when 'format' is omitted.","schema":{"type":"string","description":"Colour mode for the chart formats: light (default) or dark. Ignored when 'format' is omitted.","default":"light","example":"dark"}}],"responses":{"200":{"description":"Latest series"},"404":{"description":"Unknown axis, or no run fetched yet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}}}}},"/foehn/api/status":{"get":{"tags":["Foehn"],"summary":"Data freshness","description":"Per-axis fetch bookkeeping. `status` is `ok` when every enabled axis fetched successfully within `stale_after_seconds`, otherwise `stale`.","operationId":"getStatus","responses":{"200":{"description":"Freshness report"},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}}}}},"/foehn/healthz":{"get":{"tags":["Health"],"summary":"Liveness probe","description":"Returns `200 {\"status\":\"ok\"}` while the process is responsive. Checks no dependencies.","operationId":"healthLive","responses":{"200":{"description":"Service is alive"}}}},"/foehn/readyz":{"get":{"tags":["Health"],"summary":"Readiness probe","description":"Returns `200` when the database is reachable, `503` otherwise. Data freshness is reported by `/foehn/api/status`, not here, so stale upstream data never stops the last good charts from being served.","operationId":"healthReady","responses":{"200":{"description":"Service is ready"},"503":{"description":"Database unreachable."}}}}},"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"}}}}},"tags":[{"name":"Foehn","description":"Foehn forecast axes, pressure-difference series and rendered charts"},{"name":"Health","description":"Health and readiness probes"}]}