API Reference

WTHRS API

A read-only JSON API for consensus weather forecasts, per-provider comparisons and accuracy rankings. Designed to be boring on purpose. Stable URLs, predictable JSON, aggressive caching, no authentication for the public tier.

Base URL https://wthrs.io/api/publicAPI version 2026-07-24JSON · CORS enabled
Introduction

Overview

The WTHRS API exposes the same consensus engine that powers wthrs.io. All requests are stateless HTTP GETs returning JSON.

The API is organized around four resources:

  • forecast. The WTHRS consensus for a city
  • compare. Per-provider breakdown next to the consensus
  • rankings. MAE-baseline ordering per model track
  • cities. The supported location catalog

Every response includes a top-level generatedAt timestamp and is cacheable at the edge for 5 minutes.

Base URL
# Production
curl https://wthrs.io/api/public/cities

# Response
{
  "cities": [ ... ],
  "generatedAt": "2026-07-24T12:00:00.000Z"
}
Get started

Quickstart

Fetch a forecast in under a minute. No API key, no signup.

Point any HTTP client at https://wthrs.io/api/public. The API is read-only, soGET is the only method you'll need. All responses are JSON withContent-Type: application/json and permissive CORS (Access-Control-Allow-Origin: *).

For production use we strongly recommend caching responses for at least 5 minutes. That matches the upstream refresh cadence and dramatically reduces your latency.

curl -s https://wthrs.io/api/public/forecast/stockholm | jq .consensus.current
Spec

OpenAPI specification

Machine-readable OpenAPI 3.1 document. Import it into Postman, Insomnia, Stoplight, or generate a typed client.

The spec is served with a 1-hour cache. Pin to a specificversion from info.version if you generate clients from it.

Generate a TypeScript client
npx openapi-typescript https://wthrs.io/api/openapi.json -o wthrs.d.ts

# Or a full SDK with Fern / openapi-generator
openapi-generator-cli generate \
  -i https://wthrs.io/api/openapi.json \
  -g typescript-fetch -o ./wthrs-sdk
Auth

Authentication

The public tier requires no authentication. Requests are open to anyone.

WTHRS' consensus API is a public read-only tier. No API key, no Authorization header. This is deliberate: the underlying model data is CC BY 4.0 and we want it to be as embeddable as a Google Font.

Attribution is appreciated but not required. Please cite wthrs.io and the underlying model track when displaying values in a UI.

A future private tier with per-key quotas and higher rate limits will use a bearer token in the Authorization header. The public endpoints documented here will remain unauthenticated.

No auth required
# Just call the endpoint. No header needed
curl https://wthrs.io/api/public/forecast/stockholm

# For the (future) private tier:
# curl -H "Authorization: Bearer wthrs_sk_live_..." https://wthrs.io/api/v2/forecast
Fair use

Rate limits

Soft-limited at the edge. Cache aggressively. You almost certainly do not need to poll.

The public tier has no per-key quota, but the edge applies fair-use throttling per IP. Bursts under 60 requests/minute per resource are effectively free; sustained higher volume may return 429 Too Many Requests with aRetry-After header (in seconds).

Because the upstream model data refreshes at most every 5-15 minutes, polling faster than max-age=300 gives you the same JSON back. Respect theCache-Control header and you'll never hit a limit.

For bulk or commercial usage, get in touch via Trust Center.

Exponential backoff (Node)
async function fetchWithBackoff(url, tries = 4) {
  for (let i = 0; i < tries; i++) {
    const res = await fetch(url);
    if (res.status !== 429) return res;
    const wait = Number(res.headers.get("retry-after") ?? 2 ** i) * 1000;
    await new Promise(r => setTimeout(r, wait + Math.random() * 300));
  }
  throw new Error("rate_limited");
}
Performance

Caching

Every endpoint sets a long, cacheable Cache-Control header. Use it.

