DVSA Operator Licence Scraper - UK HGV & PSV Fleet Data avatar

DVSA Operator Licence Scraper - UK HGV & PSV Fleet Data

Pricing

from $1.20 / 1,000 per row returneds

Go to Apify Store
DVSA Operator Licence Scraper - UK HGV & PSV Fleet Data

DVSA Operator Licence Scraper - UK HGV & PSV Fleet Data

Scrape the UK Traffic Commissioners' operator-licence register: all 69,127 GB goods and PSV O-licences with authorised vehicles and trailers, operating centres, transport managers, directors and Companies House numbers. Every row carries the age of the export it came from.

Pricing

from $1.20 / 1,000 per 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

5 days ago

Last modified

Share

🚛 DVSA Operator Licence Scraper — every HGV and PSV fleet in Great Britain, with proof of how fresh it is

Every lorry, tipper, tanker, bus and coach operator in Great Britain has to hold an operator licence (O-licence) from a Traffic Commissioner. The Traffic Commissioners publish the whole granted register — 69,127 licences, 690,483 authorised vehicles, 423,603 authorised trailers, 97,348 operating centres — as eight regional CSVs on data.gov.uk.

This Actor turns that raw export into a clean, filterable, rankable dataset: one row per licence (or one row per operating centre), with the operating centres split out of the cell the publisher jams them into, the transport managers and directors de-duplicated, the postcodes parsed, and a Companies House link on every licence that has a company number.

The thing nobody else checks. Those eight CSVs sit behind CloudFront, and a stale edge answers HTTP 200 with a month-old file and no error of any kind. Measured on 2026-09-08: from one machine every route landed on an edge still serving the 07 Aug export while the origin already had the 07 Sep one — and from an Apify worker the un-proxied route hit IAD12-P5 serving a file 748 hours old. Cache-busting does nothing (the query string is not in the cache key; Cache-Control and Pragma are ignored). This Actor HEAD-probes every file, rotates the egress IP until the export is inside your freshness window, and then stamps sourceLastModified, sourceEtag, sourceVersionId, sourceAgeHours and sourceCloudfrontPop on every single row. You never have to take our word for it.


📊 What you get, per row

Fill rates below are measured over all 69,127 licences in the 07 Sep 2026 export (parsed 2026-09-08), not estimated.

