# NL Emission Zones Monitor (Milieuzones & Zero-Emission Zones) (`codeclouds/nl-emissiezones-monitor`) Actor

Landelijke, live NDW-databron van alle Nederlandse milieuzones en zero-emissiezones per gemeente, met wijzigingsdetectie (nieuw/gewijzigd/ingetrokken) tussen runs.

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

## Pricing

from $3.00 / 1,000 zone-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 Emission Zones Monitor (Milieuzones & Zero-Emission Zones) 🚫🚗

Every Dutch environmental zone (milieuzone) and zero-emission zone in one call — straight from **NDW** (Nationale Databank Wegverkeersgegevens), the official Dutch road-traffic-data cooperative of Rijkswaterstaat, provinces, and municipalities. No login, no API key, no scraping: this is a direct government GeoJSON feed, with built-in change detection between runs.

### Why this actor?

The Netherlands is in the middle of an active regulatory wave: municipalities are rolling out zero-emission zones for delivery traffic in phases from 2025 through 2027, on top of existing classic environmental zones (milieuzones) that restrict older diesel trucks and vans. Every municipality used to publish this separately — Amsterdam, Rotterdam, Utrecht, Den Haag, and 23 others each with their own dataset — until NDW started aggregating all of them into a single national feed.

This actor turns that raw feed into a clean, filterable, monitorable dataset:

- 🇳🇱 **Full national coverage, zero configuration** — 37 zones across 27 municipalities in one call, no pagination
- 🚦 **Both zone types in one place** — `ZERO_EMISSION_ZONE` (ZE zones for vans/trucks) and `ENVIRONMENTAL_ZONE` (classic milieuzones)
- 🚛 **Vehicle-type exemptions included** — per zone, which vehicle types (car/van/truck/bus) and Euro-classification are exempt
- 🔜 **Future zones already visible** — the source includes zones with a `validFrom` date years ahead (e.g. Amersfoort/Apeldoorn/Deventer/Ede 2027), flagged as "not yet active" so you can plan ahead
- 🔔 **Built-in change detection** — enable monitor mode to flag new, changed, or withdrawn zones between runs, instead of re-processing a static snapshot every time
- 🚫 **No personal data** — geographic zones, vehicle categories, and traffic orders only; no license plates or individual drivers

### When should an AI agent use this?

- "Which Dutch cities have a zero-emission zone for delivery vans right now?"
- "Is there an environmental zone in Utrecht, and which vehicles are exempt?"
- "Which new emission zones are planned for 2027?"
- "Alert me if any Dutch municipality changes or withdraws an emission zone."
- "List every municipality with a milieuzone for trucks and the required Euro classification."
- "Compare zero-emission zone rollout dates across Dutch cities."

### What this Actor does