All responses include Cache-Control: public, max-age=300 (openapi.json is max-age=3600). The edge honors it, and so should your client, CDN, or service worker.

  • Client: use fetch(url, { cache: "force-cache" }) in the browser.
  • Server: wrap calls in a 5-minute in-memory or Redis cache keyed by URL.
  • Edge: Cloudflare / Vercel / Fastly will cache automatically. No config.

The generatedAt field on every response tells you when the payload was materialized upstream, so you can display "as of HH:MM" in your UI.

Response headers
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: public, max-age=300
Access-Control-Allow-Origin: *
X-WTHRS-Version: 2026-07-24
X-Request-Id: 01HW9R9T6EHK5P8T2Z9AXX3M0Q
Lists

Pagination

Current list endpoints return complete result sets. Cursor-based pagination will be introduced when we ship the historical observations endpoints.

Because the public catalog (cities: ~15, providers: 4, ranking rows: ≤4) is small and bounded, list endpoints return the full array in one response. No pagination is required today.

When historical time-series endpoints ship, they will follow a cursor-based scheme identical to Stripe's:

  • limit. 1 to 100 (default 25)
  • starting_after. Object id to start after
  • ending_before. Object id to end before

Responses will include has_more and a data array, matching the shape below.

Future paginated shape
{
  "object": "list",
  "has_more": true,
  "data": [
    { "id": "obs_01HW...", "observed_time": "2026-07-24T10:00:00Z", ... },
    ...
  ],
  "next_cursor": "obs_01HW..."
}
Failure modes

Errors

Standard HTTP status codes. JSON error bodies. Never HTML.

Codes you might encounter:

  • 200 OK. Success
  • 400 Bad Request. Malformed query parameter
  • 404 Not Found. Unknown city slug or path
  • 429 Too Many Requests. Rate limited, see Retry-After
  • 502 / 503 / 504. Upstream provider or gateway error; safe to retry with backoff

Error bodies always contain an error string.

HTTP/1.1 404 Not Found

{
  "error": "Unknown city: \"atlantis\""
}
Data integrity

Missing-data contract

WTHRS never fabricates numeric values. When a field cannot be verified, the API returns null plus an explicit status.

Every metric may carry a dataStatus entry describing how much the value can be trusted. Only verified and verified_cached are safe to render as normal values. The remaining statuses must be surfaced to end users.

  • verified. Direct, fresh value from a source we can verify.
  • verified_cached. Last verified value, served from cache.
  • degraded. Only one upstream source responded; comparison is limited.
  • estimated_disclosed. WTHRS-derived. Always labelled as estimated.
  • unavailable. No verifiable value exists; the field is null.

Consumers must not substitute a null value with another field. In particular, never mirror wind into gust; the two represent different physical quantities and a distinct gust reading is only trusted when it is strictly greater than the sustained wind.

{
  "consensus": {
    "current": { "wind": 3.1, "gust": null }
  },
  "dataStatus": {
    "gust": {
      "status": "unavailable",
      "reason": "no_verified_gust_field"
    }
  }
}
Stability

Versioning

Date-based, additive-by-default. Breaking changes ship on a new version string.

The current API version is 2026-07-24. It appears in the OpenAPI document's info.version and in the X-WTHRS-Version response header.

Additive changes. New fields, new endpoints, new enum values. Are shipped continuously without a version bump. Backwards-incompatible changes get a new date-versioned base path (/api/v2026-12-01/...). Old versions remain callable for at least 12 months.

To pin, capture the version from a response header at build time and refuse to upgrade until you've validated against the new schema.

Pin your build
# Save the current spec into your repo
curl https://wthrs.io/api/openapi.json > vendor/wthrs-openapi.json

# CI will fail if the live version diverges from what you built against
test "$(jq -r .info.version vendor/wthrs-openapi.json)" = "2026-07-24"
Change policy

Deprecation policy

Fields are removed slowly, loudly, and predictably.

Our contract with integrators:

  • 90 days notice on Trust Center and in the Deprecation response header before any field or endpoint is removed.
  • Sunset header set to the removal date (RFC 8594).
  • Deprecated fields keep working. They just also emit warnings in the response.
  • New fields never break existing clients (additive JSON).
  • Enum values can be added; removed values follow the same 90-day window.