FieldFillWhat it is
licenceNumber100%The O-licence number (OG0093659). Globally unique — 0 duplicates across all eight regional files
operatorName100%The licence holder's registered name
operatorType100%Limited Company 53,790 · Sole Trader 10,537 · Partnership 4,183 · Other (authority/charity/trust) 478 · LLP 139
licenceCategory100%🚛 Goods 64,039 · 🚌 PSV 5,088 — derived from the licence number's first letter
licenceType100%Restricted 31,967 · Standard National 25,741 · Standard International 11,419
licenceStatus / licenceStatusLabel100%Raw lsts_valid 68,588 · lsts_curtailed 522 · lsts_suspended 17, plus a decoded label
trafficArea / trafficAreaCode100%One of the eight Traffic Commissioner areas, plus its licence letter (B/C/D/F/G/H/K/M). Verified 1:1 against the licence number on all 69,127 rows
vehiclesAuthorised100%🔔 The field the whole dataset is priced on. GB total 690,483. Median 3, mean 9.99, max 2,853 (Wincanton Holdings Limited)
trailersAuthorised96.44%GB total 423,603. The source leaves it blank, not zero, on the other 3.56%
vehiclesSpecified100%Vehicles actually on the licence today — GB total 446,930
vehicleUtilisation / spareVehicleAuthority100%Specified ÷ authorised, and the raw gap. Utilisation 1.0 = the operator has filled every slot it is licensed for
trailersSpecified100%⚠️ Always 0. The publisher does not populate this column — it is 0 on all 179,733 source rows. We ship it so you can see that for yourself rather than wondering
operatingCentres99.61%Array of {address, postcode}97,348 centres GB-wide, 13,759 licences with more than one, max 23. 99.68% of them yield a parseable postcode
operatingCentreCount / operatingCentrePostcodes100%Where the vehicles actually are, which is frequently not where the post goes
transportManagers52.88%Array of named CPC-holding transport managers — 45,955 named posts across 36,553 licences. Blank on Restricted licences, which legally need none
directorsOrPartners84.07%Array of {name, role}129,674 distinct people, 119,010 Directors and 10,664 Partners. The source's None sentinel is never emitted as a person
companyRegNumber78.57%54,316 licences: 50,171 eight-digit, 3,583 SC…, 179 NI…, 130 OC…, 10 SO…
companiesHouseUrl78.57%A ready-made link to that company's Companies House record, for enrichment
correspondenceAddress100%The address the Traffic Commissioner writes to
correspondencePostcode (+ area, district)99.98%Parsed case-insensitively on purpose — the source contains lowercase postcodes (sg1 2bh, lE16 9EE), and a case-sensitive regex only reaches 99.29%
continuationDate100%🔔 The five-yearly renewal date, which the operator pays a fee to meet: 3,715 fall due in 2026, 13,024 in 2027, 13,366 in 2028, 14,196 in 2029, 14,350 in 2030
continuationDueInDays100%Computed at run time
sourceLastModified, sourceAgeHours, sourceIsStale100%The provenance block — on every row, always, even with includeSourceProvenance off
sourceEtag, sourceVersionId, sourceCloudfrontPop, sourceEdgeCacheAgeSecs, sourceTransport, sourceFile, sourceUrl, sourceRangeLimited100%The exact S3 object, the exact CloudFront edge, and how it was fetched
rawopt-inThe 16 untouched CSV cells, so you can audit any derived field against the publisher's own bytes

Monitor mode adds: changeType, changedFields, previousVehiclesAuthorised, previousTrailersAuthorised, previousVehiclesSpecified, previousOperatingCentreCount, previousLicenceStatusLabel, previousOperatorName, vehiclesAuthorisedDelta, operatingCentreCountDelta, fieldsAvailable, removalFilterCheck.

A removed row also carries everything the baseline remembered, because those are the fields the removal was judged on: previousLicenceType, previousOperatorType, previousLicenceStatus, previousCompanyRegNumber, previousTransportManagerCount, previousHasTransportManager, previousVehicleUtilisation, previousContinuationDate, previousCorrespondencePostcode, previousOperatingCentrePostcodes, previousTransportManagers, previousDirectorOrPartnerNames. The licence is gone, so the current columns on that row are empty (transportManagerCount: 0, companyRegNumber: null) — the previous… pair is the real value, exactly as operatingCentreCount / previousOperatingCentreCount already works. A previous… key is absent, not null, when the baseline never stored that field (a baseline written by an older build of this Actor): absent means unknown, never "the operator had none". includePersonNames: false drops the two name lists and includeOperatingCentres: false drops the postcode list, the same way they do on a live row.


🎯 Who buys this, and what they buy it for

BuyerThe query
Truck, trailer and van dealers · leasing · rentalsortBy: vehiclesAuthorised, maxLicences: 1000 — the biggest 1,000 fleets in GB, ranked, in one run
Fleet insurance and finance brokersminVehiclesAuthorised: 10 → the 13,992 licences that are a real commercial fleet, not a man-and-a-van
Telematics, fuel cards, tyres, workshop servicespostcodePrefixes on the operating centre, not the correspondence address — 97,348 depots to territory-plan against
Compliance, audit and O-licence consultantscontinuationDueInNextDays: 365 (12,735 licences due within a year on the 07 Sep 2026 export), or licenceStatuses: ["curtailed","suspended"] — 539 fleets currently under a Traffic Commissioner sanction
Transport recruitershasTransportManager: "no" + licenceTypes: ["Standard National","Standard International"] — Standard licences must have a CPC holder
M&A, market research, PEThe whole register plus the RUN_SUMMARY aggregates: fleet-size distribution, vehicles per traffic area, operator legal form
Anyone running a paid fleet list todayThe best-known commercial product built on this same source covers fleets of 75+ vehicles — about 1,000 of them. Only 2,176 GB licences have 50 or more vehicles and 820 have 100 or more; the other ~68,000 small and mid operators are in here too

