Belgian Ondernemingsnummer Scraper — KBO, BTW & Contacts
Pricing
$3.50 / 1,000 per belgian disclosure lead returneds
Belgian Ondernemingsnummer Scraper — KBO, BTW & Contacts
Turn Belgian company domains into KYB-grade B2B leads from each site's statutory legal notice (WER Boek XII): ondernemingsnummer checked against its mod-97 checksum, BTW number, RPR court, legal form, address, email and phone. Dutch, French and German pages. $3.50/1,000. No login.
Pricing
$3.50 / 1,000 per belgian disclosure lead returneds
Rating
0.0
(0)
Developer
Scrapers Delight
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
Share
🇧🇪 Belgian Ondernemingsnummer Scraper — leads with a mod-97-checked KBO number
Paste a list of Belgian business domains (or point this Actor at another Actor's dataset) and get back one clean B2B/KYB lead per domain, read off each site's statutory legal notice: the company name and legal form, the ondernemingsnummer / numéro d'entreprise checked against its mod-97 checksum, the BTW/TVA number, the RPR/RPM court, the verantwoordelijke uitgever, a contact email, phone and a split postal address — for $3.50 per 1,000 leads.
Why a legal notice and not a directory? Because Boek XII, art. XII.6 §1 of the Wetboek van economisch recht (Code de droit économique) obliges every information-society service provider to make its name (1°), geographic address (2°), contact details including an email address (3°), its ondernemingsnummer (4°) and its BTW-identificatienummer (7°) easily, directly and permanently accessible. The corpus is therefore the whole Belgian commercial web — not one directory's member list — and every field is a disclosure the company published about itself, by law.
✅ What does it do?
For each domain you give it, plain HTTP hops. No browser, no login, no API key.
- Discovery. Fetch the homepage and rank every
<a href>on both its href and its anchor text, in four languages at once — exact "Wettelijke vermeldingen" / "Mentions légales" / "Impressum" (100) beats a loose text match (90) beats anhrefmatch (88) beats "Algemene voorwaarden" / "Conditions générales" / "AGB" (50) beats a contact page (26). If nothing is linked, fall through to the homepage's own footer, the XML sitemap, the WordPress REST page index, then 19 conventional paths. - Resolution. Fetch the winning candidate, gate it on a statutory content marker — a bare HTTP 200 is not enough — then read the fields out of the page text with anchored, case-sensitive patterns.
The five things a generic EU legal-notice extractor gets wrong in Belgium
- 🧮 It does not check the number. A Belgian legal notice is full of nine- and ten-digit strings: licence numbers, ITAA and BIV accreditations, IBANs, phone numbers written with dots. The ondernemingsnummer is the one that satisfies
last two digits = 97 − (first 8 digits mod 97), and the BTW number isBE+ exactly that number — so each is an exact algebraic container for the other. This Actor resolves the number from the most trustworthy statement on the page, emitsenterpriseNumberValidso you can see the checksum result rather than trust it, and flags a published typo instead of dropping the row. Measured: 262 of the 264 numbers found passed; the 2 failures are real typos on real sites. - 🗣 Belgium has three official languages, and one site may use any of them. A Dutch-only label list under-collects on Walloon, Brussels and Eupen sites, and the failure looks exactly like sparse data rather than a bug. Every label here — page titles, field labels, legal forms, street types, director roles, third-party credit blocks — lives in a config block with
nl,fr,deandenvariants, and every row carries thelanguageit was read in so you can audit the split yourself. Measured across 330 real pages: 53% nl · 34% fr · 9% de · 5% en. - 🌐
.beis not the filter. Belgian companies trade on.comand.euconstantly. Of the 734 domains probed, 165 (22%) were not.be, and 74 of the 329 delivered leads (23%) came from them. There is no TLD filter by default. - 📞 An ondernemingsnummer beginning
04is character-for-character a Belgian mobile number. Nothing in the digits tells them apart. The first live run of this Actor emitted+32466792704as a third phone number for a travel agency — it was the agency's own KBO number. A registry id the page published is never also delivered as a phone. - 🏢 The hosting credit and the web agency's credit are other companies' identity statements. Left in place they ship as the lead's own address, email and enterprise number. Both blocks are located and removed before any owner field is read — and a one-line credit ("Webhosting door lexxweb.be") is cut at the end of that line, not by a fixed 400-character window, because a fixed window swallowed one company's entire contact block including its KBO number.
📊 Measured, from real runs
Everything below is from actual runs, not estimates.
End-to-end over 734 real Belgian business domains
Sourced from OpenStreetMap (shop / office entries carrying a website tag) across the whole of Belgium and sampled across the three language regions — a realistic buyer list, not a hand-picked one. Apify datacenter proxy, concurrency 20.
| Outcome | Domains | Share | Billed? |
|---|---|---|---|
| ✅ a parsed lead | 329 | 45% | yes |
| ⚪ reachable, publishes no findable disclosure | 284 | 39% | no |
💀 unreachable — dead host (NXDOMAIN / TLS / refused) | 69 | 9% | no |
🔴 blocked by an anti-bot wall | 52 | 7% | no |
613 of the 734 domains were reachable; 329 of those 613 (54%) published a findable statutory disclosure.
How the disclosure was found: ranked footer anchors 214 · the homepage's own footer 97 · guessed paths 18. That middle number is the Belgium-specific one — nearly three in ten Belgian leads come from the identity block printed in the homepage footer, at zero extra requests, because the homepage is already in hand.
Per-field fill, over 330 delivered leads — and split by the language the disclosure is written in
Counted against the raw captured bytes, not against the parser's own optimism.
| Field | All | 🇳🇱 nl (174) | 🇫🇷 fr (111) | 🇩🇪 de (30) | 🇬🇧 en (15) |
|---|---|---|---|---|---|
companyName | 97% | 97% | 98% | 93% | 100% |
phone | 91% | 94% | 89% | 93% | 67% |
addressLine / postalCode / city | 88% | 90% | 86% | 97% | 60% |
email | 85% | 87% | 82% | 93% | 60% |
enterpriseNumber / vatNumber | 80% | 85% | 79% | 60% | 67% |
legalForm | 37% | 36% | 41% | 43% | 13% |
registerCity (RPR/RPM) | 12% | 17% | 8% | 0% | 7% |
iban | 8% | 7% | 10% | 0% | 7% |
hostingProvider | 7% | 1% | 16% | 13% | 0% |
director | 5% | 1% | 7% | 17% | 0% |
responsiblePublisher | 5% | 1% | 2% | 40% | 0% |
registerNumber | 2% | 3% | 3% | 0% | 0% |
Any registry id: 80% · any contact (email or phone): 94% · postal address: 88%. Median row carries 8 of the 15 value fields (p90 = 10, best = 12).
The number was resolved from the BTW/TVA statement on 129 pages, from an explicit ondernemingsnummer label on 66, from a bare BE… token on 62 and from an unlabelled checksum-valid token on 7. The cross-field guards fired for real: 21 rows carried a second, different valid enterprise number (surfaced in enterpriseNumberConflict rather than silently picked) and 2 RPR numbers disagreed with the company's own and were withheld.
The per-language split is published because it is the honest way to show where a tri-lingual parser is weaker. German-language pages fill worse on the enterprise number (60% vs 85% Dutch) and far better on responsiblePublisher (40% vs 1%) — the German-speaking Community's sites follow a German Impressum habit, which names a Verantwortlicher Herausgeber but states the company number less often.
Offline validation
The parser is re-run over all 330 pages captured live through the shipping proxy, with 9,915 assertions comparing each advertised field against independent evidence in the raw bytes — including the check that runs the other way: for every row with no enterprise number, the bytes must contain no checksum-valid one the parser missed. 0 failures. That check is what caught a German-language Eupen site printing its number as BE 07 0060 9422 — a grouping no 4-3-3 pattern matches.
Transport
got-scraping over the Apify datacenter proxy; Chromium is never launched, and the image is 512 MB. 3,447 requests for 734 domains ≈ 4.7 per domain. 52 domains were refused; residential-BE was tried on 51 of them and recovered 17. The 69 dead hosts reproduced identically on datacenter and residential — stale directory entries, not blocks, and never reported as blocked.
Per-domain latency on delivered rows: p50 7.2 s, p90 33.4 s, max 142.8 s — which is why perDomainTimeoutSecs exists and defaults to 60.
⚠️ Honest limits — read these before you buy
- About four in ten reachable Belgian SME sites publish no findable disclosure. That is not a parser gap: those domains were re-probed through ranked anchors in four languages, the homepage footer, the sitemap, the WordPress REST index and 19 guessed paths. They carry no ondernemingsnummer, BTW number, RPR entry or registered address anywhere reachable. You are never charged for them.
directorfills at only 5%, and that is Belgian law, not a bug. WER art. XII.6 §1 requires the company's identity — it does not require naming an officer, the way the German §5 DDG requires the Geschäftsführer. If you need officers, this is not the product; the KBO/BCE register is.registerNumberfills at 2% and that is expected. Belgian sites write "RPR Gent" without repeating the number, because by law the RPR registration number is the ondernemingsnummer — which is why this Actor reports the court inregisterCityand does not fabricate the number intoregisterNumber. Join onenterpriseNumberinstead.- This Actor does not confirm the number against the KBO/BCE register. It reports what the company published, plus an offline checksum. A registry cross-check is a different data source and a different job.
- A non-Belgian operator on a Belgian domain comes back honestly empty. Cross-border retailers in the corpus publish a Dutch
KvK-nummeror a GermanHandelsregisterentry instead; no Belgian number is invented for them, andenterpriseNumberis simplynull. - Stale lists have dead hosts. 69 of 734 domains (9%) did not resolve at all. Never charged.
robots.txtis exposed as a switch and is off by default. These pages exist to be indexed — a legal notice a search engine cannot read does not meet the statute's "easily and directly accessible" test.
🧾 Output
One row per domain. flattenOutput (default on) gives flat columns for Sheets / Clay; turn it off for nested registry{} / people{} / contact{} / hosting{} / policies{} objects.
| Field | Description |
|---|---|
domain | Registrable domain (eTLD+1, www-stripped) — the fallback stable id. |
disclosureUrl | The exact page the data came from. |
discoveryChannel | anchor · homepage · sitemap · wpjson · guess · input — how it was found. |
language | nl · fr · de · en — the language the disclosure is written in. |
companyName | The legal entity name. |
legalForm | BV · NV · CV · VOF · CommV · VZW · SRL · SA · SC · ASBL · GmbH · AG · VoG and the pre-2019 forms (BVBA, CVBA, SPRL, SCRL). Matched case-sensitively and only inside the identity block, because "1 KG KOFFIE", "Publier mon CV" and "CV en Verwarming" are not legal forms. |
enterpriseNumber | The 10-digit KBO/BCE number, normalised (BE-0446.185.845 → 0446185845). The pre-2008 nine-digit VAT form is normalised by restoring its leading zero. |
enterpriseNumberFormatted | The same number as Belgium prints it: 0446.185.845. |
enterpriseNumberValid | The mod-97 checksum result. false means the page carries a typo — flagged, never silently dropped. |
enterpriseNumberSource | label · vat · be-prefix · bare · rpr — which statement it came from. |
enterpriseNumberConflict | A second, disagreeing valid number found on the page, surfaced instead of hidden. |
vatNumber | The BTW/TVA form, BE + the enterprise number (BE0446185845). |
vatNumberPublished | Did the page actually present it as a VAT number? An entity can hold an enterprise number without being VAT-registered, so "no VAT number published" and "not VAT-registered" stay different facts. |
registerCity / registerNumber | The RPR / RPM court, and the registration number only when the page states one that agrees with the enterprise number. |
registerConflict | An RPR number that disagreed — kept visible, not published as the company's. |
responsiblePublisher | Verantwoordelijke uitgever / éditeur responsable / Verantwortlicher Herausgeber — the distinctly Belgian named-person field. |
director | Zaakvoerder · bestuurder · gérant · administrateur · Geschäftsführer. |
dpoContact | Functionaris voor gegevensbescherming / DPO, for GDPR and KYB buyers. |
addressLine / street / postalCode / city | Registered address, parsed as a block and split, handling both Belgian orders (Zuiderlaan 71 and 16 rue de la Loi) and bus/boîte suffixes. |
addressConflict | Set when the surviving address is byte-identical to the hosting provider's. |
email / emails[] | Contact addresses. mailto: preferred, Cloudflare data-cfemail obfuscation decoded, (at) / (punt) forms decoded. |
emailConflict | The primary email's domain when it is neither the company's own nor a free mailbox. |
phone / phones[] | Normalised to E.164 (03/430.59.43 → +3234305943), with the (0) trunk placeholder in +32 (0)… removed. |
iban | The company's Belgian IBAN, validated against the ISO 13616 mod-97 check. |
socialLinks[] | LinkedIn / Facebook / Instagram / X / YouTube / TikTok profiles on the page. |
hostingProvider / hostingProviderAddress | The host — Combell, One.com, OVH, Wix, Shopify, Cloudflare… A resellable technographic signal, and the guard that keeps the host's own data out of the lead's fields. |
termsUrl / privacyPolicyUrl | The site's algemene voorwaarden / conditions générales / AGB and privacy policy. |
stableId | The checksum-valid enterprise number when present, else the registrable domain. The dedupe key. |
fieldCount · status · missReason · fetchedAt · elapsedMs | Row quality, outcome and provenance. |
Example row (amigoreizen.be, real output from a live run):
{"domain": "amigoreizen.be","disclosureUrl": "https://amigoreizen.be/algemene-voorwaarden/","discoveryChannel": "anchor","language": "nl","companyName": "Amigo Reizen","enterpriseNumber": "0466792704","enterpriseNumberFormatted": "0466.792.704","enterpriseNumberValid": true,"enterpriseNumberSource": "vat","vatNumber": "BE0466792704","vatNumberPublished": true,"addressLine": "Gasthuisstraat 11, 2340 Beerse","street": "Gasthuisstraat 11", "postalCode": "2340", "city": "Beerse","email": "info@amigoreizen.be","emails": ["info@amigoreizen.be", "beerse@amigoreizen.be"],"phone": "+3233402858","socialLinks": ["https://www.facebook.com/amigoreizen1", "https://www.instagram.com/amigoreizen/"],"stableId": "0466792704","status": "ok"}
⌨️ Input
Five ways to hand over a list, because agencies do not all work the same way.
| Field | What it is |
|---|---|
domains | One entry per company. A bare domain (aktum.be), a homepage URL, or a direct legal-notice URL — a direct URL skips the homepage hop and costs one request. Any TLD. |
startUrls | The standard Apify request-list editor, so Make / Zapier / Google Sheets can hand over a URL list natively. |
sourceDatasetId + domainFieldName | Read the list straight out of another Actor's dataset — the real agency workflow: enrich a Maps or directory scrape. |
domainsFileUrl | A CSV / TXT / JSON / JSONL file at a URL, for lists too big to paste. |
skipDomains + previousDatasetId | Suppression. Point previousDatasetId at last month's run and this month's never re-buys a lead you already paid for. |
Then the knobs that matter:
| Field | Default | What it does |
|---|---|---|
maxItems | 100 | Hard cap on rows delivered and billed. Raise it for a real list — at 54% yield a 1,000-domain list needs about 480. 0 = unlimited. |
maxDomains | 0 | Cap on domains attempted — a different number, because yield is ~54%. |
discoveryChannels | all 5 | A real cost lever. homepage costs nothing and won 29% of finds. |
maxDiscoveryRequestsPerDomain | 10 | Ceiling on the guessed-path channel. |
perDomainTimeoutSecs | 60 | Per-domain deadline, so one slow host cannot stall the run. |
requestConcurrency | 12 | 1–30. |
proxyConfiguration | Apify datacenter | The measured winner. Bring your own proxies if you prefer. |
proxyCountry | none | Note: Apify datacenter proxies cannot be country-pinned (they answer HTTP 407), so choosing a country switches the run to residential exit nodes. |
escalateToResidentialOnBlock | true | One residential-BE retry on a real 403 or challenge — never on a dead host, never on a 404. |
languageFilter | all | nl / fr / de / en — for a campaign that only targets Flanders, Wallonia or the German-speaking Community. |
requireEnterpriseNumber / requireValidChecksum / requireContact / minFieldsRequired | off | Quality floors. A row filtered out here is never billed. |
emailPolicy | all | role-only (info@, contact@…) or exclude-role (named mailboxes only). |
dedupeBy | number, else domain | Or domain, enterprise-number, or none. |
includeMissRows | false | Emit an unbilled row with status + missReason for every dead / walled / notice-less domain. |
includeRawText / includeRawHtml | false | Attach the source for audit or your own LLM re-parse. |
flattenOutput | true | Flat columns vs nested objects. |
respectRobotsTxt | false | Exposed as a switch. |
Example input:
{"domains": ["aktum.be", "amigoreizen.be", "https://drukta.be/algemene-voorwaarden"],"maxItems": 1000,"requireValidChecksum": false,"includeMissRows": false}
💰 Pricing
Pay per event — one event, be-disclosure-scraped, at $0.0035 per delivered lead ($3.50 per 1,000). No start fee.
Rows are delivered and billed atomically through the same call, so if you hit a charge cap you never keep a row you were not billed for, and never pay for a row you did not receive.
You are not charged for:
- a domain whose host is dead,
- a domain blocked by an anti-bot wall,
- a domain that publishes no statutory disclosure,
- a duplicate collapsed by
dedupeBy, - a row filtered out by
requireEnterpriseNumber/requireValidChecksum/requireContact/minFieldsRequired/languageFilter, - any
includeMissRowscoverage row.
| Price per row | |
|---|---|
| Generic EU legal-notice extractors (no enterprise number, no checksum) | $0.002 – $0.0037 |
| This Actor | $0.0035 |
| Our France Mentions Légales scraper (17 fields, median 11) | $0.004 |
| Our Spain Aviso Legal scraper | $0.005 |
| Our DACH Impressum scraper | $0.015 |
It sits a notch under the French sibling because Belgian law requires a shorter disclosure than the French LCEN or the German DDG — no share capital, no activity code, no register court, no obligation to name an officer — and the measured median row carries 8 of 15 value fields against France's 11 of 17.
At 54% yield, a 1,000-domain list costs roughly $1.70 in event fees.
❓ FAQ
What is an "ondernemingsnummer"?
The 10-digit identifier every Belgian enterprise receives in the Kruispuntbank van Ondernemingen (KBO) / Banque-Carrefour des Entreprises (BCE). Since 2008 it is also the VAT number: BE + the same ten digits. It is the key you use to look the company up in the official register.
How do I know the number is real?
Every number is checked against its mod-97 checksum — the last two digits equal 97 − (first 8 digits mod 97) — and the result ships in enterpriseNumberValid. Across 330 real pages, 262 of 264 passed; the 2 failures are typos the sites published, and they are flagged, not hidden.
Is this legal to scrape? These pages are statutory public disclosures that Belgian law (WER Boek XII, art. XII.6 §1) requires to be easily, directly and permanently accessible — they exist precisely to be read. You remain responsible for each site's Terms of Service, and for handling any personal data in the output (a named verantwoordelijke uitgever, a personal email) under the GDPR.
Does it work on French- and German-language Belgian sites?
Yes — that is the point. Every label set is tri-lingual plus English, and each row reports the language it was read in. On the 330-page benchmark: 174 Dutch, 111 French, 30 German, 15 English.
Why is enterpriseNumber 80% and not 100%?
Because about one disclosure in five names a company and an address but no registry id — commonly sole traders and associations, and non-Belgian operators. Turn on requireEnterpriseNumber (or requireValidChecksum) and those rows are neither delivered nor billed.
Can I get only Flemish, only Walloon or only German-speaking companies?
Set languageFilter to ["nl"], ["fr"] or ["de"]. Filtered rows are never billed.
How is a duplicate handled?
Two ways. A repeated input line is collapsed at queue time, before it costs a request. Two different domains that turn out to share one enterprise number are collapsed before pushData — so a duplicate can never double-charge.
How do I re-run monthly without paying twice?
Put the previous run's dataset id in previousDatasetId. Every stableId, domain and enterpriseNumber it contains is suppressed.
Does it need a browser or a residential proxy? No. Datacenter HTTP reached 613 of 734 domains. Residential is a per-block escalation (tried on 51, recovered 17), not the norm. Chromium is never launched.
Does it use the KBO/BCE Open Data or a paid API? No. It reports what the company itself published, plus an offline checksum. No external API calls, no keys, no rate limits.
What happens if my list is entirely dead domains? The run exits cleanly with zero billed rows and a status message saying why. It only fails loudly when the transport itself is broken — 80%+ of domains refused with a challenge — because that is a proxy problem, not an empty market.
Can I export to Google Sheets / Clay / HubSpot?
Yes. Leave flattenOutput on and use Apify's CSV / Excel / JSON export or the dataset API.
⚖️ Legal
This Actor reads statutory public disclosures that Belgian law requires websites to publish, over ordinary HTTP requests to ordinary public pages. It does not log in, does not bypass any anti-abuse control, does not solve CAPTCHAs, and does not forge authentication. robots.txt compliance is available as an input switch.
Output may contain personal data — a named verantwoordelijke uitgever, a named zaakvoerder, a personal email address. Under the GDPR you are the controller of what you do with it: have a lawful basis, honour erasure requests, and respect each site's Terms of Service. Scraping and using this data is your responsibility, not Apify's and not this Actor's.