Belgian Ondernemingsnummer Scraper — KBO, BTW & Contacts avatar

Belgian Ondernemingsnummer Scraper — KBO, BTW & Contacts

Pricing

$3.50 / 1,000 per belgian disclosure lead returneds

Go to Apify Store
Belgian Ondernemingsnummer Scraper — KBO, BTW & Contacts

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

Scrapers Delight

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a day ago

Last modified

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.

  1. 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 an href match (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.
  2. 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.
  • 🧮 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 is BE + 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, emits enterpriseNumberValid so 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, de and en variants, and every row carries the language it was read in so you can audit the split yourself. Measured across 330 real pages: 53% nl · 34% fr · 9% de · 5% en.
  • 🌐 .be is not the filter. Belgian companies trade on .com and .eu constantly. 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 04 is character-for-character a Belgian mobile number. Nothing in the digits tells them apart. The first live run of this Actor emitted +32466792704 as 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.

OutcomeDomainsShareBilled?
✅ a parsed lead32945%yes
⚪ reachable, publishes no findable disclosure28439%no
💀 unreachable — dead host (NXDOMAIN / TLS / refused)699%no
🔴 blocked by an anti-bot wall527%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.

FieldAll🇳🇱 nl (174)🇫🇷 fr (111)🇩🇪 de (30)🇬🇧 en (15)
companyName97%97%98%93%100%
phone91%94%89%93%67%
addressLine / postalCode / city88%90%86%97%60%
email85%87%82%93%60%
enterpriseNumber / vatNumber80%85%79%60%67%
legalForm37%36%41%43%13%
registerCity (RPR/RPM)12%17%8%0%7%
iban8%7%10%0%7%
hostingProvider7%1%16%13%0%
director5%1%7%17%0%
responsiblePublisher5%1%2%40%0%
registerNumber2%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.
  • director fills 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.
  • registerNumber fills 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 in registerCity and does not fabricate the number into registerNumber. Join on enterpriseNumber instead.
  • 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-nummer or a German Handelsregister entry instead; no Belgian number is invented for them, and enterpriseNumber is simply null.
  • Stale lists have dead hosts. 69 of 734 domains (9%) did not resolve at all. Never charged.
  • robots.txt is 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.

FieldDescription
domainRegistrable domain (eTLD+1, www-stripped) — the fallback stable id.
disclosureUrlThe exact page the data came from.
discoveryChannelanchor · homepage · sitemap · wpjson · guess · input — how it was found.
languagenl · fr · de · en — the language the disclosure is written in.
companyNameThe legal entity name.
legalFormBV · 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.
enterpriseNumberThe 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.
enterpriseNumberFormattedThe same number as Belgium prints it: 0446.185.845.
enterpriseNumberValidThe mod-97 checksum result. false means the page carries a typo — flagged, never silently dropped.
enterpriseNumberSourcelabel · vat · be-prefix · bare · rpr — which statement it came from.
enterpriseNumberConflictA second, disagreeing valid number found on the page, surfaced instead of hidden.
vatNumberThe BTW/TVA form, BE + the enterprise number (BE0446185845).
vatNumberPublishedDid 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 / registerNumberThe RPR / RPM court, and the registration number only when the page states one that agrees with the enterprise number.
registerConflictAn RPR number that disagreed — kept visible, not published as the company's.
responsiblePublisherVerantwoordelijke uitgever / éditeur responsable / Verantwortlicher Herausgeber — the distinctly Belgian named-person field.
directorZaakvoerder · bestuurder · gérant · administrateur · Geschäftsführer.
dpoContactFunctionaris voor gegevensbescherming / DPO, for GDPR and KYB buyers.
addressLine / street / postalCode / cityRegistered address, parsed as a block and split, handling both Belgian orders (Zuiderlaan 71 and 16 rue de la Loi) and bus/boîte suffixes.
addressConflictSet 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.
emailConflictThe 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.
ibanThe company's Belgian IBAN, validated against the ISO 13616 mod-97 check.
socialLinks[]LinkedIn / Facebook / Instagram / X / YouTube / TikTok profiles on the page.
hostingProvider / hostingProviderAddressThe 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 / privacyPolicyUrlThe site's algemene voorwaarden / conditions générales / AGB and privacy policy.
stableIdThe checksum-valid enterprise number when present, else the registrable domain. The dedupe key.
fieldCount · status · missReason · fetchedAt · elapsedMsRow 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.

FieldWhat it is
domainsOne 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.
startUrlsThe standard Apify request-list editor, so Make / Zapier / Google Sheets can hand over a URL list natively.
sourceDatasetId + domainFieldNameRead the list straight out of another Actor's dataset — the real agency workflow: enrich a Maps or directory scrape.
domainsFileUrlA CSV / TXT / JSON / JSONL file at a URL, for lists too big to paste.
skipDomains + previousDatasetIdSuppression. 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:

FieldDefaultWhat it does
maxItems100Hard 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.
maxDomains0Cap on domains attempted — a different number, because yield is ~54%.
discoveryChannelsall 5A real cost lever. homepage costs nothing and won 29% of finds.
maxDiscoveryRequestsPerDomain10Ceiling on the guessed-path channel.
perDomainTimeoutSecs60Per-domain deadline, so one slow host cannot stall the run.
requestConcurrency121–30.
proxyConfigurationApify datacenterThe measured winner. Bring your own proxies if you prefer.
proxyCountrynoneNote: Apify datacenter proxies cannot be country-pinned (they answer HTTP 407), so choosing a country switches the run to residential exit nodes.
escalateToResidentialOnBlocktrueOne residential-BE retry on a real 403 or challenge — never on a dead host, never on a 404.
languageFilterallnl / fr / de / en — for a campaign that only targets Flanders, Wallonia or the German-speaking Community.
requireEnterpriseNumber / requireValidChecksum / requireContact / minFieldsRequiredoffQuality floors. A row filtered out here is never billed.
emailPolicyallrole-only (info@, contact@…) or exclude-role (named mailboxes only).
dedupeBynumber, else domainOr domain, enterprise-number, or none.
includeMissRowsfalseEmit an unbilled row with status + missReason for every dead / walled / notice-less domain.
includeRawText / includeRawHtmlfalseAttach the source for audit or your own LLM re-parse.
flattenOutputtrueFlat columns vs nested objects.
respectRobotsTxtfalseExposed 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 includeMissRows coverage 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.


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.