{"openapi":"3.0.1","info":{"title":"Observations API","description":"The **Atmosoar Observations API** delivers normalized, near-real-time surface weather observations from a unified global feed of operational meteorological networks (DWD Germany, DWD International, NCEP SYNOP, NCEP METAR, and Ogimet). Heterogeneous source formats — BUFR, SYNOP, METAR — are decoded server-side into a single, stable JSON schema so client applications consume one consistent observation model regardless of upstream provider.\n\n## Authentication\n\nAll data endpoints (`/observations`, `/observations/latest`, `/observations/stations`, `/observations/stations/{id}`) are served behind the **atmosoar-gateway**, which authenticates the caller and forwards identity headers (`X-Atmosoar-User`, `X-Atmosoar-Tenant`, `X-Atmosoar-Roles`). Direct calls to this service that bypass the gateway are rejected with `401 Unauthorized`. Operational endpoints (`/healthz`, `/metrics`, `/docs`, `/openapi.json`) are public.\n\n## Shared query contract\n\nThe `time` and `location` query parameters are provided by [atmosoar-api-libraries](https://github.com/Atmosoar/atmosoar-api-libraries) and behave identically across every Atmosoar service.\n\n### `time` parameter\n\n- **RFC3339 single** — `2026-04-13T12:00:00Z` — exact-match UTC timestamp.\n- **Named shortcuts** (`now`, `3day`, `7day`) are **not** registered by this service: they return `400 INVALID_TIME_FORMAT`. Pass an explicit range instead.\n- **Range** — `2026-04-10T00:00:00Z,2026-04-13T00:00:00Z` — closed interval `[start, end]`, both RFC3339 UTC.\n- **Range with resolution** — `start,end,N` where `N` is a positive **integer count of segments** (NOT a duration like `1h`). The server returns `N+1` evenly-spaced sample timestamps from `start` to `end`, inclusive.\n- **List** — pipe-separated RFC3339 UTC timestamps, e.g. `2026-04-13T00:00:00Z|2026-04-13T12:00:00Z` — exact match against any listed timestamp.\n\n### `location` parameter\n\nAll coordinates are decimal degrees, WGS84. Latitude must lie in `[-90, 90]`, longitude in `[-180, 180]`.\n\n- **Point** — `lat,lon`, e.g. `52.52,13.40`. Resolved as a 25 km radius search.\n- **Radius** — `lat,lon|R`, e.g. `52.52,13.40|50`. `R` is the search radius **in kilometres** (max `200`; a larger radius is rejected with `400 INVALID_LOCATION`).\n- **Bounding box** — `bbox:W,S,E,N`, e.g. `bbox:12.0,51.0,15.0,54.0`. West \u003c east, south \u003c north (no antimeridian wrap).\n- **Rectangle** — `NElat,NElon|SWlat,SWlon`, e.g. `54.0,15.0|51.0,12.0`, NE corner first. With the `rectangle:` prefix (`rectangle:NElat,NElon|SWlat,SWlon` or `rectangle:S,W,N,E`) swapped corners are normalised server-side. A box crossing the antimeridian (NE longitude west of the SW one by more than 180 degrees) is rejected (no antimeridian wrap).\n- **Shortcut** — `germany`, `EDDB` — registered country code or station identifier.\n\n## Units\n\nMeasurement columns are normalised at ingest so a single column carries one unit regardless of upstream provider. Temperatures °C, percentages 0..100, snow depths cm, radiation J/m², sunshine duration h, distances/heights m, wind speeds m/s, wind direction degrees true, pressures hPa, coordinates decimal degrees WGS84. ICAO METAR sentinel: `visibility=9999` means \"10 km or more\". `precipitation` is the raw accumulation total in mm (per-report-period sum OR standing snowpack water-equivalent — NOT a rate); `precipitation_rate` (mm/h) is the instantaneous rate derived from the report's `precipitation_period_hours` window and is null when the window is unknown or the rate is non-physical — read it, not `precipitation`, when you need a rate.\n\n## Field selection\n\nPass `fields=temperature_2m,wind_speed,mslp` to receive only those measurement fields plus a fixed identity bundle (`id`, `station_name`, `wmo_id`, `icao_id`, `timestamp`, `lat`, `lon`, `elevation`, `source`, `ingested_at`). Omit `fields` to receive the full observation schema.\n\n## Errors\n\nEvery error response uses the shared `httputils.APIError` envelope:\n\n```json\n{\n  \"httpStatus\": 400,\n  \"code\": \"INVALID_TIME_FORMAT\",\n  \"message\": \"Invalid time parameter.\",\n  \"errors\": [\n    { \"code\": \"INVALID_TIME_FORMAT\", \"field\": \"time\", \"message\": \"could not parse 'tomorrow'\" }\n  ]\n}\n```\n\nTyped codes you may encounter: `INVALID_TIME_FORMAT`, `INVALID_LOCATION`, `INVALID_PARAMETER`, `NOT_FOUND`, `INTERNAL_ERROR`.","contact":{"name":"Atmosoar Support","url":"https://atmosoar.io"},"version":"1.12.0","x-logo":{"url":"https://atmosoar.io/logo.png","backgroundColor":"#003b48","altText":"atmosoar"}},"paths":{"/observations":{"get":{"tags":["observations"],"summary":"Query observations","description":"Returns surface weather observations filtered by any combination of station identity (`wmo_id`, `icao_id`), time window (`time`), spatial filter (`location`), and upstream provider (`source`). Results are sorted by timestamp descending and capped by `limit` (default 1000, hard max 5000; values above the cap fall back to the default rather than silently truncating). Pagination is offset-based via `offset`. Use `fields` to project a subset of measurement columns.\n\n**Default time window for area queries:** when `location` is supplied without a `time` filter, the query is bounded to the most recent **24 hours**. Pass an explicit `time` range to query further back. A query that still exceeds the server-side statement budget returns `503` with guidance to narrow it.\n\n### Units\n\nAll measurement columns are normalised at ingest. Temperatures are °C, pressures hPa, wind speeds m/s, distances/heights m (visibility uses the ICAO METAR sentinel `9999` to mean \"≥10 km\"), snow depths cm, cloud cover %, sunshine duration h, radiation J/m², wind direction degrees true. `precipitation` is the raw accumulation total in mm (a per-report-period sum OR a standing snowpack water-equivalent — NOT a rate); `precipitation_rate` is the instantaneous mm/h rate derived from the report's stated `precipitation_period_hours` window, null when that window is unknown or the rate is non-physical. Consumers that need a rate must read `precipitation_rate`. See each field's response schema description for the authoritative unit.\n\n### Happy path — a 3-day window within 50 km of Berlin\n\n```bash\ncurl -H \"Authorization: Bearer $ATMOSOAR_TOKEN\" --get \\\n  'https://api.atmosoar.io/observations' \\\n  --data-urlencode 'location=52.52,13.40|50' \\\n  --data-urlencode 'time=2026-04-22T00:00:00Z,2026-04-25T00:00:00Z' \\\n  --data-urlencode 'fields=temperature_2m,mslp,wind_speed'\n```\n\nSample `200 OK` response (truncated):\n\n```json\n[\n  {\n    \"id\": 9128471,\n    \"station_name\": \"Berlin-Tempelhof\",\n    \"wmo_id\": \"10384\",\n    \"icao_id\": \"EDDI\",\n    \"timestamp\": \"2026-04-25T18:00:00Z\",\n    \"lat\": 52.4675,\n    \"lon\": 13.4021,\n    \"elevation\": 48.0,\n    \"source\": \"dwd_germany\",\n    \"ingested_at\": \"2026-04-25T18:04:11Z\",\n    \"temperature_2m\": 11.4,\n    \"mslp\": 1015.2,\n    \"wind_speed\": 4.6\n  }\n]\n```\n\n### Charts — `format=chart_png|chart_svg|chartspec`\n\nThe same result set can come back as a chart instead of JSON. All three encodings are rendered from one shared chart definition, so an observation chart carries the same colours, units, axis ticks and threshold bands as a Multi-Model API forecast chart of the same parameter — they read as one system.\n\nThe columns drawn are the ones `fields` already names, in that order (temperature when `fields` is omitted), and the form follows the query:\n\n| Query | Chart |\n|-------|-------|\n| one station, one field | a time series, with that parameter's operational bands shaded |\n| one station, several fields | a meteogram — one panel per field over a shared time axis |\n| several stations, one field | a time series with one line per station (first 8; the subtitle says so) |\n| several stations, several fields | a meteogram for the first station, named in the subtitle |\n\n`chart=windrose` draws the direction-and-speed distribution instead: it reads `wind_speed` and `wind_direction`, bins them into 16 compass sectors and six speed classes, counts readings below 0.5 m/s as calm, and pools every station the query matched (the subtitle says how many). `chartspec` returns the chart as JSON — series, axes, ticks, bands and the resolved palette — so a frontend draws the identical chart with its own charting library. `theme=dark` selects the dark palette, which is its own set of colour steps rather than an inversion.\n\nA row that does not report a column becomes a gap in the line, never a zero. At most 8 fields per chart — the categorical palette has eight slots and is never cycled.\n\n```bash\n# a day of wind at one station, as SVG\ncurl -H \"Authorization: Bearer $ATMOSOAR_TOKEN\" --get \\\n  'https://api.atmosoar.io/observations' \\\n  --data-urlencode 'wmo_id=10384' \\\n  --data-urlencode 'time=2026-04-24T00:00:00Z,2026-04-25T00:00:00Z' \\\n  --data-urlencode 'fields=wind_speed,wind_gust' \\\n  --data-urlencode 'format=chart_svg' -o wind.svg\n\n# the wind rose for the week up to the same day\ncurl -H \"Authorization: Bearer $ATMOSOAR_TOKEN\" --get \\\n  'https://api.atmosoar.io/observations' \\\n  --data-urlencode 'wmo_id=10384' \\\n  --data-urlencode 'time=2026-04-18T00:00:00Z,2026-04-25T00:00:00Z' \\\n  --data-urlencode 'chart=windrose' \\\n  --data-urlencode 'format=chart_png' \\\n  --data-urlencode 'theme=dark' -o rose.png\n```\n\n### Happy path — list of explicit timestamps\n\n```bash\ncurl -H \"Authorization: Bearer $ATMOSOAR_TOKEN\" --get \\\n  'https://api.atmosoar.io/observations' \\\n  --data-urlencode 'wmo_id=10382' \\\n  --data-urlencode 'time=2026-04-13T00:00:00Z|2026-04-13T12:00:00Z'\n```\n\n### Bad path — malformed `time` (`400 INVALID_TIME_FORMAT`)\n\n```json\n{\n  \"httpStatus\": 400,\n  \"code\": \"INVALID_TIME_FORMAT\",\n  \"message\": \"Invalid time parameter.\",\n  \"errors\": [\n    { \"code\": \"INVALID_TIME_FORMAT\", \"field\": \"time\", \"message\": \"could not parse 'tomorrow'\" }\n  ]\n}\n```\n\n### Bad path — out-of-range latitude (`400 INVALID_LOCATION`)\n\n```json\n{\n  \"httpStatus\": 400,\n  \"code\": \"INVALID_LOCATION\",\n  \"message\": \"Invalid location parameter.\",\n  \"errors\": [\n    { \"code\": \"INVALID_LOCATION\", \"field\": \"lat\", \"message\": \"must be between -90 and 90\" }\n  ]\n}\n```\n\n### Bad path — internal error (`500 INTERNAL_ERROR`)\n\nReturned when the underlying datastore is unavailable:\n\n```json\n{ \"httpStatus\": 500, \"code\": \"INTERNAL_ERROR\", \"message\": \"An unexpected error occurred.\" }\n```","operationId":"queryObservations","parameters":[{"name":"chart","in":"query","description":"Chart form, for the chart formats. One of timeseries, meteogram, windrose. Omitted, the form follows the fields: one field is a time series, several are a meteogram. A windrose reads wind_speed and wind_direction and ignores 'fields'.","schema":{"type":"string","description":"Chart form, for the chart formats. One of timeseries, meteogram, windrose. Omitted, the form follows the fields: one field is a time series, several are a meteogram. A windrose reads wind_speed and wind_direction and ignores 'fields'.","example":"windrose"}},{"name":"fields","in":"query","description":"Comma-separated measurement-column projection. Identity fields (id, station_name, wmo_id, icao_id, timestamp, lat, lon, elevation, source, ingested_at) are always included. Unknown names produce 400.","schema":{"type":"string","description":"Comma-separated measurement-column projection. Identity fields (id, station_name, wmo_id, icao_id, timestamp, lat, lon, elevation, source, ingested_at) are always included. Unknown names produce 400.","example":"temperature_2m,wind_speed,mslp"}},{"name":"format","in":"query","description":"Render the result 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 array.","schema":{"type":"string","description":"Render the result 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 array.","example":"chart_svg"}},{"name":"icao_id","in":"query","description":"ICAO 4-letter station identifier.","schema":{"type":"string","description":"ICAO 4-letter station identifier.","example":"EDDB"}},{"name":"limit","in":"query","description":"Max rows returned (integer). Hard cap 5000; values above 5000 fall back to the default 1000.","schema":{"type":"string","description":"Max rows returned (integer). Hard cap 5000; values above 5000 fall back to the default 1000.","default":"1000"}},{"name":"location","in":"query","description":"Location filter. Forms: 'lat,lon' (point — degrees WGS84, implicit 25 km radius); 'lat,lon|R' (radius — R is in kilometres, max 200); 'bbox:W,S,E,N' (bounding box — degrees WGS84, west \u003c east, south \u003c north); 'NElat,NElon|SWlat,SWlon' (rectangle — degrees WGS84); or a registered shortcut name (e.g. 'germany', 'EDDB'). Without an explicit 'time' filter, area queries are bounded to the most recent 24 hours.","schema":{"type":"string","description":"Location filter. Forms: 'lat,lon' (point — degrees WGS84, implicit 25 km radius); 'lat,lon|R' (radius — R is in kilometres, max 200); 'bbox:W,S,E,N' (bounding box — degrees WGS84, west \u003c east, south \u003c north); 'NElat,NElon|SWlat,SWlon' (rectangle — degrees WGS84); or a registered shortcut name (e.g. 'germany', 'EDDB'). Without an explicit 'time' filter, area queries are bounded to the most recent 24 hours.","example":"52.52,13.40|50"}},{"name":"offset","in":"query","description":"Pagination offset (integer row count).","schema":{"type":"string","description":"Pagination offset (integer row count).","default":"0"}},{"name":"source","in":"query","description":"Upstream provider filter. One of: dwd_germany, dwd_international, ncep_synop, ncep_metar, ogimet.","schema":{"type":"string","description":"Upstream provider filter. One of: dwd_germany, dwd_international, ncep_synop, ncep_metar, ogimet.","example":"dwd_germany"}},{"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"}},{"name":"time","in":"query","description":"Time filter. RFC3339 UTC. Forms: single timestamp '2026-04-13T12:00:00Z'; range 'start,end' (closed interval, both RFC3339); range with resolution 'start,end,N' where N is a positive integer count of segments yielding N+1 sample timestamps (NOT a duration like '1h'); or pipe-separated list 'T1|T2|...' for exact match. Named shortcuts such as '3day' are not registered by this service and return 400 INVALID_TIME_FORMAT.","schema":{"type":"string","description":"Time filter. RFC3339 UTC. Forms: single timestamp '2026-04-13T12:00:00Z'; range 'start,end' (closed interval, both RFC3339); range with resolution 'start,end,N' where N is a positive integer count of segments yielding N+1 sample timestamps (NOT a duration like '1h'); or pipe-separated list 'T1|T2|...' for exact match. Named shortcuts such as '3day' are not registered by this service and return 400 INVALID_TIME_FORMAT.","example":"2026-04-10T00:00:00Z,2026-04-13T00:00:00Z"}},{"name":"wmo_id","in":"query","description":"WMO 5-digit station identifier.","schema":{"type":"string","description":"WMO 5-digit station identifier.","example":"10382"}}],"responses":{"200":{"description":"Matching observations, newest first (JSON array of observation objects; identity fields plus any requested measurement columns)."},"400":{"description":"Invalid query parameter (time, location, source, fields).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"503":{"description":"Query timed out; narrow time, location, or source.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}}}}},"/observations/healthz":{"get":{"tags":["operational"],"summary":"Health check","description":"Liveness probe used by the platform (Kubernetes, load balancers, uptime monitors). Always returns `200 OK` with a fixed `{\"status\":\"ok\"}` body when the HTTP server is accepting requests. This endpoint is **public** — no authentication required — and is excluded from request metrics and tracing.\n\n```bash\ncurl 'https://api.atmosoar.io/observations/healthz'\n```\n\n```json\n{ \"status\": \"ok\" }\n```","operationId":"healthz","responses":{"200":{"description":"Service is healthy; body is {\"status\":\"ok\"}."}}}},"/observations/latest":{"get":{"tags":["observations"],"summary":"Latest observations","description":"Returns the **most recent observation per station** across the active observation window. This endpoint is optimized for dashboard-style \"current conditions\" use cases — it never returns more than one row per station and skips stations that have not reported within the freshness window.\n\nUse `source` to constrain the feed (e.g. only DWD Germany), `location` to restrict spatially (same grammar as `/observations` — point, radius, bbox, rectangle, shortcut), and `fields` to project a measurement subset.\n\n### Happy path — current temperature for every DWD German station\n\n```bash\ncurl -H \"Authorization: Bearer $ATMOSOAR_TOKEN\" \\\n  'https://api.atmosoar.io/observations/latest?source=dwd_germany\u0026fields=temperature_2m'\n```\n\nSample `200 OK` response (truncated):\n\n```json\n[\n  {\n    \"id\": 9128601,\n    \"station_name\": \"Berlin-Tempelhof\",\n    \"wmo_id\": \"10384\",\n    \"timestamp\": \"2026-04-26T07:00:00Z\",\n    \"lat\": 52.4675, \"lon\": 13.4021, \"elevation\": 48.0,\n    \"source\": \"dwd_germany\",\n    \"temperature_2m\": 9.1\n  }\n]\n```\n\n### Happy path — every source, full schema\n\n```bash\ncurl -H \"Authorization: Bearer $ATMOSOAR_TOKEN\" 'https://api.atmosoar.io/observations/latest'\n```\n\n### Bad path — internal error (`500 INTERNAL_ERROR`)\n\n```json\n{ \"httpStatus\": 500, \"code\": \"INTERNAL_ERROR\", \"message\": \"An unexpected error occurred.\" }\n```","operationId":"latestObservations","parameters":[{"name":"fields","in":"query","description":"Comma-separated measurement-column projection. Identity fields are always included. Unknown names produce 400.","schema":{"type":"string","description":"Comma-separated measurement-column projection. Identity fields are always included. Unknown names produce 400.","example":"temperature_2m,mslp"}},{"name":"limit","in":"query","description":"Max stations returned (integer). Omitted or 0, every station that reported in the window is returned; values above 5000 are capped at 5000.","schema":{"type":"string","description":"Max stations returned (integer). Omitted or 0, every station that reported in the window is returned; values above 5000 are capped at 5000.","example":"500"}},{"name":"location","in":"query","description":"Spatial filter — point 'lat,lon' (25 km radius), radius 'lat,lon|R' (km), bbox 'bbox:W,S,E,N', rectangle 'NElat,NElon|SWlat,SWlon', or a registered shortcut.","schema":{"type":"string","description":"Spatial filter — point 'lat,lon' (25 km radius), radius 'lat,lon|R' (km), bbox 'bbox:W,S,E,N', rectangle 'NElat,NElon|SWlat,SWlon', or a registered shortcut.","example":"52.52,13.40|50"}},{"name":"offset","in":"query","description":"Pagination offset (integer station count). Stations are ordered by WMO/ICAO identifier, so limit/offset pages are stable.","schema":{"type":"string","description":"Pagination offset (integer station count). Stations are ordered by WMO/ICAO identifier, so limit/offset pages are stable.","default":"0"}},{"name":"source","in":"query","description":"Upstream provider filter. One of: dwd_germany, dwd_international, ncep_synop, ncep_metar, ogimet.","schema":{"type":"string","description":"Upstream provider filter. One of: dwd_germany, dwd_international, ncep_synop, ncep_metar, ogimet.","example":"dwd_germany"}}],"responses":{"200":{"description":"Most recent observation per station (JSON array; one row per reporting station)."},"400":{"description":"Invalid query parameter (location, source, fields).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"503":{"description":"Query timed out; narrow location or source.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}}}}},"/observations/stations":{"get":{"tags":["stations"],"summary":"Query stations","description":"Returns the catalog of weather stations known to the service, filtered by any combination of WMO/ICAO ID, name substring (case-insensitive), or `location`. Useful for populating dropdowns, validating upstream identifiers, or discovering nearby stations before issuing an observation query.\n\n### Happy path — find Tempelhof in Germany\n\n```bash\ncurl -H \"Authorization: Bearer $ATMOSOAR_TOKEN\" --get \\\n  'https://api.atmosoar.io/observations/stations' \\\n  --data-urlencode 'location=germany' \\\n  --data-urlencode 'name=tempelhof'\n```\n\nSample `200 OK` response:\n\n```json\n[\n  {\n    \"id\": 4711,\n    \"name\": \"Berlin-Tempelhof\",\n    \"wmo_id\": \"10384\",\n    \"icao_id\": \"EDDI\",\n    \"lat\": 52.4675,\n    \"lon\": 13.4021,\n    \"elevation\": 48.0\n  }\n]\n```\n\n### Happy path — stations within 100 km of a point\n\n```bash\ncurl -H \"Authorization: Bearer $ATMOSOAR_TOKEN\" --get \\\n  'https://api.atmosoar.io/observations/stations' \\\n  --data-urlencode 'location=52.52,13.40|100'\n```\n\n### Bad path — invalid bounding box (`400 INVALID_LOCATION`)\n\n```json\n{\n  \"httpStatus\": 400,\n  \"code\": \"INVALID_LOCATION\",\n  \"message\": \"Invalid location parameter.\",\n  \"errors\": [\n    { \"code\": \"INVALID_LOCATION\", \"field\": \"bbox\", \"message\": \"lon bounds must be within [-180, 180]\" }\n  ]\n}\n```","operationId":"queryStations","parameters":[{"name":"icao_id","in":"query","description":"ICAO 4-letter station identifier.","schema":{"type":"string","description":"ICAO 4-letter station identifier.","example":"EDDB"}},{"name":"limit","in":"query","description":"Max rows returned (integer). Hard cap 5000; values above the cap fall back to the default 100.","schema":{"type":"string","description":"Max rows returned (integer). Hard cap 5000; values above the cap fall back to the default 100.","default":"100"}},{"name":"location","in":"query","description":"Location filter — same grammar as /observations (point, radius in km, bbox, rectangle, or shortcut).","schema":{"type":"string","description":"Location filter — same grammar as /observations (point, radius in km, bbox, rectangle, or shortcut).","example":"germany"}},{"name":"name","in":"query","description":"Station name substring (case-insensitive).","schema":{"type":"string","description":"Station name substring (case-insensitive).","example":"tempelhof"}},{"name":"offset","in":"query","description":"Pagination offset (integer row count).","schema":{"type":"string","description":"Pagination offset (integer row count).","default":"0"}},{"name":"wmo_id","in":"query","description":"WMO 5-digit station identifier.","schema":{"type":"string","description":"WMO 5-digit station identifier.","example":"10382"}}],"responses":{"200":{"description":"Matching stations (JSON array of station objects)."},"400":{"description":"Invalid query parameter (location).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"503":{"description":"Query timed out; narrow name or location.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}}}}},"/observations/stations/{id}":{"get":{"tags":["stations"],"summary":"Get station by ID","description":"Returns a single weather station by its **internal database ID** (the `id` field returned by `/observations/stations`). Use this when you have a stable Atmosoar station handle and want full metadata (coordinates, elevation, identifiers).\n\n### Happy path\n\n```bash\ncurl -H \"Authorization: Bearer $ATMOSOAR_TOKEN\" \\\n  'https://api.atmosoar.io/observations/stations/4711'\n```\n\nResponse `200 OK`:\n\n```json\n{\n  \"id\": 4711,\n  \"name\": \"Berlin-Tempelhof\",\n  \"wmo_id\": \"10384\",\n  \"icao_id\": \"EDDI\",\n  \"lat\": 52.4675,\n  \"lon\": 13.4021,\n  \"elevation\": 48.0\n}\n```\n\n### Bad path — non-numeric ID (`400 INVALID_PARAMETER`)\n\n```json\n{\n  \"httpStatus\": 400,\n  \"code\": \"INVALID_PARAMETER\",\n  \"message\": \"Invalid station id.\",\n  \"errors\": [\n    { \"code\": \"INVALID_PARAMETER\", \"field\": \"id\", \"message\": \"must be an integer\" }\n  ]\n}\n```\n\n### Bad path — unknown station (`404 NOT_FOUND`)\n\n```json\n{ \"httpStatus\": 404, \"code\": \"NOT_FOUND\", \"message\": \"Station not found.\" }\n```","operationId":"getStation","parameters":[{"name":"id","in":"path","description":"Station database ID (integer, not WMO/ICAO).","required":true,"schema":{"type":"string","description":"Station database ID (integer, not WMO/ICAO).","example":"4711"}}],"responses":{"200":{"description":"The requested station (single station object)."},"400":{"description":"Station id is not an integer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"404":{"description":"No station with that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"503":{"description":"Station lookup timed out; retry later.","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"}}}}},"tags":[{"name":"observations","description":"Surface weather observations."},{"name":"operational","description":"Operational endpoints (health)."},{"name":"stations","description":"Weather station catalog."}]}