A small, stdlib-only Go client for the KNMI EDR API — Dutch in-situ weather observations and daily climatology, returned as typed Go domain values rather than raw CoverageJSON.
⚠️ Pre-1.0 — API is not stable until thev0.1.0tag. Names, signatures, and behavior of exported identifiers may change without deprecation notice while we shake the surface out against the live KNMI API. Pin a commit if you depend on this in production.
KNMI's EDR endpoint serves:
- In-situ observations — 10-min, hourly, daily (with
_validatedvariants) from ~77 fixed weather stations across the Netherlands and surrounding waters. - Daily climatology —
Tg1,Tn1,Tx1,Rd1,EV24,wins50.
It does not serve precipitation nowcast or HARMONIE forecasts — those live behind KNMI's separate Open Data API as HDF5/GRIB files and are out of this module's scope.
The library is built around a small, opinionated surface for the "current conditions at a coordinate" use-case, with raw access for everything else.
go get go.flaticols.dev/knmi-gogo install go.flaticols.dev/knmi-go/cmd/knmi@latestGet an API key from
developer.dataplatform.knmi.nl.
Either pass it explicitly or set the KNMI_API_KEY environment variable:
c := knmi.New("eyJv...") // explicit
c := knmi.New("") // falls back to $KNMI_API_KEYThe key is sent in the raw Authorization header (no Bearer prefix)
— this is what KNMI expects.
package main
import (
"context"
"fmt"
"log"
"time"
"go.flaticols.dev/knmi-go"
)
func main() {
c := knmi.New("") // picks up KNMI_API_KEY
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
cur, err := c.GetCurrent(ctx, 52.3676, 4.9041) // Amsterdam
if err != nil {
log.Fatal(err)
}
fmt.Printf("Station: %s (%.4f, %.4f)\n",
cur.Station.Name, cur.Station.Lat, cur.Station.Lon)
if cur.TempC != nil {
fmt.Printf("%.1f°C at %s\n",
*cur.TempC, cur.Time.Format(time.RFC3339))
}
}The library asks for the nearest of KNMI's ~77 stations to the
requested coordinates — Amsterdam center will resolve to Schiphol
Airport, for example. The actual station picked is reported via
cur.Station.
knmi -location amsterdamKNMI current conditions — Amsterdam (52.3676, 4.9041)
Station: Schiphol Airport (0-20000-0-06240) at 52.3172, 4.7897
Observed: 2026-04-30 18:10 UTC
Temperature 18.0 °C
Humidity 25 %
Wind speed 6.0 m/s
Wind direction 105°
Precipitation 0.00 mm/h
Pressure 1024.9 hPa
Other CLI inputs:
knmi -lat 52.37 -lon 4.90 # explicit coordinates
knmi -location 52.5,4.7 # literal lat,lon string
knmi -list-locations # known NL place names
knmi -token <key> -location utrechtSeriesAt returns the full time-history of one parameter at the nearest
station to your coordinates. Pick the collection that matches the cadence
you want:
| Window | Collection | Cadence |
|---|---|---|
| ≤ a few h | Collection10MinObservations |
10 minutes |
| hours–days | CollectionHourlyObservations |
1 hour |
| days–weeks | CollectionDailyObservations |
1 day |
Note: KNMI EDR only serves observations — there is no forecast side. For "past 2 hours" you get the actual measured trend; there is no equivalent "next 2 hours".
to := time.Now().UTC()
from := to.Add(-2 * time.Hour)
s, err := c.SeriesAt(ctx,
knmi.Collection10MinObservations, "ta",
52.3676, 4.9041, // Amsterdam → resolves to Schiphol
from, to,
)
if err != nil { log.Fatal(err) }
fmt.Printf("%s — %d readings of %s [%s]\n",
s.Station.Name, len(s.Readings), s.Param, s.Unit)
for _, r := range s.Readings {
if r.Value != nil {
fmt.Printf(" %s %.2f\n", r.Time.Format("15:04"), *r.Value)
}
}Sample output (live, Schiphol Airport):
Schiphol Airport — 12 readings of ta [°C]
16:30 19.70
16:40 19.60
16:50 19.50
17:00 19.30
...
s, _ := c.SeriesAt(ctx,
knmi.CollectionHourlyObservations, "ta",
52.3676, 4.9041,
time.Now().Add(-48*time.Hour), time.Now(),
)
fmt.Printf("48h hourly: %d readings\n", len(s.Readings))s, _ := c.SeriesAt(ctx,
knmi.CollectionDailyObservations, "ta",
52.3676, 4.9041,
time.Now().AddDate(0, 0, -7), time.Now(),
)
for _, r := range s.Readings {
fmt.Printf("%s %.1f°C\n", r.Time.Format("Mon"), *r.Value)
}SeriesAt works for any KNMI shortname the chosen collection accepts —
not just ta. Useful examples:
| Param | Meaning |
|---|---|
ta |
air temperature (°C) |
rh |
relative humidity (%) |
ff |
10-min mean wind speed (m/s) |
gff |
wind gust (m/s) |
dd |
wind direction (°) |
pp |
station pressure (hPa) |
rg |
rain-gauge intensity (mm/h) |
R1H |
precipitation amount, last 1 h (mm) |
R24H |
precipitation amount, last 24 h (mm) |
vv |
visibility (m) |
n |
total cloud cover (octas) |
If you ask for a name the server doesn't recognize, you get
ErrBadRequest wrapping a clear "supported values are…" hint.
Runnable versions of these snippets live as godoc examples — see
Example_seriesLastTwoHours, Example_seriesLastTwoDays, and
Example_seriesLastWeekDaily in example_series_test.go.
The default fetch returns six well-known shortnames (ta, rh, ff,
dd, rg, pp). KNMI exposes ~85 valid shortnames per station; ask
for whatever subset you want and read the rest from cur.Raw:
cur, _ := c.FetchObservations(ctx, 52.37, 4.90,
knmi.WithObservationParams("ta", "gff", "vv", "R1H"),
)
fmt.Printf("gust: %v\n", cur.Raw["gff"]) // wind gust m/s
fmt.Printf("visibility: %v\n", cur.Raw["vv"]) // metres
fmt.Printf("1h rain: %v\n", cur.Raw["R1H"]) // mm in past hourParameter names are not validated client-side — KNMI's catalog evolves;
the server returns a clear 400 with the supported list (wrapped in
ErrBadRequest) if you request something it doesn't recognize.
cur, _ := c.FetchObservationsFresh(ctx, 52.37, 4.90)Or disable cache globally:
c := knmi.New("", knmi.WithCache(false))If you'd rather pick the station yourself instead of letting the library nearest-match:
stations, _ := c.Locations(ctx, knmi.Collection10MinObservations)
for _, s := range stations {
fmt.Printf("%s %s (%.4f, %.4f)\n", s.ID, s.Name, s.Lat, s.Lon)
}
raw, _ := c.LocationData(ctx,
knmi.Collection10MinObservations,
"0-20000-0-06240", // Schiphol
time.Now().Add(-30*time.Minute), time.Now(),
[]string{"ta", "rh"},
)
cov, _ := coveragejson.DecodeCollection(raw)
series, unit, _ := cov.First().Series("ta")cols, _ := c.Collections(ctx)
for _, col := range cols {
fmt.Printf("%s %s\n", col.ID, col.Title)
}
col, _ := c.Collection(ctx, knmi.Collection10MinObservations)
for name := range col.ParameterNames {
fmt.Println(name)
}// Constructor
knmi.New(apiKey string, opts ...Option) *Client
// High-level: nearest-station observations
(*Client).GetCurrent(ctx, lat, lon)
(*Client).FetchObservations(ctx, lat, lon, opts ...ObservationOption)
(*Client).FetchObservationsFresh(ctx, lat, lon, opts ...ObservationOption)
// Time series for one parameter at the nearest station
(*Client).SeriesAt(ctx, collection, param, lat, lon, from, to)
// Stations
(*Client).Locations(ctx, collectionID)
(*Client).LocationData(ctx, collectionID, stationID, from, to, params)
(*Client).NearestStation(ctx, collectionID, lat, lon)
// Catalog discovery
(*Client).Collections(ctx)
(*Client).Collection(ctx, id)
(*Client).Instances(ctx, id)
(*Client).LatestInstance(ctx, id)| Option | Purpose |
|---|---|
WithBaseURL(string) |
override EDR base URL (testing, staging) |
WithUserAgent(string) |
set the User-Agent header |
WithHTTPClient(*http.Client) |
inject your own http.Client (timeouts, proxy) |
WithClock(func() time.Time) |
inject a clock for deterministic tests |
WithLogger(LogFunc) |
structured per-request callback |
WithCache(bool) |
enable/disable in-memory caching (default true) |
Per-call observation options:
| Option | Purpose |
|---|---|
WithObservationParams(params ...string) |
replace the default parameter set |
WithObservationLookback(d time.Duration) |
widen the time window scanned for the latest reading |
type CurrentConditions struct {
Station Station
Time time.Time
TempC *float64
Humidity *float64
WindSpeedMS *float64
WindDirDeg *float64
PrecipMMHour *float64
PressureHPa *float64
Raw map[string]*float64 // every shortname returned, nullable
}
type Station struct {
ID, Name string
Lat, Lon float64
}
type Series struct {
Station Station
Param string // KNMI shortname (e.g. "ta")
Unit string // KNMI-reported unit (e.g. "°C", "m s-1")
Readings []Reading
}
type Reading struct {
Time time.Time
Value *float64 // nil for missing
}Pointer fields are nil when the sensor dropped out — distinguish "no data" from "zero".
Network and HTTP errors are mapped to typed sentinels you can match
with errors.Is:
| Sentinel | HTTP | Meaning |
|---|---|---|
ErrAuth |
401, 403 | API key issue |
ErrNotFound |
404 | wrong collection / no stations |
ErrBadRequest |
400, 422 | invalid param or coords |
ErrRateLimit |
429 | throttled (auto-retried once) |
ErrUpstream |
5xx | KNMI side |
The full *APIError carries the response body (with KNMI's helpful
"supported values are…" hint when you guess a parameter name).
Three TTLs:
| Cache | TTL |
|---|---|
| Stations list | 6 hours |
| Observations data | 10 min |
Coordinates round to 3 decimals (~100 m) when forming cache keys, so small jitter in caller-supplied coords doesn't fragment the cache. Different parameter sets cache separately so they never alias each other.
Collection10MinObservations = "10-minute-in-situ-meteorological-observations"
CollectionHourlyObservations = "hourly-in-situ-meteorological-observations"
CollectionHourlyObservationsValidated = "hourly-in-situ-meteorological-observations-validated"
CollectionDailyObservations = "daily-in-situ-meteorological-observations"
CollectionDailyObservationsValidated = "daily-in-situ-meteorological-observations-validated"
CollectionDailyMeanTemperature = "Tg1"
CollectionDailyMinTemperature = "Tn1"
CollectionDailyMaxTemperature = "Tx1"
CollectionDailyPrecipAmount = "Rd1"
CollectionDailyEvapotrans = "EV24"
CollectionWindStatistics = "wins50"go test ./... # unit tests, no network
go test -race ./...
KNMI_API_KEY=... go test -tags=integration ./... # live API
go run ./cmd/knmi -location amsterdam # CLI demo- Observations are sparse. KNMI's in-situ network is fixed sites (airports, offshore platforms, KNMI HQ in De Bilt). The library resolves your coordinates to the nearest station — Amsterdam center hits Schiphol, for example. If you need gridded data, this isn't the right API.
- No nowcast / HARMONIE. Those are HDF5/GRIB on KNMI's separate Open Data API.
- No write side. Read-only client.