🔎 What you can filter on

Every filter runs over the whole file before any row cap, so the counts the run log prints are real. 30+ inputs, all wired to behaviour:

  • Scope — traffic areas (any of the eight, or all of GB), row cap, one row per licence or per operating centre
  • Fleet size — min/max authorised vehicles, authorised trailers, specified vehicles, utilisation ratio
  • Licence — Goods vs PSV, Restricted / Standard National / Standard International, valid / curtailed / suspended
  • Operator — legal form, name (contains / starts with / exact, accent-insensitive), has a Companies House number
  • Geography — postcode prefixes matched against the correspondence address, the operating centres, or either
  • People — has a named transport manager, transport-manager name contains, director/partner name contains, min transport managers
  • Sites — min/max operating centres
  • Renewal — continuation due within N days, or between two dates
  • Lookup — exact licence numbers, exact Companies House numbers

🏆 The ranking that makes this worth more than the raw CSV

{ "sortBy": "vehiclesAuthorised", "sortOrder": "desc", "maxLicences": 1000 }

The ranking is applied across every licence read, before the cap is taken — a bounded top-N selector, verified against a full sort of all 69,127 licences. So that really is the 1,000 biggest fleets in Great Britain, not the biggest of the first 1,000 rows. Measured on the 07 Sep 2026 export: the 1,000th biggest fleet holds 85 authorised vehicles; the largest is Wincanton Holdings Limited at 2,853.

Verified live on the platform (run bWxDsKMqg3LvEWBgw, 2026-09-08): all eight files read, 69,127 licences scanned, 2,176 matched minVehiclesAuthorised: 50, top 500 delivered, 81.1 MB, 28 HTTP requests, 0 retries, 293.8 s, peak 219 MB.


🔔 Monitor mode — only what moved

Set mode: "monitor" and schedule it. The Actor downloads the same files, diffs them against a baseline kept in a named key-value store (an unnamed store is wiped every run, which would make every licence look new every time), and delivers only the licences that are new, changed or gone — with changedFields naming exactly which fields moved.

Monitor mode needs maxLicences: 0, and refuses to start without it. maxLicences defaults to 200, so it has to be set to 0 explicitly. A capped monitor run cannot tell unchanged from not reached, so it deliberately does not advance its baseline — which would leave it reporting the same licences as new, and billing for them, on every single run. Rather than let that happen quietly the Actor fails before it downloads or charges anything and says so. To bound what a monitor run can spend, use the run's maxTotalChargeUsd instead.

Measured, 07 Aug 2026 export vs 07 Sep 2026 export, all eight regions, 69,127 licences:

Count
New licences418
Changed licences8,989
Removed licences464
Unchanged59,720
Share of the register that moved in 31 days14.28%

Which fields moved, on those 8,989 changed licences:

FieldLicences
vehiclesSpecified5,198
operatingCentres2,384
transportManagers901
directorsOrPartners817
continuationDate746
vehiclesAuthorised426
correspondenceAddress401
trailersAuthorised256
operatorName74
licenceStatus47
licenceType38

Narrow watchedFields to cut both the noise and the bill: watching only vehiclesAuthorised and licenceStatus turns 8,989 rows into 473.

A removed licence is filtered like any other row