Deprecation headers (RFC 8594)
HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 01 Nov 2026 00:00:00 GMT
Link: <https://wthrs.io/docs/api#ep-forecast>; rel="successor-version"
Warning: 299 - "field 'consensus.rain_mm' will be removed on 2026-11-01, use 'consensus.precipitation_mm'"
Endpoint

Retrieve a forecast

GET/api/public/forecast/{city}

Returns the consensus forecast for a supported city.

Path parameters
ParameterTypeDescription
cityrequiredstringSlug or Swedish name (e.g. Stockholm, göteborg, malmo). See /cities for the full list.

Returns a ForecastResponse object with location, consensus and generatedAt.

curl https://wthrs.io/api/public/forecast/stockholm
Example response
{
  "location": {
    "name": "Stockholm",
    "lat": 59.3293,
    "lon": 18.0686
  },
  "consensus": {
    "current": {
      "temperature": 4.2,
      "condition": "cloudy",
      "wind": 3.1,
      "humidity": 78
    },
    "confidence": 87,
    "hourly": [
      { "time": "2026-07-24T14:00:00Z", "temperature": 4.6, "precipitation": 0.0 },
      { "time": "2026-07-24T15:00:00Z", "temperature": 4.8, "precipitation": 0.1 }
    ],
    "daily": [
      { "date": "2026-07-24", "high": 6.1, "low": 1.4, "condition": "cloudy" }
    ]
  },
  "generatedAt": "2026-07-24T12:00:00.000Z"
}
Endpoint

Compare providers

GET/api/public/compare/{city}

Returns each provider's current forecast alongside the WTHRS consensus. Useful for building side-by-side comparison UIs or auditing the weighting engine.

Path parameters
ParameterTypeDescription
cityrequiredstringCity slug or name.
curl https://wthrs.io/api/public/compare/goteborg
Example response
{
  "location": { "name": "Göteborg", "lat": 57.7089, "lon": 11.9746 },
  "providers": [
    { "id": "smhi",  "label": "SMHI (SNOW1gv1)",   "current": { "temperature": 5.1, "wind": 4.2 } },
    { "id": "yr",    "label": "Yr (MetCoOp MEPS)",         "current": { "temperature": 5.4, "wind": 4.0 } },
    { "id": "apple", "label": "Apple Weather (ECMWF IFS)", "current": { "temperature": 4.9, "wind": 4.5 } },
    { "id": "icon",  "label": "DWD ICON",                  "current": { "temperature": 5.3, "wind": 4.1 } }
  ],
  "consensus": { "current": { "temperature": 5.2, "wind": 4.2 }, "confidence": 91 },
  "generatedAt": "2026-07-24T12:00:00.000Z"
}
Endpoint

Accuracy rankings

GET/api/public/rankings

Ordered baseline of model tracks by MAE (or hit rate for precipitation).

Query parameters
ParameterTypeDescription
citystringDefaults to Stockholm.
parameter"temperature" | "rain" | "wind"Defaults to temperature.
horizon"24h" | "48h" | "7d"Defaults to 24h.
curl
curl "https://wthrs.io/api/public/rankings?city=Stockholm&parameter=temperature&horizon=24h"
Example response
{
  "city": "Stockholm",
  "parameter": "temperature",
  "horizon": "24h",
  "rankings": [
    { "providerId": "smhi",  "mae": 1.02, "sampleSize": 512 },
    { "providerId": "yr",    "mae": 1.11, "sampleSize": 512 },
    { "providerId": "apple", "mae": 1.24, "sampleSize": 512 },
    { "providerId": "icon",  "mae": 1.31, "sampleSize": 512 }
  ],
  "generatedAt": "2026-07-24T12:00:00.000Z"
}
Endpoint

List supported cities

GET/api/public/cities

