Netherlands Childcare Register - Nursery & Daycare Leads
Pricing
$1.20 / 1,000 per childcare location returneds
Netherlands Childcare Register - Nursery & Daycare Leads
From $1.20 per 1,000 locations, no start fee. The official Dutch LRK register: 18,399 registered nurseries, out-of-school care sites and childminder agencies with email (97.5% filled), phone, website, named contact, licensed child places, operator KvK number and coordinates.
Pricing
$1.20 / 1,000 per childcare location returneds
Rating
0.0
(0)
Developer
Scrapers Delight
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
Netherlands Childcare Register (LRK) scraper
Exports the Netherlands' official Landelijk Register Kinderopvang — the register every Dutch childcare location must be listed in before it is allowed to open — as a clean B2B lead table.
Price: $0.0012 per location delivered ($1.20 per 1,000), no run-start fee. Rows your filters exclude are never delivered and never charged.
The numbers, all counted on 2026-09-16
Not estimates. The national totals come from sweeping all 342 gemeente codes and fetching the last page of each so rows are counted rather than inferred from page totals. The field-fill numbers come from 1,493 deduped real records captured across 118 pages — 60 randomly-drawn municipalities, each contributing its page 1 plus one randomly-drawn deeper page.
| measured | |
|---|---|
| Registered childcare locations (KDV + BSO + GOB) | 18,399 across 1,389 pages |
| Whole register, any status and type | 78,452 across 5,386 pages |
| Gemeenten resolved in the sweep | 342 of 342, 0 unresolved |
| Locations per operating company | 3.72 (401 distinct operators across 1,493 rows) |
Field fill on the registered KDV/BSO/GOB slice (n = 1,493)
Counting rule: a literal "N/A", "-", "Onbekend" or an empty string is not filled.
| field | filled | % |
|---|---|---|
locationId, lrkNumber, name, type, registrationStatus, registeredSince | 1,493 | 100.0% |
street, houseNumber, postalCode, city, fullAddress, municipality | 1,493 | 100.0% |
childPlaces (licensed capacity) | 1,493 | 100.0% |
operatorId | 1,493 | 100.0% |
latitude / longitude | 1,489 | 99.7% |
correspondenceAddress | 1,477 | 98.9% |
email | 1,456 | 97.5% |
contactPerson (a named human) | 1,434 | 96.0% |
phone | 1,406 | 94.2% |
website | 1,341 | 89.8% |
establishmentNumber (KvK vestigingsnummer) | 1,125 | 75.4% |
| email and phone | 1,384 | 92.7% |
| email or phone | 1,478 | 99.0% |
Operator columns (includeOperatorDetails, on by default)
Measured row-level over the 536 sample rows whose operator was actually looked up, and holder-level over 182 distinct operators:
| field | filled | % |
|---|---|---|
operatorKvkNumber | 536 / 536 | 100.0% |
operatorName | 536 / 536 | 100.0% |
operatorDisputesCommitteeSince | 535 / 536 | 99.8% |
operatorLegalForm | 428 / 536 | 79.9% |
(The live 393-row Utrecht run came out at operatorKvkNumber 100.0%, operatorName 100.0%,
operatorLegalForm 96.4% — Utrecht is corporate-heavy. The table above is the national sample,
which is the conservative number and the one to plan against.)
operatorLegalForm is the one that is not near-total, and it fails in two distinct ways: the
register records a legal form only for a legal entity (present on 120 of 120 corporate holders,
0 of 62 sole-trader holders), and 7 of those 120 say "Onbekend" — its own filler — which is
emitted as null, not as a legal form. operatorAddress follows the same corporate-only pattern.
See trap 3 below for why the 100% KvK figure is only reachable one particular way.
The single most important thing about this register
That 97.5% email fill exists only behind the filter. Measured on a 300-record sample taken with no status or type filter, from the same 20 municipalities:
| slice | n | phone | |
|---|---|---|---|
| everything, unfiltered | 300 | 15.7% | 14.3% |
UITGESCHREVEN (deregistered) | 205 | 0.0% | 0.0% |
VGO (home-based childminders) | 235 | 1.3% | 0.4% |
KDV (day nurseries) | 22 | 86.4% | 77.3% |
BSO (out-of-school care) | 43 | 58.1% | 58.1% |
The register strips contact details on deregistration — all 205 deregistered rows, zero exceptions — and privacy-suppresses home-based childminders, who are private households rather than businesses. A scraper that walks the register naively ships an email column that is ~84% empty and looks broken.
So this actor defaults to includeTypes: ["KDV","BSO","GOB"] and
registrationStatus: "INGESCHREVEN". Both filters are applied server-side, which also cuts the
national walk from 5,386 pages to 1,389 — a 3.9x cost reduction on top of the quality gain.
What you get per row
locationId · lrkNumber · name · type + typeLabel · registrationStatus ·
registeredSince · childPlaces · preschoolEducation · parentRunCreche ·
contactPerson · email · phone · website ·
street · houseNumber · postalCode · city · fullAddress · municipality ·
correspondenceAddress · correspondencePoBox · correspondencePostalCode · correspondenceCity ·
latitude · longitude · rdX · rdY ·
establishmentNumber ·
operatorId · operatorName · operatorKvkNumber · operatorLegalForm · operatorLegalFormRaw ·
operatorDisputesCommitteeSince · operatorAddress ·
inspectionId · apiUrl · scrapedAt
Why childPlaces is the interesting column: it is the licensed capacity, i.e. a firmographic
size signal a Dutch childcare SaaS can price against. Measured means: KDV 34.0 places, BSO 41.5,
both with a minimum of 5. GOB agencies license 0 by definition (all 28 zero-capacity rows in the
sample were GOB, none were KDV or BSO) — that 0 is real and is not nulled.
Fields that were CUT, and why
These are in the source and are not shipped, because they are not populated:
historischeNamen(former names) — an empty array on 1,493 of 1,493 rows. Cut entirely.- A human-facing register link. The LRK public site is a Vue SPA and the server does not
rewrite deep paths to the app shell:
/pp/inzien/oko/gegevens/<uuid>,/pp/inzien/houder/<uuid>/<kvk>and/pp/inspectierapport/<uuid>/<id>— the app's own routes, read out of its JS bundle — each redirect a cold load to/storing/index.html, a maintenance page, with HTTP 200. AregisterUrlcolumn would have been 100% broken links, so the actor emitsapiUrlinstead, which was verified: HTTP 200, 1,193 bytes, the same record. houderDatumAanvang— the only field the per-location detail endpoint adds. It costs one extra request per location and the detail response actually dropsinspectie, which the list already carries. Strictly worse per request, so no N+1 detail call is made.
And one field that ships but is deliberately not advertised: parentRunCreche (opc,
parent-run crèche) was false on 1,493 of 1,493 rows. It is present and unambiguous, so it is
emitted — but a column that never varies is not a selling point and is not claimed as one.
The traps this build had to get right
Each one was verified against live bytes; the captured responses are in fixtures/ and
offline_validate.mjs asserts against them.
- The documented query string is dead.
?plaats=&soort=returns HTTP 400, body verbatim:[PARAMETER]\r[voorzieningenRead.arg7]\r[size must be between 2 and 2147483647]\r[]. An empty search parameter is rejected outright — at least one non-empty one is mandatory. - The
Acceptheader trap runs the opposite way from the obvious.Accept: application/json→ HTTP 500{"title":"COMMON","status":500,"code":"9999"}, andAccept: text/html→ the same 500.application/hal+json→ 200. (NoAcceptheader at all also → 200, so the rule is "never send a non-HALAccept".) - The KvK rollup is behind a link that looks wrong. A row carries two different holder
uuids: the
houderfield and_links.houder.href. They differ on 97 of 1,493 rows and both return HTTP 200. Measured across 244 holder requests:/houders/<houder FIELD>→kvkNummeron 182 of 182 (100.0%);/houders/<HAL link uuid>→kvkNummeron 0 of 62 (0.0%), silently absent. Following the HAL link is the obvious move and loses the field entirely. status=INGESCHREVENleaks. 1 of 1,493 returned rows carriedAANGEMELD. Worse,status=AANGEMELDis itself rejected with HTTP 400 — the register publishes a status it refuses as a filter. The actor re-asserts status and type client-side and counts what it drops inRUN_SUMMARY; dropped rows are never delivered and never charged.vestigingsNummeris not unique. 1,125 filled values held only 837 distinct — a KDV and a BSO at the same site share one KvK establishment number. Deduplication usesidentificatie(1,493 unique of 1,493);lrkNumberis equally unique.- Coordinates are Rijksdriehoek, not lon/lat.
locatie.coordinatesranged x 68,953–274,916 and y 308,121–591,813. Published as "longitude, latitude" every Dutch nursery would land off the coast of Somalia. They are converted with the Schreutelkamp / Strang van Hees polynomial (0.0 m error at the Amersfoort origin, 0.9 m at the Groningen Martinitoren) and the raw RD pair is kept asrdX/rdY. 0 of 1,489 converted rows fell outside the Netherlands. - House numbers are split across three keys. 198 of 1,493 addresses use
huisnummer+letter(192) +toevoeging(7). Joining onlyhuisnummeremits a wrong address silently. - The correspondence address has two mutually exclusive shapes. Of the 1,477 rows that carry
one, 494 use
postbusnummer(a PO box) and 983 use street+number — 494 + 983 = 1,477 exactly. A naive street join drops a third of them to null. - A 200 can carry a block page. Deep paths on this site answer HTTP 200 with a Dutch maintenance page ("Onderhoudswerkzaamheden … niet bereikbaar"). The walk classifies the body, not the status code, and treats that as a transport failure — not as end-of-data.
- Out-of-range pages return HTTP 200 with an empty array, never 404. Page 60 of a 59-page result is a normal, empty envelope. A loop that terminates on a status code never terminates.
rechtsvormis spelled three ways. Across 120 holders: "Besloten Vennootschap" 59, "BV" 8, "BeslotenVennootschap" 6; "Vennootschap Onder Firma" 7, "VOF" 5, "VennootschapOnderFirma" 5. Both a foldedoperatorLegalFormand the untouchedoperatorLegalFormRaware shipped.
Speed, proxies and the concurrency inversion
The origin resets connections above ~2 requests in flight from a single IP. Measured cleanly, 24 requests each, nothing else running:
| source | conc 1 | conc 3 | conc 6 | conc 8 | conc 12 |
|---|---|---|---|---|---|
| one IP (no proxy) | 24/24 | 18/24 | 12/24 | — | — |
| Apify Proxy, datacenter, fresh session per request | — | — | — | 48/48 | 47/48 |
| Apify Proxy, residential NL, fresh session per request | — | — | — | 22/24 | — |
The failures on a single IP are TCP connection resets, never HTTP statuses — so a scraper that only checks status codes reads them as nothing at all.
Two consequences, both baked in:
- The default proxy is Apify datacenter, not residential. Residential was measurably worse here (22/24 vs 48/48) and buys nothing: this register has no CAPTCHA, no Cloudflare, no JS challenge, no cookie gate and no auth. Page bodies are ~17.8 KB decompressed / ~2.5 KB on the wire, far below the size where a datacenter-proxy body truncates mid-stream, so the integrity guard here is the page-count reconciliation rather than a byte-length check.
- With no proxy the actor forces concurrency to 1 and says so in the log, rather than running at 8 and quietly shedding a third of the register.
Measured throughput at concurrency 8 through Apify datacenter proxy: 3.77 req/s in isolation, 3.2 req/s on a real end-to-end run including operator lookups.
Real runs against the live register through Apify Proxy, on the real Apify SDK:
| input | wall | rows | result |
|---|---|---|---|
| All of gemeente Utrecht, no cap, operator details on | 36 s | 393 | complete: true, 27 of 27 pages, 393 distinct ids, 100% email, 99.5% phone, 100% operator KvK, 0 unreachable |
| Utrecht, cap 50 — the daily health-test input | 12–25 s | 50 | 100% email, phone and operator KvK |
Utrecht, requireEmail, cap 30 | 7 s | exactly 30 | the cap counts delivered rows, not fetched ones |
Utrecht, minChildPlaces: 60, cap 20 | 6 s | exactly 20 | ditto, on a filter that rejects most rows |
| 3 small municipalities, no cap | 21 s | 51 | complete: true, 6 of 6 pages, 100% operator KvK |
| One municipality, proxy off | 3 s | 20 | concurrency auto-dropped to 1, warned in the log |
| A name that matches nothing | 4 s | 0 | 0 delivered, 0 charged, reported as a real empty result |
The 393-row run is also the coverage proof: 393 is exactly the number the independent 342-gemeente
corpus sweep counted for Utrecht. Two of those runs hit a transient proxy error mid-walk
(ECONNRESET, and a TLS socket disconnect); both were retried on a fresh proxy session and both
runs finished with zero unreachable pages.
Extrapolating the 393-row run (115 requests in 36 s), a complete national export — 1,389 pages
plus roughly 5,000 operator lookups — lands around 30–45 minutes. Turning
includeOperatorDetails off cuts that to about 8 minutes. The actor's default run timeout is 2
hours, so either fits.
Cost
At 512 MB the full national run is ~0.2 compute units and ~5 MB of proxy traffic — around $0.05 of platform usage for 18,399 rows, about $0.0000027 per row. The $0.0012 price is a lane decision, not a cost floor.
Honest limits
- Not a Chamber of Commerce extract.
operatorKvkNumberis the operator's KvK number as the childcare register holds it; it is not validated against the KvK's own register, and no turnover, employee count or director data is available here. establishmentNumberis 75.4% filled and not unique (see trap 5). Treat it as an enrichment key, not a primary key.operatorLegalFormis 79.9%, not near-total. Sole-trader operators have no legal form in the register at all, and 7 of 120 corporate holders record it as"Onbekend". See the operator table above.- Coordinates are derived, not published as lat/lon. The conversion is accurate to roughly a metre, which is fine for mapping and radius work and is not a survey-grade position.
GOBrows are agencies, not sites. They carry contact data (they are businesses) but license 0 child places. SetminChildPlaces: 1to exclude them.VGOrows are households. They are available (includeTypes) but 98.7% of them publish no email. They are off by default for that reason.- One country, one register. Only the Netherlands; there is no equivalent call for BE/DE here.
- The register is rebuilt continuously. A page fetched mid-walk can shift by a row or two; the actor warns on a short non-final page rather than truncating, and reconciles pages read against the page count the register itself reported.
- Not verified under sustained multi-hour load, and never run inside an Apify container. The runs in the table above are real, on the real SDK, through Apify Proxy — but from a local machine, not from a platform run. The longest continuous exercises were a full 342-gemeente sweep (672 requests, 147 s) and a 1,327-request corpus count, both with zero unresolved municipalities. A ~20-minute national run with operator enrichment has not been run end to end.
robots.txt
https://www.landelijkregisterkinderopvang.nl/robots.txt returns, verbatim:
User-agent: *Disallow: /
The data itself is the Dutch government's public childcare register, published under the Wet
kinderopvang so that parents and businesses can look it up, and the /pp-api/v1/ endpoints this
actor uses are the same public API the register's own website calls from the browser. No
authentication, no rate-limit headers, no terms gate. Flagging the robots line here because it is
what the file says; the decision to publish is the operator's.
Files
src/query.js— URL building, the gemeente lookup, and every 400/500 trap encoded as a comment with the response that produced it.src/walk.js— pagination, the maintenance-page classifier, concurrency viapool.js.src/transform.js— the record shape, the RD→WGS84 conversion, and every counted data defect.src/main.js— scope resolution, billing, floor assertions,RUN_SUMMARY.fixtures/— the real bytes all of the above was written against, captured 2026-09-16.offline_validate.mjs—node offline_validate.mjs. No network, no platform, nonode_modules.