A removal is the one row type that has no current values to test — the licence is gone from the export, so all that is left is what the last run saw. This Actor therefore keeps every field its filters read in the baseline (licence type, operator type, company registration number, status, the four vehicle/trailer counts, operating-centre count and postcodes, correspondence postcode, transport-manager and director names, continuation date) and re-runs the same predicate against that stored row. A removal is delivered — and billed — only when the licence it replaces would itself have matched your filters.

Measured on Wales (4,744 licences), 400 removals put through all 33 filter clauses one at a time — 13,200 removal decisions in total:

Removals delivered and billed
Before this was fixed13,200 — every removal, whatever the filter said ($79.20)
Now5,544 — exactly the ones whose last-seen row matched ($33.26)
Disagreements with running the filter on the full record0

Concretely: a minVehiclesAuthorised: 10 monitor fed 200 removals (100 large fleets, 100 small) used to deliver and bill all 200 for $1.20; it now delivers the 100 that really did run 10+ vehicles, for $0.60, and drops the rest without charging. Every delivered removal carries removalFilterCheck: "matched-against-previous-run-state" so the judgement is auditable, and the remembered row itselfpreviousTransportManagerCount, previousCompanyRegNumber, previousLicenceType and the rest (full list in the field table above) — so a row billed for matching minTransportManagers: 1 shows you the transport managers it matched on instead of only a transportManagerCount: 0 for a licence that no longer exists. The run log and RUN_SUMMARY both report how many removals were dropped.

The one case this cannot answer is a baseline written by an older build of this Actor, which did not keep all those fields. removalsWithUnmatchableFilters decides what happens then: the default "skip" never charges for a removal it cannot vouch for and hands you the licence numbers for free in RUN_SUMMARY under monitor.removalsSkippedAsUnmatchable; "deliver" ships them at $0.006 each, flagged removalFilterCheck: "not-checkable: …". Old baselines are upgraded in place, not rebuilt, so nothing is re-reported and nothing is re-billed — from the second run onwards the question does not arise. Keeping the extra fields costs the baseline 1,805,740 → 3,293,541 bytes for the largest traffic area (East of England, 13,144 licences), against a 9 MB key-value record ceiling. The second figure is not a projection: it is what a live GB-wide run reports in RUN_SUMMARY.monitor.stateRecordBytesMax, to the byte.

Honest about what "new" means here. Net licence churn is small — 418 new against 464 removed is a net -46 in 31 days. The value in monitoring this register is the field-level movement (a fleet growing, a depot opening, a transport manager leaving, a licence being curtailed), not a flood of brand-new operators. Anyone selling you "hundreds of new UK hauliers every month" from this source is inventing it.


🚀 Example inputs

The 1,000 biggest fleets in Great Britain

{ "sortBy": "vehiclesAuthorised", "sortOrder": "desc", "maxLicences": 1000 }

Every commercial fleet in South Wales, by depot location

{
"regions": ["Wales"],
"postcodePrefixes": ["CF", "NP", "SA"],
"postcodeMatchField": "operatingCentre",
"minVehiclesAuthorised": 5,
"maxLicences": 0
}

Renewal leads: O-licences up for continuation in the next 6 months

{ "continuationDueInNextDays": 180, "minVehiclesAuthorised": 10, "sortBy": "continuationDate", "sortOrder": "asc", "maxLicences": 0 }

Compliance signal: every fleet currently curtailed or suspended

{ "licenceStatuses": ["curtailed", "suspended"], "maxLicences": 0 }

Every bus and coach operator, one row per depot

{ "licenceCategories": ["psv"], "outputLevel": "operating-centre", "maxLicences": 0 }

Weekly change monitor (schedule it)

{ "mode": "monitor", "maxLicences": 0, "watchedFields": ["vehiclesAuthorised", "licenceStatus", "operatingCentres"], "monitorStateKey": "gb-all" }

The whole register

{ "maxLicences": 0 }

💵 Pricing

Pay per event. No start fee, no monthly fee, no platform-usage surcharge.

