# NL Waterstanden & Watertemperatuur Monitor (`codeclouds/nl-waterstanden-monitor`) Actor

Real-time water levels, water temperature and other Rijkswaterstaat measurements for Dutch waterways. Filters out decades-old zombie readings and flags stations whose feed has gone stale.

- **URL**: https://apify.com/codeclouds/nl-waterstanden-monitor.md
- **Developed by:** [Dennis](https://apify.com/codeclouds) (community)
- **Categories:** Travel, Lead generation
- **Stats:** 2 total users, 1 monthly users, 66.7% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 waterdata-results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## NL Waterstanden & Watertemperatuur Monitor 🌊

Real-time **water levels, water temperature and other Rijkswaterstaat measurements** for Dutch waterways — straight from RWS's official Waterwebservices (Data Distributielaag). No login, no API key: this is the same public feed that powers [waterinfo.rws.nl](https://waterinfo.rws.nl).

### Why this actor?

The source API's "latest observation" endpoint doesn't just return one current value — it returns **one entry per underlying data-owner series** for a location/parameter, and most of those turn out to be decades-old historical archives mixed in with the real live reading. In testing, a single query for one location's water level returned 9 candidate "latest" values with timestamps ranging from **1900 to today** — only one of them was actually current.

This actor does the cleanup for you:

- ✅ **True-latest selection** — picks the genuinely most recent reading per location+parameter, discarding the historical noise
- 🚨 **Stale-station detection** — flags a reading as `stale` when its age exceeds a threshold you set, surfacing measurement stations whose feed has effectively gone offline (some go years without an update)
- 📭 **Explicit "unavailable" records** — if a location doesn't measure a requested parameter, you get a clear `unavailable` record instead of the combination silently vanishing
- 🌡️ **Multi-parameter in one call** — water level, temperature, wind, salinity, visibility and more, per location, in a single run
- 📍 **Named, geocoded locations** — resolved from RWS's own location catalog, no separate lookup needed
- ⚠️ **Value threshold alerts** — optionally flag when a fresh reading itself crosses a threshold you set (e.g. high water, low water, minimum swim temperature), not just when the sensor feed goes stale

### When should an AI agent use this?

- "What's the current water level at Hoek van Holland, and is that reading actually up to date?"
- "Is the water temperature near Rotterdam right now suitable for swimming or sailing?"
- "Which RWS measurement stations near a waterway have gone offline or stopped reporting recently?"
- "Give me the wind, salinity and visibility readings for a location, not just the water level."
- "Find the RWS location code for a place name so I can monitor it in future runs."
- "Check the water level and temperature before I head out on the water today."

### What data do you get?

```json
{
  "locationCode": "hoekvanholland",
  "locationName": "Hoek van Holland",
  "location": { "lat": 51.976899, "lon": 4.119827 },
  "grootheid": "WATHTE",
  "grootheidLabel": "Waterhoogte",
  "eenheid": "cm",
  "compartiment": "OW",
  "hoedanigheid": "NAP",
  "hoedanigheidLabel": "t.o.v. Normaal Amsterdams Peil",
  "value": -35,
  "timestamp": "2026-07-09T13:50:00.000Z",
  "ageMinutes": 34,
  "dataQuality": "fresh",
  "statuswaarde": "Ongecontroleerd",
  "thresholdAlert": null,
  "temperatureDifference": null,
  "trend": null
}
```

#### Water vs. air temperature (`includeAirTemperature`, optional)

By default, requesting grootheid `"T"` returns only the water variant (OW) — the air variant (LT) is silently
filtered out. Enable `includeAirTemperature: true` to get both variants for locations that report them, each
with a `temperatureDifference` field (water minus air, e.g. `-4` means the water is 4°C colder than the air).
`temperatureDifference` stays `null` when only one variant is fresh (or available at all) for that location —
never a guessed value.

A station whose feed has gone dark instead returns:

```json
{
  "locationCode": "rotterdam.dintelhaven",
  "grootheid": "WATHTE",
  "value": 34,
  "timestamp": "2003-01-14T22:00:00.000Z",
  "ageMinutes": 12370555,
  "dataQuality": "stale"
}
```

### Use cases

- **Vaarwater-planning** — check current water level and temperature before heading out
- **Jachthaven / marina dashboards** — combine with a berth-rate feed for a complete "can I get in, and what does it cost" picture
- **Station health monitoring** — schedule runs with a tight `staleAfterMinutes` to get alerted when a measurement point you rely on stops reporting
- **AI agents & MCP tools** — flat JSON with a small input schema; one call answers "what's the water level/temperature at X right now, and can I trust that number?"
- **Research & dashboards** — pull water temperature trends across many locations at once

### Quick start

**Zero-config**: returns water levels for the first 25 locations in the catalog.

**Specific location, multiple parameters:**

```json
{ "locationCodes": ["hoekvanholland"], "grootheden": ["WATHTE", "T"] }
```

**Search by name:**

```json
{ "query": "Rotterdam", "grootheden": ["WATHTE"], "maxLocations": 50 }
```

**Tight staleness check (e.g. hourly monitoring of a critical station):**

```json
{ "locationCodes": ["hoekvanholland"], "grootheden": ["WATHTE"], "staleAfterMinutes": 30 }
```

### Input reference

| Field | Description |
|---|---|
| `locationCodes` | Exact RWS location codes to fetch (skips name search) |
| `query` | Case-insensitive name search, e.g. a place name |
| `grootheden` | Parameter codes to fetch per location (default `["WATHTE"]`) — common: `WATHTE` water level, `T` temperature, `WINDSHD`/`WINDRTG` wind, `SALNTT` salinity, `ZICHT` visibility |
| `maxLocations` | Cap on locations queried per run (default `25`, max `500`) |
| `staleAfterMinutes` | Age threshold for the `stale` flag (default `120`) |
| `alertThresholds` | Optional array of `{ grootheid, drempel, richting }` — alert when a **fresh** reading itself crosses a value threshold. `richting: "boven"` alerts when value >= `drempel` (e.g. high water); `"onder"` alerts when value <= `drempel` (e.g. low water, or a minimum swim temperature) |
| `includeAirTemperature` | Return both the water (OW) and air (LT) variant of `T`, plus a `temperatureDifference` field on each (default `false`) |
| `refreshCatalog` | Bypass the 24-hour metadata-catalog cache and force a fresh fetch (default `false`) |
| `trackTrend` | Compare each fresh reading against the previous run and flag rising/falling/unchanged (default `false`) |

Note: when a parameter code exists in both water (`OW`) and air (`LT`) compartments (e.g. `T` for temperature), this actor prefers the water reading by default — unless `includeAirTemperature` is set.

**Threshold alerts example** — flag high water above 250cm and low temperature below 12°C:

```json
{
  "locationCodes": ["hoekvanholland"],
  "grootheden": ["WATHTE", "T"],
  "alertThresholds": [
    { "grootheid": "WATHTE", "drempel": 250, "richting": "boven" },
    { "grootheid": "T", "drempel": 12, "richting": "onder" }
  ]
}
```

A matching record gets `"thresholdAlert": { "drempel": 250, "richting": "boven" }` — only evaluated against
`fresh` readings, never `stale`/`unavailable` ones.

#### Trend between runs (`trackTrend`, optional, separately charged)

With `trackTrend: true`, the actor stores each fresh reading's value per location+parameter in its key-value
store after every run and compares against it on the next one:

```json
{ "trend": { "vorigeWaarde": 100, "delta": 20, "richting": "oplopend" } }
```

`richting` is `"oplopend"`/`"dalend"`/`"gelijk"` — unlike some other actors in this portfolio there's no
noise-margin band, since a continuous measurement like water level is meaningfully directional even for small
changes (like a tide moving). Stays `null` on the first run, or for a location+parameter combination never seen
before. Charged as `trend-signaal`, only for readings actually flagged `"oplopend"` or `"dalend"`.

### Pricing

Pay per event: a normal fresh/unavailable record is billed at the base `waterdata-result` rate; a detected **`stale-data-alert`** (a station's true-latest reading is older than your threshold) is billed at a higher rate — you pay more for the actionable "this station may be down" signal than for a plain data point. A detected **`threshold-alert`** (a fresh reading crosses a value threshold you configured via `alertThresholds`) is billed separately, on top of the base rate. A detected **`trend-signaal`** (a fresh reading actually rose or fell compared to the previous run, with `trackTrend` enabled) is billed separately too.

### Data source & reliability

- Source: **Rijkswaterstaat Waterwebservices** (Data Distributielaag v2.0) — public domain (CC0), no authentication required
- Location and parameter metadata comes from RWS's own catalog (~2,600 locations, refreshed each run)
- One broken batch never fails the whole run — errors are collected in the run's key-value store (`RUN_SUMMARY`)
- Only environmental/infrastructure measurements — no personal data of any kind

### FAQ

**Why did I get a `stale` record instead of a normal reading?** The most recent value RWS has for that exact location+parameter combination is older than your `staleAfterMinutes` setting. This can mean the sensor is temporarily offline, or that location never measured that parameter in real time to begin with — check `timestamp`/`ageMinutes` for the actual age.

**Why is a combination `unavailable`?** The location doesn't publish that parameter at all. Try a different `grootheid` or check nearby locations.

**How do I find a location's code?** Run once with a `query` (e.g. a city or waterway name) — the returned `locationCode` is reusable in future runs.

**Does `T` mean air or water temperature?** Both exist in the source data; this actor defaults to the water-compartment (`OW`) reading when a code is ambiguous.

### Related Actors

Also by this developer:

- **[NL Parking Rates & Live Availability Monitor](../nl-parking-monitor)** — same profile: a real-time Dutch open-government-data monitoring feed, no login/API key required, and change detection between runs (there: parking status flips; here: stale-station detection).

***

*Keywords: rijkswaterstaat, waterstanden nederland, waterinfo, watertemperatuur, waterhoogte NAP, vaarwater data, RWS open data, real-time water level API, MCP water data tool.*

### Keywords

netherlands, rijkswaterstaat, water-level, waterstanden, open-data, real-time, environmental-data, mcp-tool

### Changelog

#### 0.5.0 - Trend between runs

- Added `trackTrend` (opt-in) — compares each fresh reading against the previous run (via the key-value
  store), exposed as `trend` (`{ vorigeWaarde, delta, richting }`). New `trend-signaal` event ($0.01), only
  charged for readings actually flagged `"oplopend"` or `"dalend"`.

#### 0.4.0

- Added `includeAirTemperature`: returns both the water (OW) and air (LT) variant of `T` (when a location
  reports both), plus a `temperatureDifference` field on each. No pricing change — both variants are still
  billed as normal `waterdata-result`/`stale-data-alert` records.
- The RWS metadata catalog (all locations + parameters/units/compartments) is now cached in the key-value
  store for 24 hours instead of being fully re-fetched every run, cutting one full catalog request per run
  for scheduled monitoring. Add `refreshCatalog: true` to bypass the cache. No pricing change.

#### 0.3.1 - Bugfix

- Records with `dataQuality: 'unavailable'` (no measurement delivered at all for that location+parameter) are
  no longer charged as a normal `waterdata-result` — the customer received no data, so they aren't billed for it.

#### 0.3.0

- Added `alertThresholds`: optionally flag when a fresh reading itself crosses a value threshold (high
  water, low water, minimum swim temperature), independent of the existing staleness check. New field
  `thresholdAlert` on each record. New charged event `threshold-alert` ($0.01), confirmed by the user
  (2026-07-14).

#### 0.2.0

- Added a free `gebiedssamenvatting` record at the end of multi-location runs: per grootheid, count of
  fresh/stale/unavailable readings plus average/min/max value across the fresh ones. No pricing change — not
  counted towards `waterdata-result`/`stale-data-alert`.

#### 0.1.0 - Initial release

- True-latest selection per location+parameter, filtering out decades-old zombie readings.
- Stale-station detection with configurable `staleAfterMinutes` threshold.
- Multi-parameter support (water level, temperature, wind, salinity, visibility) per location, in a single run.
- Named, geocoded locations resolved from RWS's own location catalog.

# Actor input Schema

## `locationCodes` (type: `array`):

Exact RWS location codes to fetch, e.g. "hoekvanholland". Skips the name search entirely. Leave empty to search by name/query instead.

## `query` (type: `string`):

Case-insensitive text match against the location name, e.g. "Rotterdam" or "IJssel". Ignored if Location codes is set. Leave empty to browse all locations.

## `grootheden` (type: `array`):

RWS "Grootheid" codes to fetch per location. Common ones: WATHTE (water level), T (temperature), WINDSHD (wind speed), WINDRTG (wind direction), SALNTT (salinity), ZICHT (visibility).

## `maxLocations` (type: `integer`):

Caps how many locations are queried per run (1-500).

## `staleAfterMinutes` (type: `integer`):

A location+parameter reading older than this is flagged "stale" instead of reported as current -- useful for detecting a station whose feed has gone offline.

## `alertThresholds` (type: `array`):

Optional: alert when a fresh reading itself crosses a threshold (not just its age). Each item: { "grootheid": "WATHTE", "drempel": 250, "richting": "boven" }. "boven" alerts when value >= drempel (e.g. high water), "onder" when value <= drempel (e.g. low water, or a minimum swim temperature).

## `includeAirTemperature` (type: `boolean`):

When grootheid "T" is requested: by default only the water variant (OW) is returned and the air variant (LT) is filtered out. Enable to get both variants, plus a temperatureDifference field (water minus air) on each.

## `refreshCatalog` (type: `boolean`):

The RWS metadata catalog (all locations + parameters/units/compartments) is cached in the key-value store for 24 hours instead of being fully re-fetched every run. Enable to bypass the cache and force a fresh catalog (e.g. after an RWS change).

## `trackTrend` (type: `boolean`):

Compares each fresh reading against the previous run (via the actor's key-value store) and flags a trend (rising/falling/unchanged) plus delta. New charged event trend-signaal ($0.01), only when a reading is actually flagged rising or falling. On the very first call (no previous snapshot yet) no location+parameter combination has a known prior value, so nothing is flagged/charged; the current fresh values are simply saved as the baseline. Uses a shared, persistent key-value store that scheduled monitoring runs also read/write, so an ad-hoc call in between can shift the baseline seen by the next scheduled run.

## Actor input object example

```json
{
  "locationCodes": [],
  "grootheden": [
    "WATHTE"
  ],
  "maxLocations": 25,
  "staleAfterMinutes": 120,
  "alertThresholds": [],
  "includeAirTemperature": false,
  "refreshCatalog": false,
  "trackTrend": false
}
```

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("codeclouds/nl-waterstanden-monitor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("codeclouds/nl-waterstanden-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call codeclouds/nl-waterstanden-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=codeclouds/nl-waterstanden-monitor",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/acts/lyDxoHUkrN37mditG/builds/nDAmsd2945VPghMje/openapi.json