- Fetches the full national zone feed from the NDW `emission-zones/v1/map` GeoJSON endpoint
- Maps every raw feature into a flat, analysis-ready JSON object
- Flags zones whose `validFrom` date is still in the future (`isNogNietActief`)
- Filters by municipality, zone type, and/or active status
- Optional monitor mode: compares against the previous run (via the actor's key-value store) and marks each zone as new, changed, withdrawn, or unchanged

### Input

| Field | Type | Description |
|---|---|---|
| `gemeenten` | string\[] | Exact municipality names (case-insensitive), e.g. `"Amsterdam"`. Leave empty for all municipalities. |
| `zoneTypes` | string\[] | Filter on `"ZERO_EMISSION_ZONE"` and/or `"ENVIRONMENTAL_ZONE"`. Leave empty for both. |
| `alleenActief` | boolean | Hide zones not yet in effect (`validFrom` in the future). Default `false`. |
| `includeGeometrie` | boolean | Include the raw GeoJSON polygon/multipolygon per zone. Increases payload size. Default `false`. |
| `maxResults` | integer | Cap on output records per run (default `1000`, max `5000`). |
| `monitorMode` | boolean | Compare against the previous run and flag new/changed/withdrawn zones. Default `false`. |
| `onlyChanges` | boolean | With monitor mode on, only output changes, skip unchanged zones. Default `false`. |
| `webhookUrl` | string | Optional webhook posted with a summary after the run, only when monitor mode found at least one change. |
| `adresLookup` | string | Free-text address (e.g. `"Damrak 1, Amsterdam"`) to check against every zone's polygon (point-in-polygon). |

### Output

```json
{
  "zoneId": "acbf0f7f-56a6-4b36-9a66-4b6d0c69d474",
  "zoneNaam": "Milieuzone Amsterdam",
  "zoneType": "ENVIRONMENTAL_ZONE",
  "status": "DEFINITIVE",
  "gemeente": "Amsterdam",
  "gemeenteCode": "GM0363",
  "provincie": "Noord-Holland",
  "geldigVanaf": "2025-01-01T00:00:00Z",
  "geldigTot": "2030-01-01T00:00:00Z",
  "isNogNietActief": false,
  "dagenTotIngang": null,
  "regulationId": "gmb-2024-272098",
  "informatieUrl": "https://www.amsterdam.nl/verkeer-vervoer/milieuzone-uitstootvrijezone/",
  "uitzonderingen": [
    { "vehicleType": "CAR", "classification": "EURO5", "vehicleTypeLabel": "Personenauto", "classificationLabel": "Euro 5" },
    { "vehicleType": "VAN", "classification": "EURO5", "vehicleTypeLabel": "Bestelwagen", "classificationLabel": "Euro 5" },
    { "vehicleType": "TRUCK", "classification": "EURO6", "vehicleTypeLabel": "Vrachtwagen", "classificationLabel": "Euro 6" },
    { "vehicleType": "BUS", "classification": "EURO6", "vehicleTypeLabel": "Bus", "classificationLabel": "Euro 6" }
  ],
  "bron": "NDW Emissiezones"
}
```

`provincie` is derived from `gemeenteCode` via a small static lookup table covering the municipalities currently in the NDW feed; it is `null` for a (currently unseen) municipality code outside that table. `dagenTotIngang` is only filled in for zones where `isNogNietActief` is `true` — the number of days until `geldigVanaf` kicks in — and is `null` once a zone is already active.

With `monitorMode: true`, every record additionally includes a `"wijziging"` field: `"nieuw"`, `"gewijzigd"`, `"ingetrokken"`, or `"ongewijzigd"`.

#### Notification webhook (`webhookUrl`, optional, separately charged)

With `webhookUrl` set and `monitorMode: true`, the actor POSTs a JSON summary after the run — but only when at
least one new/changed/withdrawn zone was actually found, so a quiet run never fires the webhook:

```json
{
  "nieuweZones": 1,
  "gewijzigdeZones": 0,
  "ingetrokkenZones": 0,
  "zones": [{ "zoneId": "acbf0f7f-...", "zoneNaam": "Milieuzone Amsterdam", "wijziging": "nieuw" }]
}
```

Charged as `notificatie-verzonden` (once per run, not per zone), and only when the POST actually succeeds — a
failing webhook logs a warning but never fails the run or gets charged.

#### Address-to-zone lookup (`adresLookup`, optional, separately charged)

With `adresLookup` set to a free-text address (e.g. `"Damrak 1, Amsterdam"`), the actor geocodes it via the
public PDOK Locatieserver (the same authentication-free government source used by this developer's own
[PDOK Locatieserver](../pdok-locatieserver) actor), then checks the resulting point against every zone's
polygon (point-in-polygon, including holes in the geometry). One extra dataset item is pushed:

```json
{
  "recordType": "adres_zone_check",
  "adres": "Damrak 1, Amsterdam",
  "gegeocodeerd": { "lon": 4.8929, "lat": 52.3762 },
  "matches": [ /* zero or more full zone records, same shape as above, that contain this point */ ],
  "aantalMatches": 1,
  "bron": "NDW Emissiezones + PDOK Locatieserver"
}
```

Charged as `zone-match-resultaat`, only when the geocode actually succeeds — an address that geocodes but
falls outside every zone still returns a valid (empty-`matches`) answer and is charged, since that's a real
compliance answer too. An address PDOK can't geocode at all is not charged.

### Use cases

- **Logistics & fleet planning** — check which cities restrict your fleet's vehicle types before routing
- **Compliance / fleet-management software** — feed a single authoritative national dataset instead of maintaining 27 separate municipal integrations
- **Change monitoring** — run weekly with `monitorMode: true` to get alerted the moment a new zone is announced or an existing one's dates change
- **AI agents & MCP tools** — flat JSON, small input schema, direct yes/no "is this zone active" signal
- **Navigation apps / route planners** — build restriction layers from an authoritative government source

### Pricing

This Actor uses Apify's Pay-Per-Event (PPE) pricing model.

- **Actor Start:** $0.00005 (Apify default)
- **zone-result:** $0.003 per plain zone record
- **zone-wijziging-alert:** $0.01 per zone flagged as new, changed, or withdrawn in monitor mode — priced higher because this is the actionable signal, not just a data point
- **notificatie-verzonden:** $0.0015 per run, only with `webhookUrl` set, monitor mode on, at least one change found, and the POST actually succeeding
- **zone-match-resultaat:** $0.03 per address lookup, only with `adresLookup` set and the address actually geocoded

### Legal

- Source: **NDW** (Nationale Databank Wegverkeersgegevens), a Dutch government road-traffic-data cooperative between Rijkswaterstaat, provinces, and municipalities. Documented at `docs.ndw.nu/en/producten/emissiezones/`.
- No authentication, no scraping — this is a direct, public REST/GeoJSON API intended for reuse.
- No personal data of any kind. Zones, vehicle *categories* (not license plates), and public traffic orders only.

### FAQ

**Q: What's the difference between a "zero-emission zone" and an "environmental zone" (milieuzone)?**
A: An environmental zone (`ENVIRONMENTAL_ZONE`) restricts older, more polluting vehicles based on Euro emission classification (e.g. no diesel trucks below Euro 6). A zero-emission zone (`ZERO_EMISSION_ZONE`) is stricter and, once fully in effect, only allows zero-emission vehicles for commercial/delivery traffic.

**Q: A zone has `validFrom` in 2027 — why is it already in the dataset?**
A: NDW publishes confirmed future traffic orders as soon as the municipality's decision is definitive/planned, even years ahead. This actor surfaces that as `isNogNietActief: true` so you can plan for upcoming changes rather than being surprised by them.

**Q: How does monitor mode work?**
A: When `monitorMode` is enabled, the actor stores a snapshot (per-zone status + validity dates) in its key-value store after each run and compares against it on the next run. Set `onlyChanges: true` to only receive the zones that are new, changed, or withdrawn.

**Q: How often does NDW update this feed?**
A: Traffic orders don't change daily — a weekly scheduled run with `monitorMode: true` is more than sufficient to catch new announcements.

### Related Actors

Also by this developer:

- **[NL Parking Monitor](../nl-parking-monitor)** — same profile: a direct, authentication-free Dutch open road-traffic-data feed (RDW), no scraping involved.
- **[NL Ziekenhuis Wachttijden Monitor](https://apify.com/codeclouds/nl-ziekenhuis-wachttijden-monitor)** — same profile: a direct, authentication-free Dutch government REST feed, no scraping involved.

***

*Keywords: emissiezone, milieuzone, zero-emissiezone, ndw, low emission zone netherlands, environmental zone, ze-zone, dutch traffic regulation, fleet compliance.*

### Keywords

netherlands, environmental-zone, emission-zone, milieuzone, ndw, open-data, government-data, logistics, mcp-tool

### Changelog

#### 0.3.0 - Notification webhook & address-to-zone lookup

- New `webhookUrl` input: after a `monitorMode` run, POSTs a JSON summary of new/changed/withdrawn zones —
  only when at least one change was found and only charged (`notificatie-verzonden`) on a successful POST.
- New `adresLookup` input: geocodes a free-text address via the public PDOK Locatieserver and checks it
  against every zone's polygon (point-in-polygon, holes included). Pushes one `adres_zone_check` dataset item.
  Charged as `zone-match-resultaat`, only on a successful geocode.

#### 0.2.2

- Added `dagenTotIngang` — number of days until a planned/not-yet-active zone's `validFrom` kicks in
  (`null` for zones already active). No new event, just an extra field on the existing record.
- Added `provincie` — the province derived from `gemeenteCode` via a small static lookup table
  (`null` for a municipality code outside that table). No pricing change.
- `uitzonderingen` entries now include `vehicleTypeLabel`/`classificationLabel` alongside the raw
  NDW enum values (e.g. `"TRUCK"` → `"Vrachtwagen"`, `"EURO6"` → `"Euro 6"`), for non-technical
  users and reporting. Unknown enum values fall back to the raw value. No pricing change.

#### 0.2.1 - Bugfix

- Fixed `monitorMode` change detection never seeing prior-run state: the actor was reading/writing its
  snapshot to Apify's per-Run *default* key-value store, which is not shared between separate runs. Now uses a
  named, persistent key-value store. A new baseline will be captured on the next run.

#### 0.2.0

- Change detection (`monitorMode`) now also compares `uitzonderingen`/exemptions between runs, not just
  status/validity dates — a stricter Euro-class requirement for the same vehicle type now correctly triggers
  a `gewijzigd` signal. Comparison is order-independent (a different JSON array order alone is not a change).
  No pricing change.

#### 0.1.0 - Initial release

- Fetch all Dutch environmental/zero-emission zones from the NDW national GeoJSON feed.
- Filter by municipality, zone type, and active status.
- Optional monitor mode with new/changed/withdrawn change detection via the key-value store.
- Pay-per-event pricing: base zone record vs. higher-value change alert.

# Actor input Schema

## `gemeenten` (type: `array`):

Exact municipality names (case-insensitive), e.g. "Amsterdam", "Rotterdam". Leave empty to fetch all municipalities.

## `zoneTypes` (type: `array`):

Filter on zone type. Leave empty for both types.

## `alleenActief` (type: `boolean`):

Hide zones that are not yet in effect (validFrom date is in the future).

## `includeGeometrie` (type: `boolean`):

Include the raw GeoJSON polygon/multipolygon per zone. Increases output payload size significantly.

## `maxResults` (type: `integer`):

Caps how many output records this run produces (1-5,000).

## `monitorMode` (type: `boolean`):

Compare this run against the previous run (stored in the actor's key-value store) and flag new, changed, or withdrawn zones.

## `onlyChanges` (type: `boolean`):

When monitor mode is on, only output new/changed/withdrawn zones and skip unchanged ones.

## `webhookUrl` (type: `string`):

Optional webhook posted with a summary after the run, only when monitor mode is on and at least one new/changed/withdrawn zone was found. New charged event notificatie-verzonden ($0.0015 per run), only on a successful POST.

## `adresLookup` (type: `string`):

Free-text address (e.g. "Damrak 1, Amsterdam"), geocoded via the public PDOK Locatieserver, then checked against every zone's polygon (point-in-polygon). New charged event zone-match-resultaat ($0.03), only on a successful geocode.

## Actor input object example

```json
{
  "gemeenten": [],
  "zoneTypes": [],
  "alleenActief": false,
  "includeGeometrie": false,
  "maxResults": 1000,
  "monitorMode": false,
  "onlyChanges": 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-emissiezones-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-emissiezones-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-emissiezones-monitor --silent --output-dataset

```

## MCP server setup

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

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/AfBtqD6RMoGbqVvTl/builds/tUJUnkmSPMO02wzfQ/openapi.json