EventPriceFires
licence-scraped$0.0012 per rowFull-export mode — once per row actually written to your dataset
licence-change-detected$0.006 per changeMonitor mode only, and it replaces the row charge. A row is never billed under both
What you runRowsCost
Wales only4,744$5.69
Every fleet with 10+ vehicles13,992$16.79
Top 1,000 fleets by size1,000$1.20
Every licence in Great Britain69,127$82.95
Great Britain, one row per operating centre97,348$116.82
A monthly GB change monitor, all 13 fields watched~9,871$59.23
The same monitor watching only fleet size + status~473$2.84

Things that are never billed: rows your filters removed — including removals whose last-seen row did not match your filters — licences dropped by cross-run de-duplication, rows a charge cap prevented from being delivered, and any traffic area that could not be downloaded. Two things that ARE billed and are easy to miss, so they are stated here rather than discovered on an invoice: ticking changeTypes: ["unchanged"] delivers — and charges licence-change-detected for — every licence that did not move, at $0.006 a row rather than the $0.0012 the same row costs in full-export mode (all of GB = 69,127 rows ≈ $414.76 for a run where nothing changed; it is off by default). Delivery is budget-aware — rows are written and charged atomically, so delivered always equals billed, and if a cap truncates the run the log says so and names the cap as the cause.

outputLevel: "operating-centre" legitimately bills more rows than licence mode (97,348 against 69,127 for all of GB) because those rows are genuinely delivered — one per depot. That is stated here, on the pricing page, and in the input form, so it can never read as a trick.


⚙️ How it works, and what it does not hide

The route. Eight public CSVs on content.mgmt.dvsacloud.uk (Amazon S3 behind CloudFront), listed on data.gov.uk under the dataset Traffic Commissioners: goods and public service vehicle operator licence records. No login, no API key, no cookies, no CAPTCHA, no browser. Recon and live runs made ~100 requests with zero non-200 responses and zero rate limiting. There is no robots.txt on the data host (the path 403s an AccessDenied), and www.data.gov.uk/robots.txt disallows only /v1. Nothing here is disallowed.

Four ways this route silently lies, and what the Actor does about each.

  1. A stale CloudFront edge serves a month-old file with HTTP 200. Measured 2026-09-08: the un-proxied route from an Apify worker returned a 748-hour-old export; across 12 rotated proxy sessions, 7 (58%) hit a fresh edge and 5 hit a stale one (fresh: YUL62, SEA900, ARN56, JFK50 — stale: ORD58, DFW56, LAX50). Query-string cache-busting, Cache-Control: no-cache and Pragma: no-cache all returned the identical cached object with an unchanged Age. 👉 The Actor HEAD-probes, rotates the egress IP until Last-Modified is inside maxSourceAgeDays, downloads on the same approved session, and asserts the ETag of the body matches the one the probe approved. If it never finds a fresh copy it delivers the freshest one it saw with sourceIsStale: true on every row and names the edges it tried.
  2. OCAddress is not one operating centre — it is all of them, semicolon-joined into a single cell, byte-identical on every row of a licence (verified: 0 of 69,127 licences vary it). The row multiplication is transport managers × directors, not centres:
    rows == distinct(TM) × distinct(director)
    holds for 68,817 of 69,127 (99.55%). 👉 Read naively you get 179,733 rows where there are 69,127 licences, and anyone who sums NumberOfVehiclesAuthorised across raw rows overstates the GB fleet by 2.6× (1,806,663 instead of the true 690,483). This Actor de-duplicates on licence number, splits the centre list on ;, and de-duplicates inside the cell too — 74 licences repeat an address in their own cell.
  3. A proxy hop can drop the connection mid-body and still return HTTP 200. Measured 2026-09-08 (run cecZj8th5gyrlJD33): 1.6 MB of the 4.5 MB Wales export came back, the ETag matched, the CSV parsed cleanly — and the run reported 2,294 licences as if that were the whole traffic area. 👉 Every download now counts its own bytes against Content-Length (or Content-Range) and rejects a short body, and the parser independently refuses a full read whose body stops without a line terminator. A truncated transfer is retried on a different egress IP; it is never delivered.
  4. The traffic areas are eight separate files with no directory listing (the S3 prefix 403s), so a ninth would be invisible. 👉 The run reads the data.gov.uk CKAN record at the start and warns if the publisher has added, renamed or dropped an export.

