# Samen Meten (RIVM)

Citizens, municipalities and researchers put up their own sensor boxes; RIVM collects their measurements in the Samen Meten data portal and produces calibrated series for particulate matter. On 16 October 2026 the API held 12,419 measuring points with 56,391 data streams: particulate matter (PM10, PM2.5), nitrogen dioxide, ammonia, temperature, humidity and air pressure, per hour. The largest group is Luftdaten (6,099 points).

- Publisher: Rijksinstituut voor Volksgezondheid en Milieu
- Area: Netherlands · Finest detail: measuring point (sensor box)
- Period: actueel, met historie per meetpunt · Refreshed: hourly
- Access: Open · Licence: niet vastgesteld ("Onze data zijn Open Data", zonder licentienaam)
- Join keys: Thing-naam (SMC_20058), codegemeente, knmicode, dichtstbijzijnde Luchtmeetnet-station (no2closecode, pm25closecode …)
- Status: ok (2026-10-16) · PM2,5 6,61 ug/m3 (SMC_20058)
- URL: https://www.samenmeten.nl/dataportaal/api-application-programming-interface

## What can you do with it?
- **How dirty is the air in my street?.** Find the measuring points around an address with geo.distance and fetch the calibrated particulate series (pm25_kal). Around the Utrecht Dom tower there are 10 points within 0.005 degrees.
- **Who is measuring?.** Every point belongs to a project: Luftdaten (6,099), Palmes (1,837), Luchtclub (704), Hollandse Luchten (573). The chart on this page counts them.
- **Citizen sensor next to the official station.** Every point names the nearest Luchtmeetnet station (e.g. no2closecode NL10131). Put the two series side by side and see how well the calibration works.
- **Show fireworks and wood burning.** There are projects named Vuurwerk (fireworks, 75 points) and Houtstook (wood burning, 28). Look at the particulate peaks around New Year or on cold winter evenings.

## How to process it
1. **Measuring points of one project.** Things are the measuring points. Filter on properties/project; $count=true gives the count, $top at most 200 per page (follow @iot.nextLink).
   `https://api-samenmeten.rivm.nl/v1.0/Things?$filter=properties/project%20eq%20'Smartcitizen'&$count=true&$top=5`
2. **The latest values of a series.** Every series (Datastream) is named <point>-<number>-<quantity>. With $expand you fetch the latest values in the same call.
   `https://api-samenmeten.rivm.nl/v1.0/Datastreams?$filter=name%20eq%20'SMC_20058-21-pm25_kal'&$expand=Observations($top=2;$orderby=phenomenonTime%20desc)`
3. **Measuring points around a place.** geo.distance only works with SRID=4326 in the geography. Coordinates in the responses are rounded to three decimals (about 100 metres).
   `https://api-samenmeten.rivm.nl/v1.0/Locations?$filter=geo.distance(location,geography'SRID=4326;POINT(5.121314%2052.090654)')%20le%200.005&$expand=Things($select=name)`
4. **Which quantities exist.** Nine: pres, rh, temp, nh3, no2, pm25_kal, pm10_kal, pm25 and pm10. For particulate matter use the calibrated ones (_kal).
   `https://api-samenmeten.rivm.nl/v1.0/ObservedProperties`

