Germany Transport Operators - VUDat Permits & Fleet
Pricing
$2.00 / 1,000 per operator row returneds
Germany Transport Operators - VUDat Permits & Fleet
From $2.00 per 1,000 operators, no start fee. Germany's official BALM/VUDat licence register: ~57,645 haulage and bus operators with address, legal form, trade-register number, named legal rep and transport manager, vehicle counts and permit numbers. No email or phone - the register publishes none.
Pricing
$2.00 / 1,000 per operator row returneds
Rating
0.0
(0)
Developer
Scrapers Delight
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
3 days ago
Last modified
Categories
Share
🇩🇪 Germany Transport Operators — VUDat Permits & Fleet
Every licensed road-transport operator in Germany, from the federal register itself — with the fleet size and the name of the person legally responsible for it.
VUDat (Verkehrsunternehmensdatei) is the register the Bundesamt für Logistik und Mobilität (BALM) is required to publish under EU Regulation 1071/2009. A company cannot legally move freight for hire or carry passengers in Germany without an entry in it. That makes it a census, not a directory: ~57,645 operators, every one of them licensed, each with a postal address, a legal form, a named legal representative, a named transport manager, a vehicle count by class, and every permit it holds with the number, the validity window and the authority that issued it.
⚠️ Read this before you buy: there is no email, no phone and no website
Counted, not assumed. Over a seeded random sample of 120 records taken on 2026-09-16 and
re-counted by offline_validate.mjs on every run:
| found in 120 record bodies | |
|---|---|
| email addresses | 0 |
tel: / mailto: links | 0 |
| company websites | 0 (the only URLs in a record are schema.org / w3.org boilerplate) |
This is structural, not a parser miss — a statutory permit register publishes who is licensed, not
how to ring them. There is therefore no email, phone or website column in the output at
all, and none in the schema. An actor that took "the first URL in the record" as a website would
ship a 100% filled website column made entirely of XML namespace declarations; that mistake is
specifically guarded against in the validator.
What you get instead is the hard part of a lead: an exhaustive, legally-authoritative list of
qualified companies with the decision-maker's name and the fleet size. Join companyName +
postalCode + legalRepresentative against a contact source to reach them. If you need the
contact channel in the same row, this is not that product.
📊 The numbers, measured on 2026-09-16
Everything below was counted against the live register on the day this Actor was built.
| Operators addressable | 57,645 (ids 1–57,645 populated; 57,646 and up are empty) |
| Holes inside that range | none observed — 125 distinct ids inside it came back populated 125/125 (a seeded random sample of 120, plus 5 probed by hand) |
| Page size | 47,655 / 48,055 / 50,481 bytes (min / median / max over 120 live fetches) |
| Anti-bot | none — no CAPTCHA, no Cloudflare, no JS gate; a bare request with no User-Agent gets a byte-identical page |
| Permits per operator | 127 permits across 120 operators; 7 operators hold more than one |
| Oldest permit seen | valid from 1990-02-19 |
The boundary was re-confirmed on the day of the build by a live run over ids 57,638–57,652: eight populated, then seven empty, exactly at 57,645/57,646.
Field fill — counted over the 120-record random sample
offline_validate.mjs recounts this from the captured bytes every time it runs, so the table
cannot drift away from the code.
| Field | Fill | Field | Fill | |
|---|---|---|---|---|
companyName | 100.0% | trucks / buses / vans | 100.0% | |
legalForm | 100.0% | totalVehicles | 100.0% | |
postalCode / city / streetAddress | 99.2% | permitCount / permitTypes | 100.0% | |
legalRepresentative | 99.2% | permitNumbers | 97.5% | |
transportManager | 95.0% | issuingAuthorities | 95.8% | |
tradeRegisterNumber | 49.2% | hasCurrentPermit | 100.0% | |
registerCourt | 50.0% | firstPermitValidFrom | 100.0% | |
branchCount | 100% (0 for most) | lastPermitValidUntil | 83.3% |
"Filled" excludes "", null, "N/A", "-", "k.A.", "keine Angabe" and a literal "*".
Three of those four sub-100% figures are true absences, not gaps, and each one is explained:
tradeRegisterNumber49.2% — the other half are sole traders (Einzelunternehmen, 60 of 120 in the sample) and natural persons, who have no Handelsregister entry at all. SetrequireTradeRegisterNumberif you only want incorporated companies.lastPermitValidUntil83.3% — 23 of 127 permits publishunbefristet, "open-ended". Those carryisIndefinite: trueandisCurrent: true. A parser that readunbefristetas a missing date and then testedvalidUntil >= todaywould mark Germany's longest-standing licence holders as expired; this one does not.permitNumbers97.5% — three operators carry a literal*where a legacy Land put a placeholder. That is nulled rather than shipped as a licence number.
What the register looks like (same 120 operators)
| Fleet (trucks) | operators | Legal form | operators | |
|---|---|---|---|---|
| 0 | 19 | Einzelunternehmen (sole trader) | 60 | |
| 1–2 | 45 | GmbH / UG | 41 | |
| 3–9 | 33 | OHG/KG without a natural GP | 12 | |
| 10–49 | 19 | Eingetragener Kaufmann | 4 | |
| 50+ | 4 (max 102) | AG / OHG / KG | 3 |
Mean 7.5 trucks. 34% run 5 or more, 19% run 10 or more, 90% run at least one vehicle. 115 of 120 are freight operators, 5 are passenger (bus). Postcode first digits are spread 4–18% each, so the register is genuinely national rather than clustered.
📦 What one row is
One row per operator. One charge per row.
| Field | What it is |
|---|---|
operatorId / sourceUrl | The VUDat idUnternehmen and a link straight back to the register page |
companyName | Name as filed |
legalForm | Einzelunternehmen, GmbH, Aktiengesellschaft, Kommanditgesellschaft… |
tradeRegisterNumber / registerCourt | HRB 17280 / Münster. Null for sole traders — a true absence. |
streetAddress / postalCode / city / fullAddress | Postal address. Null, never a mis-split postcode, when the register omits the block. |
legalRepresentative + legalRepresentatives[] | Named officer(s), as Surname, Firstname and split into surname / firstName. 17 of 120 operators name more than one. |
transportManager + transportManagers[] | The Verkehrsleiter — the person who legally answers for the fleet, and usually the right person to talk to about it |
trucks / buses / smallVehicles2500to3500kg / totalVehicles | Vehicle count by class, from the register's own Fuhrpark block. A 0 is a real zero; a missing block is null, never a fabricated 0. |
permits[] | Every licence: permitType, permitNumber, validFrom, validUntil, isIndefinite, issuingAuthority, isCurrent, transportMode |
permitNumbers / permitTypes / issuingAuthorities | Flattened for a spreadsheet. 91 distinct issuing authorities appeared in a 120-operator sample. |
transportModes | freight and/or passenger, derived from the permit type |
hasCurrentPermit / hasIndefinitePermit | true / false / null when the register published no end date |
firstPermitValidFrom / lastPermitValidUntil | The operator's overall licence window |
branchCount / branches[] | Registered branch establishments (Niederlassungen). Sparse and real: 5 of 120 operators list one or more. 0 means none registered, which is data. |
scrapedAt | ISO timestamp of the fetch |
Permit numbers are not one format. Real values in the sample include D-05-059-G-0177,
0219PAF, BY/041/schw/gz, 281-E and 159. Everything here is extracted by label
(Berechtigungsnummer), never by pattern — a regex for the common shape scores about 75% fill and
silently drops every Land that numbers its permits differently.
🔎 How to choose which operators you get
VUDat publishes no list, no search-by-place and no page cursor. Its own search form makes
both a company name and a place mandatory and explicitly refuses wildcards, so it cannot
enumerate anything. The integer idUnternehmen is the only enumeration primitive there is.
That has one consequence you should know before you set a filter:
Every filter in this Actor is applied AFTER the page is fetched. Rows you filter out are never delivered and never charged — but they are fetched, so a narrow filter over a wide id range spends time to deliver few rows.
RUN_SUMMARYreports theexaminedPerDeliveredyour filters actually produced.
operatorIds— read exactly these ids. One page per id: the cheap, precise mode.startId/endId— walk a range.endId: 0walks to the end of the register and stops after 150 consecutive empty ids (no holes were observed anywhere inside the populated range, so 150 is decisive).postalCodePrefixes—["8"]the south-east,["80"]Munich,["80331"]one postcode.transportModes/permitTypes— freight vs passenger; the register's three permit types, validated against the real list before the run starts so a typo cannot return an empty result that looks like a real answer.minTrucks/minTotalVehicles— a genuine fleet-size filter, because the register publishes the count. An operator whose fleet block is missing is excluded, never counted as zero.onlyCurrentPermits,requireTradeRegisterNumber,requireAddress,requireTransportManager— quality gates with measured costs (they remove 0%, 51%, 1% and 5% of the sample respectively).maxRecordsExamined/maxRunSeconds— budgets. Use them with a narrow filter.
💰 Billing
$0.002 per operator row delivered. No run-start fee. You are charged for delivered rows and nothing else.
Nothing else is billable, and the three kinds of nothing are kept apart in RUN_SUMMARY so you can
see which happened:
notFound— the id answered HTTP 200 with an empty page. The register holds no operator there. Not a failure, not charged.filteredOut— a real operator that did not match your filters, with a per-filter breakdown of which one removed it. Not charged.unreachable— could not be read after retries on fresh proxy sessions. A transport failure, not evidence the operator does not exist. Not charged; re-run those ids withoperatorIds.
A fourth counter, unparsable, means the page carried a record the parser did not recognise.
Non-zero there means BALM changed its template, and it is loud on purpose.
⚙️ Reliability, measured today
The host throttles by dropping connections, not by returning an error. Measured on 2026-09-16 from a single IP:
| what was run | result |
|---|---|
| 120 requests at concurrency 10 | 67 succeeded, 53 connection timeouts, then the host stopped answering that IP entirely for several minutes |
| the same 53 ids, serial with a 2.5 s pause | 53/53 succeeded |
| live Actor runs at concurrency 2 | 19 requests, 0 failures |
So concurrency defaults to 4 and is capped at 10, and Apify Proxy is on by default — the
point of the proxy here is rotation, not evasion, since there is no anti-bot to evade. If you
see the unreachable count climb, lower concurrency or switch the proxy to RESIDENTIAL.
Three failure modes this Actor refuses to paper over:
- An empty id answers HTTP 200. Existence is decided by page content, never by status code — a status-code check would report a 100% successful crawl over an entirely empty range.
- A truncated download also answers HTTP 200, and what survives is the chrome without the record — byte-identical in shape to an empty id. Every body is checked for completeness before it is parsed, and an incomplete one is retried on a fresh session rather than counted as "no such operator".
- A walk that finds nothing at all fails the run. If every examined id comes back empty, the
endpoint moved (it already moved once, from
/SiteGlobals/Forms/vudat/to/DE/Service/EEREinzelansicht/) — that is reported as a failure, not as a clean empty run.
📜 Source and robots
Data: VUDat — Verkehrsunternehmensdatei, published by the Bundesamt für Logistik und Mobilität
at https://www.balm.bund.de/DE/Service/EEREinzelansicht/einzelansicht_node.html?idUnternehmen=…,
a public register the agency is required to publish under EU Regulation 1071/2009. No login, no
cookie, no licence key.
robots.txt, verbatim as served on 2026-09-16:
User-agent: *Disallow: /SiteGlobals/Disallow: /EN/Service/Allow: /SiteGlobals/Modules/Allow: /SiteGlobals/StyleBundles/Allow: /SiteGlobals/Frontend/Crawl-delay: 30Sitemap: https://www.balm.bund.de/Sitemap_Index.xml
The path this Actor reads, /DE/Service/EEREinzelansicht/, is not under any Disallow rule.
The Crawl-delay: 30 directive is stated here in full so you can make your own call: at 30 seconds
per request a full pass of the register would take 20 days, and this Actor does not honour it.
🧪 Verifying this build yourself
node offline_validate.mjs
295 assertions, no network and no platform, run against 12 whole operator pages captured from
balm.bund.de on 2026-09-16 (covering the edge cases: no address, no named representative, a
placeholder permit number, an open-ended permit, branch establishments, the last populated id) plus
the first empty id, the legacy 404 page, robots.txt, and the verbatim <main> slice of
the 120-record random sample the fill table above is counted over.