Germany Transport Operators - VUDat Permits & Fleet avatar

Germany Transport Operators - VUDat Permits & Fleet

Pricing

$2.00 / 1,000 per operator row returneds

Go to Apify Store
Germany Transport Operators - VUDat Permits & Fleet

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

Scrapers Delight

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

3 days ago

Last modified

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 addresses0
tel: / mailto: links0
company websites0 (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 addressable57,645 (ids 1–57,645 populated; 57,646 and up are empty)
Holes inside that rangenone observed — 125 distinct ids inside it came back populated 125/125 (a seeded random sample of 120, plus 5 probed by hand)
Page size47,655 / 48,055 / 50,481 bytes (min / median / max over 120 live fetches)
Anti-botnone — no CAPTCHA, no Cloudflare, no JS gate; a bare request with no User-Agent gets a byte-identical page
Permits per operator127 permits across 120 operators; 7 operators hold more than one
Oldest permit seenvalid 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.

FieldFillFieldFill
companyName100.0%trucks / buses / vans100.0%
legalForm100.0%totalVehicles100.0%
postalCode / city / streetAddress99.2%permitCount / permitTypes100.0%
legalRepresentative99.2%permitNumbers97.5%
transportManager95.0%issuingAuthorities95.8%
tradeRegisterNumber49.2%hasCurrentPermit100.0%
registerCourt50.0%firstPermitValidFrom100.0%
branchCount100% (0 for most)lastPermitValidUntil83.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:

  • tradeRegisterNumber 49.2% — the other half are sole traders (Einzelunternehmen, 60 of 120 in the sample) and natural persons, who have no Handelsregister entry at all. Set requireTradeRegisterNumber if you only want incorporated companies.
  • lastPermitValidUntil 83.3% — 23 of 127 permits publish unbefristet, "open-ended". Those carry isIndefinite: true and isCurrent: true. A parser that read unbefristet as a missing date and then tested validUntil >= today would mark Germany's longest-standing licence holders as expired; this one does not.
  • permitNumbers 97.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)operatorsLegal formoperators
019Einzelunternehmen (sole trader)60
1–245GmbH / UG41
3–933OHG/KG without a natural GP12
10–4919Eingetragener Kaufmann4
50+4 (max 102)AG / OHG / KG3

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.

FieldWhat it is
operatorId / sourceUrlThe VUDat idUnternehmen and a link straight back to the register page
companyNameName as filed
legalFormEinzelunternehmen, GmbH, Aktiengesellschaft, Kommanditgesellschaft
tradeRegisterNumber / registerCourtHRB 17280 / Münster. Null for sole traders — a true absence.
streetAddress / postalCode / city / fullAddressPostal 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 / totalVehiclesVehicle 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 / issuingAuthoritiesFlattened for a spreadsheet. 91 distinct issuing authorities appeared in a 120-operator sample.
transportModesfreight and/or passenger, derived from the permit type
hasCurrentPermit / hasIndefinitePermittrue / false / null when the register published no end date
firstPermitValidFrom / lastPermitValidUntilThe 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.
scrapedAtISO 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_SUMMARY reports the examinedPerDelivered your filters actually produced.

  • operatorIds — read exactly these ids. One page per id: the cheap, precise mode.
  • startId / endId — walk a range. endId: 0 walks 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:

  1. notFound — the id answered HTTP 200 with an empty page. The register holds no operator there. Not a failure, not charged.
  2. filteredOut — a real operator that did not match your filters, with a per-filter breakdown of which one removed it. Not charged.
  3. 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 with operatorIds.

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 runresult
120 requests at concurrency 1067 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 pause53/53 succeeded
live Actor runs at concurrency 219 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: 30
Sitemap: 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.