Order of operations, because a filter applied after truncation is a lie. Filters run over the whole parsed file, before the cap. Ranking runs across every licence read, before the cap. The RUN_SUMMARY aggregates cover every matching licence the run examined, not the delivered slice (and they count the licences cross-run de-duplication dropped, which are never billed), and carry a corpusComplete flag that goes false — with a named reason — whenever a cap, a byte-range read, a skipped stale area or a failed download meant the run did not see the whole thing. When corpusComplete is false the aggregates are a sample of what the sweep reached before it stopped, and aggregatesCover says so in words.

Memory, measured rather than guessed. The Actor declares 1,024 MB, because two paths were measured on the platform, not estimated:

PathPeak
Whole-GB export, streaming (maxLicences: 0)228 MBrun yLc5kaB4znLDDo7wf, 69,127 rows
One traffic area, capped98 MBrun zIry9h5GE0Oo9k6hE
Ranking the whole register uncapped (sortBy + maxLicences: 0)432 MBrun UW0tGfBlBEtNMDyKl, 69,127 rows in 174.6 s

Ranking is the one operation that has to buffer: a sort cannot write its first row until it has seen every matching licence. At 512 MB that path was OOM-killed at a 508 MB peak after it had already billed 32,000 rows (run jZlyMtwOBdXDijYiW) — so the Actor now checks the memory it has been given before it downloads anything, and refuses with an actionable message instead of dying half-way through a paid run. 1,024 MB clears every measured path with better than 2× headroom. (An earlier 256 MB build was also OOM-killed on a whole-GB export; the parser was then rewritten to walk bytes rather than materialise the file as a string, which cut whole-corpus parse time from 7.9 s to 1.4 s and the peak from 256 MB to 228 MB.)

Speed and cost. The un-proxied route pulls all 81 MB in about 8 seconds; the proxy is roughly 35× slower on this source (measured 12 s to 270 s for a single regional file), which is why the default is try direct first, rotate only when the file is too old. A run with a row cap and no filter fetches only a byte range of each CSV — 50 licences from Wales is ~150 KB instead of 4.5 MB. A stalled download rotates to a different egress IP rather than waiting out a slow session.


⏱️ Measured on the Apify platform, not estimated

Every figure below is from a real run of this Actor on 2026-09-08, or 2026-09-09 for the removal-filter rows.