Returns the catalog of supported city slugs, names and coordinates. Use this to power a search field or validate user input before calling /forecast/{city}.

Example response
{
  "cities": [
    { "slug": "stockholm",  "name": "Stockholm",  "region": "Svealand", "lat": 59.3293, "lon": 18.0686 },
    { "slug": "goteborg",   "name": "Göteborg",   "region": "Götaland", "lat": 57.7089, "lon": 11.9746 },
    { "slug": "malmo",      "name": "Malmö",      "region": "Götaland", "lat": 55.6050, "lon": 13.0038 }
  ],
  "generatedAt": "2026-07-24T12:00:00.000Z"
}
SDK

JavaScript / Node

Idiomatic fetch, retries, and typed helpers.

const BASE = "https://wthrs.io/api/public";

export async function getForecast(city) {
  const res = await fetch(`${BASE}/forecast/${encodeURIComponent(city)}`, {
    headers: { Accept: "application/json" },
    cache: "force-cache",
  });
  if (!res.ok) throw new Error(`WTHRS ${res.status}: ${await res.text()}`);
  return res.json();
}

// Usage
const { consensus } = await getForecast("stockholm");
console.log(consensus.current.temperature, "°C ·", consensus.confidence, "confidence");
SDK

Python

import httpx

BASE = "https://wthrs.io/api/public"

def get_forecast(city: str) -> dict:
    with httpx.Client(timeout=10.0, headers={"Accept": "application/json"}) as c:
        r = c.get(f"{BASE}/forecast/{city}")
        r.raise_for_status()
        return r.json()

data = get_forecast("stockholm")
print(data["consensus"]["current"]["temperature"], "°C")
SDK

Swift (iOS / macOS)

import Foundation

struct Consensus: Decodable {
  struct Current: Decodable { let temperature: Double; let condition: String; let wind: Double }
  let current: Current
  let confidence: Int
}
struct Forecast: Decodable {
  struct Location: Decodable { let name: String; let lat: Double; let lon: Double }
  let location: Location
  let consensus: Consensus
  let generatedAt: String
}

enum WTHRSError: Error { case badStatus(Int) }

func getForecast(city: String) async throws -> Forecast {
  let url = URL(string: "https://wthrs.io/api/public/forecast/\(city)")!
  let (data, resp) = try await URLSession.shared.data(from: url)
  guard (resp as? HTTPURLResponse)?.statusCode == 200 else {
    throw WTHRSError.badStatus((resp as? HTTPURLResponse)?.statusCode ?? -1)
  }
  return try JSONDecoder().decode(Forecast.self, from: data)
}

// Usage in a Task
Task {
  let f = try await getForecast(city: "stockholm")
  print(f.consensus.current.temperature, "°C · conf", f.consensus.confidence)
}
SDK

Kotlin (Android / JVM)

import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.Serializable

@Serializable data class Current(val temperature: Double, val condition: String, val wind: Double)
@Serializable data class Consensus(val current: Current, val confidence: Int)
@Serializable data class Location(val name: String, val lat: Double, val lon: Double)
@Serializable data class ForecastResponse(val location: Location, val consensus: Consensus, val generatedAt: String)

val http = HttpClient(CIO) {
  install(ContentNegotiation) { json() }
}

suspend fun forecast(city: String): ForecastResponse =
  http.get("https://wthrs.io/api/public/forecast/\$city").body()

// suspend fun main() {
//   val f = forecast("stockholm")
//   println("${f.consensus.current.temperature}°C · conf ${f.consensus.confidence}")
// }
WTHRS API · API version 2026-07-24 · Data licensed under CC BY 4.0.

WTHRS. Oberoende väderintelligens. Baserad på öppna vädermodeller. Vi ger prognoser i informationssyfte, inte som officiella varningar.

© 2026 WTHRS. Prognos från SMHI (SNOW1gv1) direkt via SMHI Open Data, samt modellspåren MET Norway Nordic Seamless, ECMWF IFS och DWD ICON via Open-Meteo. Observationer från SMHI:s mätstationer.