{"openapi":"3.0.1","info":{"title":"Stations API","description":"Observations from weather stations operated by Atmosoar customers: Ecowitt, Bresser WSLink, Wunderground-protocol stations, IQtronic, Campbell Scientific data loggers and generic stations. Stations normally push straight to Atmosoar over HTTP, WebSocket, MQTT(S), TCP (optionally TLS) or UDP; a station whose device cannot be reconfigured may instead be polled from its vendor's cloud. Readings are identical either way: every payload is normalized into the common observation model (SI units, the field names observation-api uses).\n\nEach station carries a fixed position. While `position.placeholder` is true its coordinates are not known yet and are null.\n\n## Access\n\nEvery route below needs a bearer token and the realm role `api:stations`. Stations are owned by tenants: a tenant is one Keycloak organization group, and a caller only ever sees the stations of the organizations they belong to (a team membership counts as its parent organization). A station owned by another tenant answers `404`, exactly like one that does not exist.\n\n## Response envelope\n\nJSON responses use the shared Atmosoar envelope `{httpStatus, message, data}`; errors use `{httpStatus, code, message, errors[]}`.","version":"1.7.0"},"paths":{"/stations/api/stations":{"get":{"tags":["Stations"],"summary":"List stations","description":"The caller's stations with position, transports, owning tenant and freshness. `status` is `live`, `silent` (no observation within stations.silent_after) or `never_reported`. Only stations owned by the caller's tenants are listed.\n\n`name` is what YOU call the station and `default_name` is the operator's; they differ when you have renamed it (`renamed: true`). Pass `include=latest` to fold each station's newest observation into the list, which is one query for the whole list rather than a request per station.","operationId":"listStations","parameters":[{"name":"include","in":"query","description":"Comma-separated extras. \"latest\" folds each station's newest observation into the list.","schema":{"type":"string","description":"Comma-separated extras. \"latest\" folds each station's newest observation into the list.","example":"latest"}}],"responses":{"200":{"description":"Stations"},"401":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}}}}},"/stations/api/stations/{id}":{"get":{"tags":["Stations"],"summary":"Get a station","operationId":"getStation","parameters":[{"name":"id","in":"path","description":"Station ID.","required":true,"schema":{"type":"string","description":"Station ID.","example":"ecowitt-gw1100-1"}}],"responses":{"200":{"description":"Station"},"401":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"404":{"description":"Unknown station, or one owned by another tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}}}}},"/stations/api/stations/{id}/latest":{"get":{"tags":["Stations"],"summary":"Latest observation of a station","description":"The newest normalized observation, with the station position filled into `lat`, `lon` and `elevation`.","operationId":"getLatestObservation","parameters":[{"name":"id","in":"path","description":"Station ID.","required":true,"schema":{"type":"string","description":"Station ID.","example":"ecowitt-gw1100-1"}}],"responses":{"200":{"description":"Latest observation"},"401":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"404":{"description":"Unknown station, one owned by another tenant, or no observation yet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}}}}},"/stations/api/stations/{id}/name":{"put":{"tags":["Stations"],"summary":"Rename a station, for you","description":"Sets the name YOU see for this station. The operator's name is untouched, and so is what every other member of the tenant sees. An empty name restores the operator's name, which is also what DELETE does. At most 80 characters.","operationId":"renameStation","parameters":[{"name":"id","in":"path","description":"Station ID.","required":true,"schema":{"type":"string","description":"Station ID.","example":"ecowitt-gw1100-1"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenameStationInput"}}}},"responses":{"200":{"description":"Display name set"},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"401":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"404":{"description":"Unknown station, or one owned by another tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}}}},"delete":{"tags":["Stations"],"summary":"Restore the operator's name","description":"Drops your display name for this station.","operationId":"clearStationName","parameters":[{"name":"id","in":"path","description":"Station ID.","required":true,"schema":{"type":"string","description":"Station ID.","example":"ecowitt-gw1100-1"}}],"responses":{"200":{"description":"Display name cleared"},"401":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"404":{"description":"Unknown station, or one owned by another tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}}}}},"/stations/api/stations/{id}/observations":{"get":{"tags":["Stations"],"summary":"Observations of a station","description":"Observations within the requested window, newest first.","operationId":"getObservationHistory","parameters":[{"name":"id","in":"path","description":"Station ID.","required":true,"schema":{"type":"string","description":"Station ID.","example":"ecowitt-gw1100-1"}},{"name":"limit","in":"query","description":"Maximum number of observations, newest first (1..10000).","schema":{"type":"string","description":"Maximum number of observations, newest first (1..10000).","default":"1000"}},{"name":"time","in":"query","description":"Observation-time window: a single RFC 3339 timestamp (from then until now) or a range 'start,end'. Defaults to the last 24 hours; at most stations.history_max_range (31 days by default).","schema":{"type":"string","description":"Observation-time window: a single RFC 3339 timestamp (from then until now) or a range 'start,end'. Defaults to the last 24 hours; at most stations.history_max_range (31 days by default).","example":"2026-09-14T00:00:00Z,2026-09-15T00:00:00Z"}}],"responses":{"200":{"description":"Observations"},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"401":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"404":{"description":"Unknown station, or one owned by another tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}}}}},"/stations/api/tenants":{"get":{"tags":["Stations"],"summary":"List the caller's tenants","description":"The organizations the caller belongs to that own stations here. The list is empty when the caller's Keycloak groups match no tenant, which is also when the station list is empty.","operationId":"listTenants","responses":{"200":{"description":"Tenants"},"401":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttputilsAPIError"}}}}}}},"/stations/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"}}}},"/stations/readyz":{"get":{"tags":["Health"],"summary":"Readiness probe","description":"Returns `200` when the database is reachable, `503` otherwise.","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"}}},"RenameStationInput":{"type":"object","properties":{"name":{"type":"string","description":"Display name, or empty to restore the operator's name.","example":"North quay mast"}}}}},"tags":[{"name":"Health","description":"Health and readiness probes"},{"name":"Stations","description":"Registered stations, their positions and normalized observations"}]}