RunInputResult
zIry9h5GE0Oo9k6hEThe demo input (Wales, 50 licences)4.3 s, 50 rows, 50 charged, 0.2 MB downloaded, 4 requests, peak 98 MB
YEW98ZhoH4pxc7MtbEmpty input {}8.0 s, 200 rows, 200 charged, would bill $0.24
bWxDsKMqg3LvEWBgwRank all of GB, minVehiclesAuthorised: 50, top 500293.8 s, 69,127 licences scanned, 2,176 matched, 500 delivered, 81.1 MB, 0 retries
yLc5kaB4znLDDo7wfThe whole register (maxLicences: 0)610 s, 69,127 rows delivered, 69,127 charged, 81.05 MB, 35 requests, 1 retry, corpusComplete: true
Sj78vkHsfT5hqp6akWales, uncapped, with a $0.06 charge cap50 rows delivered, 50 charged — the cap held exactly, and the log named it as the cause
0j9ZUJ62SwnoIPM1VRank the whole register by fleet size, uncapped530 s, 69,127 rows, charged 69,127, peak 424 MB of 1,024
Monitor, Walesbaseline then repeat4,744 changes then 0 changes / 0 charged, exiting clean with "nothing in the register changed since the last run"
E0d98nQVnSheu56ayMonitor, 6 removals, minVehiclesAuthorised: 103 rows delivered, 3 charged — the 3 removals whose last-seen row ran 10+ vehicles; the other 3 dropped, never charged
ozqcADUU5PbdVav6BThe same 6 removals, no filter6 rows, 6 charged — the removal feature itself is unchanged
RWUgRdeJDXOEDlGdOThe same 6, minTransportManagers: 14 rows, 4 charged — and each carries previousTransportManagerCount, previousTransportManagers and the rest of the row it was judged on
L9ZWuxjblyhEYHQVBThe same, with includePersonNames: false and includeOperatingCentres: false4 rows, 4 charged, and the three opted-out previous… lists are absent from the row, not blanked
HNDZEsVOexURoNOrvA baseline from an older build + a filter it cannot answer0 rows, 0 charged; all 6 licence numbers handed back free in RUN_SUMMARY
vjj09Le1b19w3QMH5The same, with removalsWithUnmatchableFilters: "deliver"6 rows, 6 charged, each flagged not-checkable: …, carrying only the 2 previous… fields that baseline could actually answer
wKdKDQ3nKoMx4jA4NGB-wide monitor baseline, all 8 traffic areas41.8 s, 69,127 licences diffed, peak 284 MB of 1,024, largest state record 3,293,541 bytes of a 9 MB ceiling, 81.0 MB in 16 requests

Freshness on that whole-GB run: all eight exports came back 27.63–27.64 hours old — the same 07 Sep 23:02 publication, served consistently from one CloudFront edge (IAD12-P5) with no egress rotation needed. On an earlier whole-GB run the same eight files came back 5.75–5.90 hours old and five of the eight did need a rotation, which is exactly why the rotation exists. Delivered rows and charged events matched exactly on every run above — delivered always equals billed.

Parsing cost: all eight files, 81 MB, 179,733 source rows → 69,127 licence records in 1.4 s of CPU (the parser walks the bytes, so a whole traffic area is never held in memory twice). The network is the entire cost of a big run, not the parse.


🔒 Privacy — read this before you use the people fields

This is a statutory public register, published by the Traffic Commissioners under the UK Open Government Licence, and it names individuals at scale: 129,674 directors and partners and 45,955 transport-manager posts. 10,537 sole traders and 4,183 partnerships mean the correspondence address — and often the operating centre — is somebody's home address.

The Actor ships includePersonNames (default on). Turn it off and the transportManagers and directorsOrPartners arrays are dropped while the counts stay, so you can use the fleet data without holding named natural persons.

The publisher's licence permits commercial reuse. UK GDPR still applies to the natural-person rows independently of that, and how you use them — marketing, enrichment, retention — is your decision and your legal responsibility, not ours.


  • Source: Traffic Commissioners: goods and public service vehicle operator licence records, published on data.gov.uk from the Vehicle Operator Licensing (VOL) database. Confirmed licence: uk-ogl — UK Open Government Licence (OGL), read live from the publisher's own CKAN record.
  • Attribution, as OGL requires: Contains public sector information licensed under the Open Government Licence v3.0.
  • This Actor reads only the bulk export the publisher put on data.gov.uk for reuse. It does not touch the interactive VOL lookup service, and it holds no credentials.
  • Great Britain only. Northern Ireland operator licensing is a separate register (opendatani.gov.uk) and is not in this export. We say GB, not UK, on purpose.
  • Nothing here is legal advice. What you do with personal data in this register is your call.

❓ FAQ

Is this the same as the DVSA's own operator licence lookup? It is the same underlying register (the VOL database), published by the Traffic Commissioners as a bulk export for reuse. The interactive lookup service sits behind a bot-protection interstitial; the bulk export is the route the publisher intends for reuse, and it is the only one this Actor uses.

