# NL Bodemrisico Check — Bodemloket per Adres (`codeclouds/nl-bodemrisico-check`) Actor

Zoekt bodemverontreinigingslocaties (Wbb) uit het landelijke Bodemloket op voor een NL-adres/postcode/coordinaat, met risicolabels, bevoegd gezag en een expliciete dekkingsindicatie per regio. Voor aannemers en projectontwikkelaars vóór grondverzet.

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

## Pricing

from $6.00 / 1,000 locatie-resultaats

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/actors/running/actors-in-store.md#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 Bodemrisico Check — Bodemloket per Adres 🇳🇱🟫

Check a Dutch address, postcode, place name, or coordinate against the **national Bodemloket** (soil
contamination register, Wbb locations) before starting excavation or groundwork — a fast, structured
pre-screen for soil contamination financial risk.

Excavation, land development and construction projects in the Netherlands can run into unexpected
remediation (sanering) costs if the site sits on a known or suspected soil contamination location under
the Wet bodembescherming (Wbb). The **Bodemloket** — a nationwide register maintained by the responsible
authorities (provinces, municipalities, environmental services) — publishes this data as a public GIS
service. This actor turns that into a simple per-address JSON lookup, including an honest "no coverage
data" signal so an empty result is never silently read as "no contamination".

### When should an AI agent use this?

- "Is there a known soil contamination location at this address before we start digging?"
- "Check these 5 candidate development sites for Bodemloket (Wbb) hits."
- "What's the remediation status of this former industrial site?"
- "Which authority is responsible for soil files at this location, and do they publish dossier data online?"
- "Screen a list of addresses for soil risk before submitting an excavation permit."

### What this Actor does

- Geocodes your input (postcode, address, place name, or `"lat,lon"`) via the official PDOK Locatieserver
- Runs a point-in-polygon lookup against the national **Bodemloket** ArcGIS service (Wbb-locaties layer)
- Returns every overlapping Wbb soil location, translated to plain-language Dutch labels (location type,
  contamination assessment, follow-up/remediation status)
- Looks up the responsible authority (bevoegd gezag) for the location
- Includes an explicit **coverage indicator**: whether the responsible authority publishes additional
  dossier data/web portal for this region — so "no result found" can be told apart from "no coverage data
  available for this region"
- Processes multiple locations per run, in parallel

### Who is this for?

- **Contractors & earthworks companies (grondverzetbedrijven)** — pre-screen a site before excavation to
  avoid unbudgeted remediation costs
- **Project developers** — early risk signal before acquiring or developing a plot
- **Environmental consultants** — quick first-pass check before commissioning a formal soil survey
  (bodemonderzoek)
- **AI agents** — structured JSON output, ideal as a tool for agentic due-diligence workflows

### Input

| Field | Type | Description |
|---|---|---|
| `locaties` | array of strings | Postcodes, addresses, place names, or `"lat,lon"` coordinates to check, e.g. `"Bathseweg 27, Rilland"`, `"4411BA"`, `"51.415,4.186"` |
| `concurrency` | integer | How many locations to process in parallel, 1-10 (default `3`) |
| `nabijheidsradiusMeter` | integer | Search for Wbb locations within this radius (meters) around the point instead of only exact overlap. `0` (default) = off, current point-only behaviour. Max `500`. |
| `monitorLocaties` | boolean | Compares each location's Wbb data with the previous run (via the actor's key-value store) and signals a new Wbb dossier or a changed `vervolgWbb` status. Default `false`. |

### Output

One result per input location:

```json
{
  "invoer": "Bathseweg 27, Rilland",
  "gevonden": true,
  "fout": null,
  "foutCode": null,
  "weergavenaam": "Bathseweg 27, 4411BA Rilland",
  "postcode": "4411BA",
  "gemeente": "Reimerswaal",
  "provincie": "Zeeland",
  "lat": 51.41533763,
  "lon": 4.18586885,
  "geocodingScore": 19.113255,
  "geocodingBetrouwbaarheid": "hoog",
  "wbbLocaties": [
    {
      "sikbId": null,
      "wbbDossierNummer": 126236372,
      "locatiecodeBevoegdGezag": "ZL070300019",
      "bisLoccode": "AA070302070",
      "typeCd": "21",
      "typeLabel": "Gesaneerd of in sanering",
      "vervolgWbb": "voldoende gesaneerd",
      "vervolgWbbLabel": "Voldoende gesaneerd",
      "statusver": null,
      "statusverLabel": "Geen beoordeling geregistreerd",
      "statusOordeel": "Onverdacht/Niet verontreinigd",
      "statusOordeelLabel": "Onverdacht / niet verontreinigd",
      "administrator": "Reimerswaal",
      "afstandMeter": null
    }
  ],
  "aantalWbbLocaties": 1,
  "bevoegdGezag": {
    "naam": "RUD Zeeland",
    "heeftWebsite": true,
    "websiteUrl": "https://zeeland.nazca4u.nl/Rapportage/",
    "heeftDossierdata": true
  },
  "dekkingBeschikbaar": false,
  "dekkingToelichting": "Geen dekkingsgegevens gevonden voor deze locatie in het Bodemloket — dit bevoegd gezag publiceert (nog) geen aanvullende dekkingsinformatie via deze laag.",
  "bron": "Bodemloket (Rijkswaterstaat, gis.gdngeoservices.nl/standalone/rest/services/blk_gdn/lks_blk_rd_v1)",
  "nabijheidsradiusToegepast": null
}
```

- **`geocodingBetrouwbaarheid`** (`"hoog"` / `"middel"` / `"laag"`) is an honest, best-effort classification
  based on PDOK's own relevance score (`geocodingScore`) — **not a guarantee**. PDOK's `/free` endpoint does
  fuzzy/best-effort matching and can return an irrelevant match even for a nonsensical address (see the FAQ
  below); `"laag"` means "verify this manually before relying on the result", not "this address doesn't exist".
  Direct `"lat,lon"` input is always `"hoog"` (no fuzzy matching involved).
- **`wbbLocaties`** is an array because multiple overlapping Wbb polygons can exist at a single point — an
  empty array is a **valid result**, not an error: it means no registered Wbb location overlaps this point.
- **`dekkingBeschikbaar`** reflects a *separate* Bodemloket layer (regional data-availability), not the
  Wbb-locaties layer itself. It can be `false` even when `wbbLocaties` is non-empty, and vice versa — always
  read both fields together. A `false` value means "this region's coverage/portal data isn't registered in
  this specific layer", **not** "this location has no contamination".
- Unknown/unmapped status or type codes are shown as `"Onbekende ... (code)"` with the raw code — labels are
  never guessed.
