{"openapi":"3.1.0","info":{"title":"WTHRS Public API","version":"2026-07-27","summary":"Consensus weather forecasts, provider comparisons and accuracy rankings.","description":"The WTHRS API returns consensus forecasts weighted across four forecast sources (SMHI SNOW1gv1 direct, MET Norway Nordic Seamless, ECMWF IFS 0.25°, DWD ICON) with a scientifically-derived confidence score.\n\n**Weighting and accuracy windows.** Source weights are computed on a rolling 14-day window of verified comparisons. Published accuracy figures (rankings, statistics, evidence) are computed over the full measurement period. These are two distinct quantities and must not be presented as the same number. Precipitation amount is weighted on MAE; precipitation probability is weighted on hit rate.\n\n**Four sources, one consensus track.** The WTHRS consensus is verified as its own track but is never counted as a source. The canonical denominator is four.\n\n**Station requirement.** Verified statistics require a qualified observation station within the distance requirement for the parameter. A location without such a station never receives verified statistics for that parameter; this is a property of the observation network, not a pending state.\n\n**Missing-data contract.** WTHRS never fabricates numeric values. When a field cannot be verified, the JSON returns `null` and the accompanying `dataStatus` entry carries an explicit `status` (`verified` | `verified_cached` | `degraded` | `estimated_disclosed` | `unavailable`) and machine-readable `reason`. Consumers must treat `null` values as missing rather than substituting them from other fields (e.g. Never mirror `wind` into `gust`).\n\n**Source-count semantics (2026-07-27 contract change).** WTHRS now distinguishes independent forecast streams from populated provider display slots after introducing lineage-based pre-collapse (fallback clones that share one upstream stream count once for consensus math).\n\n- `independentSourceCount` — unique independent upstream forecast streams that actually contributed to the current-hour consensus. Canonical numerator for \"N av M källor\". Use this for consensus, confidence and trust copy.\n- `providerSlotCount` — populated provider/display slots (temperature != null) before lineage collapse. Use this for slot-level diagnostics such as \"3 of 4 providers reported a reading\". This is the meaning `sourceCount` used to carry.\n- `totalSources` — maximum configured independent sources for the product (typically 4). Canonical denominator.\n- `sourceCount` — **deprecated alias of `independentSourceCount`**. Its previous meaning (populated slots) has moved to `providerSlotCount`. Kept in the wire format so existing consumers keep receiving the number the UI has always shown. Read `providerSlotCount` for the old diagnostic meaning and `independentSourceCount` for the new canonical meaning.","termsOfService":"https://wthrs.io/trust","contact":{"name":"WTHRS","url":"https://wthrs.io/anslut-ai"},"license":{"name":"CC BY 4.0","url":"https://creativecommons.org/licenses/by/4.0/"}},"servers":[{"url":"https://wthrs.io","description":"Production"},{"url":"https://wthrs.io","description":"Current host"}],"tags":[{"name":"Forecasts","description":"Consensus forecasts and provider comparisons."},{"name":"Rankings","description":"Historical accuracy rankings per model track."},{"name":"Reference","description":"Supported cities and metadata."}],"paths":{"/api/public/forecast/{city}":{"get":{"tags":["Forecasts"],"summary":"Retrieve a consensus forecast","description":"Returns the WTHRS consensus forecast for a city, including current conditions, hourly and 10-day outlook, and a 0 till 100 confidence score.","operationId":"getForecast","parameters":[{"name":"city","in":"path","required":true,"schema":{"type":"string","example":"stockholm"},"description":"Slug or Swedish name of a supported city. See `/api/public/cities`."}],"responses":{"200":{"description":"Forecast bundle","headers":{"Cache-Control":{"schema":{"type":"string"},"description":"public, max-age=300"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForecastResponse"},"example":{"location":{"name":"Stockholm","lat":59.3293,"lon":18.0686},"consensus":{"current":{"temperature":4.2,"condition":"cloudy","wind":3.1,"gust":null},"dataStatus":{"gust":{"status":"unavailable","reason":"no_verified_gust_field"},"temperature":{"status":"verified","source":"ecmwf_ifs025"}},"confidence":87,"hourly":[{"time":"2026-07-24T14:00:00Z","temperature":4.6}],"daily":[{"date":"2026-07-24","high":6.1,"low":1.4}]},"generatedAt":"2026-07-24T12:00:00.000Z"}}}},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"5XX":{"$ref":"#/components/responses/ServerError"}}}},"/api/public/compare/{city}":{"get":{"tags":["Forecasts"],"summary":"Compare providers side-by-side","description":"Returns per-provider forecasts (SMHI/SNOW1gv1 direct, Yr/metno_seamless, Apple/ecmwf_ifs025, icon/icon_seamless) alongside the WTHRS consensus.","operationId":"compareProviders","parameters":[{"name":"city","in":"path","required":true,"schema":{"type":"string","example":"goteborg"}}],"responses":{"200":{"description":"Comparison bundle","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompareResponse"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/public/rankings":{"get":{"tags":["Rankings"],"summary":"Accuracy rankings by model track","operationId":"getRankings","parameters":[{"name":"city","in":"query","schema":{"type":"string","default":"Stockholm"}},{"name":"parameter","in":"query","schema":{"type":"string","enum":["temperature","rain","wind"],"default":"temperature"}},{"name":"horizon","in":"query","schema":{"type":"string","enum":["24h","48h","7d"],"default":"24h"}}],"responses":{"200":{"description":"Ranking table","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RankingsResponse"}}}}}}},"/api/public/cities":{"get":{"tags":["Reference"],"summary":"List supported cities","operationId":"listCities","responses":{"200":{"description":"City list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CitiesResponse"}}}}}}}},"components":{"schemas":{"Location":{"type":"object","properties":{"name":{"type":"string"},"lat":{"type":"number","format":"double"},"lon":{"type":"number","format":"double"}},"required":["name","lat","lon"]},"Consensus":{"type":"object","properties":{"current":{"type":"object","properties":{"temperature":{"type":"number"},"condition":{"type":"string"},"wind":{"type":"number"},"gust":{"type":["number","null"],"description":"Verified wind gust in m/s. `null` when no source reports a distinct gust value greater than the sustained wind. WTHRS never mirrors wind speed into this field."}}},"confidence":{"type":"integer","minimum":0,"maximum":100,"description":"Composite data-quality score. Not a probability of correctness."},"dataStatus":{"type":"object","description":"Per-metric data-integrity status. Only `verified` and `verified_cached` are safe to render as normal values; `degraded`, `estimated_disclosed` and `unavailable` must be surfaced to the user.","additionalProperties":{"type":"object","properties":{"status":{"type":"string","enum":["verified","verified_cached","degraded","estimated_disclosed","unavailable"]},"source":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"},"reason":{"type":"string"}},"required":["status"]}},"independentSourceCount":{"type":"integer","minimum":0,"description":"Unique independent upstream forecast streams that actually contributed to the current-hour consensus. Fallback clones sharing one upstream stream count once. Canonical numerator for \"N of M sources\" copy."},"providerSlotCount":{"type":"integer","minimum":0,"description":"Populated provider display slots (temperature != null) before lineage collapse. Use for slot-level diagnostics such as \"3 of 4 providers reported a reading\". This is the meaning `sourceCount` carried before 2026-07-27."},"totalSources":{"type":"integer","minimum":0,"description":"Maximum configured independent sources for the product (typically 4). Canonical denominator."},"sourceCount":{"type":"integer","minimum":0,"deprecated":true,"description":"Deprecated alias of `independentSourceCount`. Its previous meaning (populated slots) moved to `providerSlotCount` on 2026-07-27. New consumers must read `independentSourceCount` for consensus/confidence and `providerSlotCount` for slot diagnostics."},"hourly":{"type":"array","items":{"type":"object"}},"daily":{"type":"array","items":{"type":"object"}}}},"ForecastResponse":{"type":"object","properties":{"location":{"$ref":"#/components/schemas/Location"},"consensus":{"$ref":"#/components/schemas/Consensus"},"generatedAt":{"type":"string","format":"date-time"}}},"CompareResponse":{"type":"object","properties":{"location":{"$ref":"#/components/schemas/Location"},"providers":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","enum":["smhi","yr","apple","icon"],"description":"Canonical provider id. The ICON track was published as \"klart\" before 2026-08-20. Requests may still send \"klart\"; responses always return \"icon\"."},"label":{"type":"string"},"current":{"type":"object"}}}},"consensus":{"$ref":"#/components/schemas/Consensus"},"generatedAt":{"type":"string","format":"date-time"}}},"RankingsResponse":{"type":"object","properties":{"city":{"type":"string"},"parameter":{"type":"string"},"horizon":{"type":"string"},"rankings":{"type":"array","items":{"type":"object","properties":{"providerId":{"type":"string"},"mae":{"type":"number","nullable":true},"sampleSize":{"type":"integer"}}}},"generatedAt":{"type":"string","format":"date-time"}}},"CitiesResponse":{"type":"object","properties":{"cities":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"region":{"type":"string"},"lat":{"type":"number"},"lon":{"type":"number"}}}},"generatedAt":{"type":"string","format":"date-time"}}},"Error":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"},"request_id":{"type":"string"}},"required":["error"]}},"responses":{"NotFound":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Unknown city: \"atlantis\""}}}},"RateLimited":{"description":"Too many requests. Back off and retry with jitter.","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"rate_limited"}}}},"ServerError":{"description":"Upstream provider or gateway error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}