## Interesting combinations
- Is the citizen sensor right? → [luchtmeetnet](https://www.opendataindex.nl/en/bron/luchtmeetnet.md) via no2closecode / pm25closecode (stationscode, bv. NL10131). Every point names its nearest, regional and city station. Fetch the same hours for that station from Luchtmeetnet and compare.
- More particulate matter with little wind? → [knmi-daggegevens](https://www.opendataindex.nl/en/bron/knmi-daggegevens.md) via knmicode (knmi_06375 = station 375). The KNMI station is listed for each point. Compare the daily wind speed with the daily mean particulate matter.

## Similar sources
- [luchtmeetnet](https://www.opendataindex.nl/en/bron/luchtmeetnet.md): The official network of RIVM, health services and provinces; far fewer points, but with reference equipment.
- [rivm-gcn](https://www.opendataindex.nl/en/bron/rivm-gcn.md): Calculated annual averages on a map of the whole country, not hourly measurements.
- [eea-lucht](https://www.opendataindex.nl/en/bron/eea-lucht.md): Official measurements across Europe, comparable between countries.

## What to watch out for
- **Many points no longer measure.** The API counts every point ever registered. A Palmes point we looked at had no measurements at all. Check the latest value per series.
- **Raw sensor values are unreliable.** Cheap particulate sensors can be far off, for example at high humidity. Use the calibrated series (pm25_kal, pm10_kal), not pm25 and pm10.
- **Broad queries time out.** Observations without a filter, or an $expand over many points at once, returned a 504 (Gateway Time-out). A filter on Observations/phenomenonTime from Datastreams returned an empty reply. Work per series.
- **Licence not named.** Samen Meten says "Our data are Open Data" without naming a licence. An older Utrecht dataset on data.overheid.nl is CC0, but for the RIVM portal this is not established.
- **Locations rounded.** Coordinates have three decimals (about 100 metres). Enough for a neighbourhood, not for a house number.

## API (OGC SensorThings API 1.0 (OData-achtig, JSON))

- Base: https://api-samenmeten.rivm.nl/v1.0
- Access: No key; the API is public (tested). (tested)
- Formats: json
- Size: 12425 Things (sensor boxes) with 56,418 datastreams, 24,613 locations and 52 sensor types ($count, 16 Oct 2026) (tested)
- Filtering: OData-like expressions in $filter (SensorThings), also on properties/… and with geo.distance(location, geography'SRID=4326;POINT(lon lat)') le <degrees>.; eq, ne, gt, ge, lt, le, and, or, startswith(), geo.distance()

### Endpoints
- `GET /Things`: Sensor boxes; properties with project, owner, municipality code and the nearest official stations per substance (no2closecode, pm25regiocode …).
- `GET /Datastreams`: One series per box and quantity, named <box>-<no>-<quantity> (SMC_20058-21-pm25_kal), with unit.
- `GET /Datastreams({id})/Observations`: Hourly values of one series (phenomenonTime, result).
- `GET /Observations`: All observations; always filter, the whole set is too big to count.
- `GET /ObservedProperties`: The 9 quantities: pm10, pm25, pm10_kal, pm25_kal (calibrated), no2, nh3, temp, rh, pres.
- `GET /Sensors`: The 52 sensors by name (Sensirion SEN5x, AirVisual …); the number in description reappears in the datastream name.
- `GET /Locations`: Box locations (GeoJSON point); search nearby with geo.distance.
- `GET /HistoricalLocations`: Earlier locations of a box (moved).
- `GET /FeaturesOfInterest`: What an observation is about (usually the location).

### Parameters
- `$filter`: Filter, e.g. name eq 'SMC_20058-21-pm25_kal', properties/project eq 'Smartcitizen', phenomenonTime ge 2026-10-16T00:00:00Z, startswith(name,'ZWL'). (tested)
- `$expand`: Include related entities, with their own options: Observations($top=2;$orderby=phenomenonTime desc). (tested)
- `$select`: Only these fields (phenomenonTime,result). (tested)
- `$orderby`: Sort. (tested)
- `$top / $skip`: Paging; $top at most 200 (more silently becomes 200). (tested)
- `$count`: Total in @iot.count. (tested)

### Fields in the response (selection) (8)
`@iot.id` integer, `name` tekst, `properties.project / owner` tekst, `properties.codegemeente` tekst, `unitOfMeasurement.symbol` tekst, `phenomenonTime` datetime, `result` getal, `location.coordinates` punt

### Limits and pitfalls
- $top at most 200 per page; follow @iot.nextLink for the rest. (tested)
- Counting over all Observations or all active boxes timed out (HTTP 504). Count per series or per project. (experience)
- Particulate matter has two series per box: raw (pm25, pm10) and calibrated (pm25_kal, pm10_kal, 'fijnstof gekalibreerd' in ObservedProperties). Compare only the _kal series with official measurements. (tested)
- geo.distance only works with geography'SRID=4326;POINT(lon lat)', distance in degrees (0.005 ≈ 350 m east-west). (experience)
- A mistake in $filter returns 400 with the raw database error ('pq: column "bogus" does not exist'): handy to check field names. (tested)
- Each box lists in properties the nearest official Luchtmeetnet stations per substance: compare directly with the reference measurement. (tested)

### Example
```
https://api-samenmeten.rivm.nl/v1.0/Datastreams?$filter=name%20eq%20'SMC_20058-21-pm25_kal'&$expand=Observations($top=3;$orderby=phenomenonTime%20desc)
```

## How to fetch it

### curl
```bash
curl "https://api-samenmeten.rivm.nl/v1.0/Datastreams?\$filter=name%20eq%20'SMC_20058-21-pm25_kal'&\$expand=Observations(\$top=24;\$orderby=phenomenonTime%20desc)"
```

### python
```python
import requests

basis = "https://api-samenmeten.rivm.nl/v1.0"
r = requests.get(f"{basis}/Datastreams", params={
    "$filter": "name eq 'SMC_20058-21-pm25_kal'",
    "$expand": "Observations($top=24;$orderby=phenomenonTime desc)"}).json()
for o in r["value"][0]["Observations"]:
    print(o["phenomenonTime"], o["result"], "µg/m³")
```

## What you can build with it
- [Air quality alert for your own street](https://www.opendataindex.nl/en/bouwen/luchtkwaliteit-alert.md): A notification on your phone when the air at home is bad or about to get bad, based on the Luchtmeetnet forecast, the monitoring stations in your city, neighbours' sensors and the RIVM annual map.


## Examples with real figures (1)

For each example the finding, the reference date and the call that fetches exactly these figures.

- **Samen Meten measuring points by project: most Luftdaten (6,103 measuring points), then Palmes and Luchtclub** (oktober 2026). Table SensorThings Things. [Example](https://www.opendataindex.nl/en/voorbeeld/samenmeten-meetpunten-per-project)
  - `https://api-samenmeten.rivm.nl/v1.0/Things?$filter=properties/project%20eq%20'Smartcitizen'&$count=true&$top=0`
  - `https://api-samenmeten.rivm.nl/v1.0/Things?$top=200&$select=properties&$count=true`