- If geocoding fails, `gevonden` is `false` and `fout` explains why.
- **`foutCode`** is a machine-readable classification of `fout`, for agents/scripts that need to decide
  automatically whether retrying makes sense: `"UPSTREAM_ERROR"` (a temporary PDOK/Bodemloket HTTP failure —
  safe to retry the same input) or `"ADRES_NIET_GEVONDEN"` (the input produced no geocoding result — retrying
  the same input won't change that; fix the input instead). `null` when there is no error.

#### Proximity radius (`nabijheidsradiusMeter`, optional, separately charged)

By default, only Wbb polygons that overlap the exact geocoded point are returned — a contamination location
20 meters away is missed. Set `nabijheidsradiusMeter` (e.g. `50`) to search within that radius instead, using
the same Bodemloket layer (no new data source). `nabijheidsradiusToegepast` reflects the radius actually used
(`null` in the default exact-point mode). Only charged the extra `nabijheidsradius-resultaat` event when at
least one location is found within the radius — a location for which nothing is found still costs only the
base `locatie-resultaat`.

In radius mode, each `wbbLocaties` item also gets an `afstandMeter` field (haversine distance in meters from
your point to the polygon's approximate centroid), and the array is sorted nearest-first — so with several
hits within range you immediately see which one is closest. `afstandMeter` stays `null` in the default
exact-point mode (the point is by definition inside the polygon, so distance isn't meaningful there).

#### Change monitoring (`monitorLocaties`, optional, separately charged)

With `monitorLocaties: true`, each location's Wbb data is compared with the previous run (via the actor's
key-value store, keyed on the normalized input string) — ideal for scheduled runs on a fixed shortlist of
locations (e.g. an active construction/remediation project):

```json
{
  "type": "wijziging_signaal",
  "invoer": "Bathseweg 27, Rilland",
  "signaalType": "status_gewijzigd",
  "wbbDossierNummer": 126236372,
  "vorigeVervolgWbb": "in sanering",
  "huidigeVervolgWbb": "voldoende gesaneerd"
}
```

`signaalType` is `"nieuw_dossier"` (a Wbb dossier that wasn't there last run) or `"status_gewijzigd"` (an
existing dossier's `vervolgWbb` value changed, e.g. from "in sanering" to "voldoende gesaneerd"). The very
first run for a location never produces a signal — there's nothing to compare against yet, not a guessed
"everything is new". Only charged the extra `wijziging-signaal` event when an actual change is detected.

#### Run summary (key-value store, free)

At the end of each run, a `RUN_SUMMARY` object is written to the actor's key-value store (visible in the Apify
Console run's storage tab) — no extra charge. It aggregates the whole batch: total locations processed, how
many had at least one Wbb location, how many errored, how many lacked `dekkingBeschikbaar`, and the
distribution of `geocodingBetrouwbaarheid` (hoog/middel/laag/onbekend). Useful for a portfolio-level overview
when screening dozens of addresses in one run, without scrolling through the full dataset.

### Use cases

**Pre-screen a shortlist of development sites:**

```json
{
  "locaties": ["Bathseweg 27, Rilland", "Industrieterrein Moerdijk", "52.09,5.12"]
}
```

**Single-address check before submitting an excavation permit:**

```json
{
  "locaties": ["4411BA"]
}
```

### Pricing

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

- **Actor Start:** $0.00005 (Apify default)
- **`locatie-resultaat`:** $0.006 per location checked (geocoding + Wbb-locaties lookup + bevoegd
  gezag + coverage indicator)
- **`nabijheidsradius-resultaat`:** $0.01 extra, only with `nabijheidsradiusMeter` enabled and only when at
  least one Wbb location is found within the radius
- **`wijziging-signaal`:** $0.012 extra, only with `monitorLocaties` enabled and only when an actual new
  dossier or status change is detected vs. the previous run

### Legal

- Data source: the **Bodemloket** (landelijk register van bodemverontreinigingslocaties), served via a
  public ArcGIS Server REST service (`gis.gdngeoservices.nl`), copyright Rijkswaterstaat. No authentication
  required.
- This is environmental/location data about land parcels, not personal data — no GDPR concerns.
- **Coverage is not uniform across the Netherlands** — the level of detail and dossier availability differs
  per responsible authority. This actor reports what the Bodemloket returns and makes the coverage gap
  explicit (`dekkingBeschikbaar`); it is **not a substitute for a formal soil survey (bodemonderzoek)** or
  legal/technical advice before excavation.
- Geocoding uses the official Dutch government **PDOK Locatieserver** (Kadaster/BZK), a free public service.

### FAQ

**Q: My address returned an empty `wbbLocaties` array — is the soil safe?**
A: It means no registered Wbb soil location overlaps this exact point in the Bodemloket. It does not mean
"no contamination has ever occurred" — always also check `dekkingBeschikbaar` for this region, and consult a
certified soil survey before relying on this for a real excavation decision.

**Q: What does `dekkingBeschikbaar: false` mean?**
A: The responsible authority for this region has not registered an additional web portal/dossier-data flag
in the Bodemloket's own coverage layer. It is informational, not a warning about the Wbb result itself.

**Q: My input was a nonsensical or misspelled address — why did I still get a result?**
A: PDOK's `/free` endpoint does fuzzy/best-effort matching and can return an unrelated (but real) location
even for input that doesn't correspond to any actual address. Check `geocodingBetrouwbaarheid`: a `"laag"`
value is a signal to verify the match manually before relying on it — it does not affect `wbbLocaties` itself,
only how much you should trust that the geocoded location is what you intended.

**Q: Why do some locations show multiple `wbbLocaties` entries?**
A: Wbb soil location polygons can overlap (e.g. an old dossier boundary and a newer one for the same site).
Each is reported separately so nothing is silently merged or dropped.

**Q: Why is `statusver`/`statusOordeel` sometimes `null`?**
A: Not every Wbb dossier has both fields filled in — this is common in the source data itself, not a mapping
error.

### Related Actors

- **[PDOK Adres Geocoding & Buurtdata](https://apify.com/codeclouds/pdok-locatieserver)** — the same official
  PDOK Locatieserver this actor uses internally for geocoding, useful if you need broader address/CBS
  neighbourhood-data lookups beyond soil risk.
- **[NL Faillissementen Monitor](https://apify.com/codeclouds/nl-faillissementen-monitor)** — a complementary
  due-diligence check: bankruptcy/receivership risk on the company side, soil-contamination risk on the
  property side.

***

*Zoekwoorden: bodemloket, bodemverontreiniging check, bodemrisico per adres, Wbb-locatie zoeken,
grondverzet risicocheck, saneringsstatus opzoeken, bodemonderzoek voorscreening, bevoegd gezag bodem.*

### Keywords

bodemloket, soil contamination, bodemverontreiniging, bodemrisico, wbb, grondverzet, sanering, netherlands,
due diligence, excavation risk

### Changelog

#### 0.5.0

- Added `foutCode`: a machine-readable error classification (`"UPSTREAM_ERROR"` retryable, `"ADRES_NIET_GEVONDEN"`
  not retryable, `null` when there is no error) alongside the existing human-readable `fout` text, so an agent
  can decide automatically whether retrying an item makes sense. No pricing change, no change to any
  business-logic calculation.

#### 0.4.0

- Added `monitorLocaties`: compares each location's Wbb data with the previous run and signals a new dossier
  or a changed `vervolgWbb` status. New charged event `wijziging-signaal` ($0.012), only when a change is
  actually detected. Confirmed by the user (2026-07-16).

#### 0.3.1

- Added a free `RUN_SUMMARY` written to the key-value store at the end of each run (total locations,
  count with a Wbb location, count with errors, count without `dekkingBeschikbaar`, and the
  `geocodingBetrouwbaarheid` distribution). No pricing change.
- Added `afstandMeter` per `wbbLocaties` item when `nabijheidsradiusMeter` is used: haversine distance in
  meters from the input point to the polygon's approximate centroid, with results sorted nearest-first.
  `null` in the default exact-point mode. Enrichment of the already-charged `nabijheidsradius-resultaat`
  event, no pricing change.

#### 0.3.0

- Added `nabijheidsradiusMeter`: search for Wbb locations within a radius around the point instead of only
  exact overlap, using the same Bodemloket layer's `distance`/`units` query parameters. New
  `nabijheidsradiusToegepast` field and charged event `nabijheidsradius-resultaat` ($0.01), confirmed by the
  user (2026-07-14, see docs/actor-verbeteringen/PRIJSBESLISSINGEN.md).

#### 0.2.0

- Added `geocodingScore` (raw PDOK relevance score) and `geocodingBetrouwbaarheid` (hoog/middel/laag,
  calibrated against live PDOK score data) so users can flag geocoding matches worth a manual check. No
  pricing change — enrichment of the existing per-location record.

#### 0.1.0

- Initial release: address/postcode-level Bodemloket (Wbb-locaties) lookup with translated status labels,
  bevoegd gezag, and an explicit regional coverage indicator.

# Actor input Schema

## `locaties` (type: `array`):

Een of meer NL-adressen, postcodes, plaatsnamen, of coordinaten ("lat,lon") om te controleren op bekende bodemverontreinigingslocaties (Wbb) uit het Bodemloket.

## `concurrency` (type: `integer`):

Hoeveel locaties tegelijk verwerkt worden (geocoding + Bodemloket-lookup per locatie).

## `nabijheidsradiusMeter` (type: `integer`):

Zoekt Wbb-locaties binnen deze straal rond het punt i.p.v. alleen exacte overlap (0 = uit, standaardgedrag). Alleen gecharged (nabijheidsradius-resultaat) als er daadwerkelijk locaties binnen de straal gevonden zijn.

## `monitorLocaties` (type: `boolean`):

Compares each location's Wbb data with the previous run (via the actor's key-value store) and signals a new Wbb dossier or a changed vervolgWbb status. New charged event wijziging-signaal ($0.012), only when a change is actually detected. A standalone one-off call for a location with no prior snapshot returns the normal Wbb result for that location (not empty) but deliberately produces no change signals yet, since there is nothing to compare against — it just establishes the baseline. This setting uses a shared, persistent key-value-store snapshot that scheduled monitoring runs also read/write, so an ad-hoc call in between can shift the baseline a later scheduled run compares against.

## Actor input object example

```json
{
  "locaties": [
    "Bathseweg 27, Rilland"
  ],
  "concurrency": 3,
  "nabijheidsradiusMeter": 0,
  "monitorLocaties": false
}
```

# Actor output Schema

## `results` (type: `string`):

Alle bodemrisico-resultaten in het default dataset.

# 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 = {
    "locaties": [
        "Bathseweg 27, Rilland"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("codeclouds/nl-bodemrisico-check").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 = { "locaties": ["Bathseweg 27, Rilland"] }

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

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

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

```

## CLI example

```bash
echo '{
  "locaties": [
    "Bathseweg 27, Rilland"
  ]
}' |
apify call codeclouds/nl-bodemrisico-check --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,codeclouds/nl-bodemrisico-check"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

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