How often is it actually updated? Don't just tell me "weekly". The publisher's own note on data.gov.uk says "These records are updated on a weekly basis, every Sunday." We quote that as their claim. What we have measured is three Last-Modified stamps: Sun 18 May 2025 22:00 GMT, Fri 07 Aug 2026 23:02 GMT and Mon 07 Sep 2026 23:02 GMT — two of those three are not Sundays. So we do not repeat the weekly claim as fact. What we guarantee instead is the thing that actually matters: every row tells you exactly how old the file it came from is, and you can refuse anything older than maxSourceAgeDays.

Why does one licence have 300+ rows in the raw CSV? Because the publisher writes one row per transport-manager × director combination. DPDgroup UK Ltd (OG0093659) is 184 raw rows: 23 transport managers × 8 directors, one operating-centre cell. This Actor collapses that to one row per licence with both lists de-duplicated.

Are the operating centres really all in one cell? Yes — semicolon-joined, and identical on every row of the licence. DHL Supply Chain's cell even repeats one Ebbw Vale address. We split on ;, trim, de-duplicate and parse each postcode.

Why is trailersSpecified always 0? Because the publisher does not populate that column. It is 0 on all 179,733 source rows. We ship it rather than dropping it so you can verify that yourself, and so nothing looks like it went missing.

Can I get just the biggest fleets without paying for the whole register? Yes, and that is the intended use: sortBy: "vehiclesAuthorised", sortOrder: "desc", maxLicences: 1000 costs $1.20 and ranks across all 69,127 licences before the cap is applied.

What is the difference between authorised and specified vehicles? Authorised is the ceiling the Traffic Commissioner granted (690,483 GB-wide). Specified is what is actually on the licence today (446,930). The gap — spareVehicleAuthority — is headroom the operator has already paid to have.

How do I turn a licence into a full company record? 78.57% of licences carry a Companies House number and every one of those rows ships a ready-made companiesHouseUrl. Feed those into a Companies House scraper for accounts, SIC codes and officers.

Does monitor mode charge me for a run where nothing changed? No — with one opt-in exception. There is deliberately no per-run charge, and a monitor run that finds nothing delivers nothing and bills nothing; the log says the register has not moved since the last run rather than pretending something went wrong. The exception is changeTypes: ["unchanged"], which asks for every licence that did not move: those rows are delivered and billed at the change price. It is off by default, the input form says what it costs, and the run log warns before it happens.

Why won't monitor mode run with maxLicences set? Because it could never make progress. A capped run cannot tell unchanged from not reached, so it does not advance its baseline — and would therefore report the same licences as new, and bill for them, on every run. The Actor refuses before downloading or charging anything and tells you to set maxLicences: 0 (it defaults to 200). Bound a monitor run's spend with maxTotalChargeUsd.

What happens if my charge cap runs out mid-run? Rows are written and charged atomically, so you are never billed for a row you did not receive. The run stops, the log names the cap as the cause and says exactly how many rows were withheld, and in monitor mode the baseline is not advanced — so the next run reports those changes again instead of losing them.

What if one of the eight files won't download? It is retried on a different egress IP, with a much longer timeout on the last attempt. If it still fails, that traffic area is skipped, named in regionsFailed, and corpusComplete goes false — a partial sweep is never reported as a complete one. Set onRegionFailure: "fail" if you would rather the whole run stop.

Is Northern Ireland included? No. This export is Great Britain: eight Traffic Commissioner areas covering England, Scotland and Wales. Northern Ireland licensing is a separate register on opendatani.gov.uk.

Does it need a proxy? Not for access — every file returns HTTP 200 from a plain IP. The proxy is there to rotate around a stale CloudFront edge, which is the only reliable way to get the current file. Set proxyMode: "never" if you genuinely do not care how old the data is.