DVSA Operator Licence Scraper - UK HGV & PSV Fleet Data
Pricing
from $1.20 / 1,000 per row returneds
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
5 days ago
Last modified
Categories
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-P5serving a file 748 hours old. Cache-busting does nothing (the query string is not in the cache key;Cache-ControlandPragmaare ignored). This Actor HEAD-probes every file, rotates the egress IP until the export is inside your freshness window, and then stampssourceLastModified,sourceEtag,sourceVersionId,sourceAgeHoursandsourceCloudfrontPopon 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.
| Field | Fill | What it is |
|---|---|---|
licenceNumber | 100% | The O-licence number (OG0093659). Globally unique — 0 duplicates across all eight regional files |
operatorName | 100% | The licence holder's registered name |
operatorType | 100% | Limited Company 53,790 · Sole Trader 10,537 · Partnership 4,183 · Other (authority/charity/trust) 478 · LLP 139 |
licenceCategory | 100% | 🚛 Goods 64,039 · 🚌 PSV 5,088 — derived from the licence number's first letter |
licenceType | 100% | Restricted 31,967 · Standard National 25,741 · Standard International 11,419 |
licenceStatus / licenceStatusLabel | 100% | Raw lsts_valid 68,588 · lsts_curtailed 522 · lsts_suspended 17, plus a decoded label |
trafficArea / trafficAreaCode | 100% | 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 |
vehiclesAuthorised | 100% | 🔔 The field the whole dataset is priced on. GB total 690,483. Median 3, mean 9.99, max 2,853 (Wincanton Holdings Limited) |
trailersAuthorised | 96.44% | GB total 423,603. The source leaves it blank, not zero, on the other 3.56% |
vehiclesSpecified | 100% | Vehicles actually on the licence today — GB total 446,930 |
vehicleUtilisation / spareVehicleAuthority | 100% | Specified ÷ authorised, and the raw gap. Utilisation 1.0 = the operator has filled every slot it is licensed for |
trailersSpecified | 100% | ⚠️ 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 |
operatingCentres | 99.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 / operatingCentrePostcodes | 100% | Where the vehicles actually are, which is frequently not where the post goes |
transportManagers | 52.88% | Array of named CPC-holding transport managers — 45,955 named posts across 36,553 licences. Blank on Restricted licences, which legally need none |
directorsOrPartners | 84.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 |
companyRegNumber | 78.57% | 54,316 licences: 50,171 eight-digit, 3,583 SC…, 179 NI…, 130 OC…, 10 SO… |
companiesHouseUrl | 78.57% | A ready-made link to that company's Companies House record, for enrichment |
correspondenceAddress | 100% | 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% |
continuationDate | 100% | 🔔 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 |
continuationDueInDays | 100% | Computed at run time |
sourceLastModified, sourceAgeHours, sourceIsStale | 100% | The provenance block — on every row, always, even with includeSourceProvenance off |
sourceEtag, sourceVersionId, sourceCloudfrontPop, sourceEdgeCacheAgeSecs, sourceTransport, sourceFile, sourceUrl, sourceRangeLimited | 100% | The exact S3 object, the exact CloudFront edge, and how it was fetched |
raw | opt-in | The 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
| Buyer | The query |
|---|---|
| Truck, trailer and van dealers · leasing · rental | sortBy: vehiclesAuthorised, maxLicences: 1000 — the biggest 1,000 fleets in GB, ranked, in one run |
| Fleet insurance and finance brokers | minVehiclesAuthorised: 10 → the 13,992 licences that are a real commercial fleet, not a man-and-a-van |
| Telematics, fuel cards, tyres, workshop services | postcodePrefixes on the operating centre, not the correspondence address — 97,348 depots to territory-plan against |
| Compliance, audit and O-licence consultants | continuationDueInNextDays: 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 recruiters | hasTransportManager: "no" + licenceTypes: ["Standard National","Standard International"] — Standard licences must have a CPC holder |
| M&A, market research, PE | The whole register plus the RUN_SUMMARY aggregates: fleet-size distribution, vehicles per traffic area, operator legal form |
| Anyone running a paid fleet list today | The 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.maxLicencesdefaults 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'smaxTotalChargeUsdinstead.
Measured, 07 Aug 2026 export vs 07 Sep 2026 export, all eight regions, 69,127 licences:
| Count | |
|---|---|
| New licences | 418 |
| Changed licences | 8,989 |
| Removed licences | 464 |
| Unchanged | 59,720 |
| Share of the register that moved in 31 days | 14.28% |
Which fields moved, on those 8,989 changed licences:
| Field | Licences |
|---|---|
vehiclesSpecified | 5,198 |
operatingCentres | 2,384 |
transportManagers | 901 |
directorsOrPartners | 817 |
continuationDate | 746 |
vehiclesAuthorised | 426 |
correspondenceAddress | 401 |
trailersAuthorised | 256 |
operatorName | 74 |
licenceStatus | 47 |
licenceType | 38 |
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 fixed | 13,200 — every removal, whatever the filter said ($79.20) |
| Now | 5,544 — exactly the ones whose last-seen row matched ($33.26) |
| Disagreements with running the filter on the full record | 0 |
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 itself — previousTransportManagerCount, 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.
| Event | Price | Fires |
|---|---|---|
licence-scraped | $0.0012 per row | Full-export mode — once per row actually written to your dataset |
licence-change-detected | $0.006 per change | Monitor mode only, and it replaces the row charge. A row is never billed under both |
| What you run | Rows | Cost |
|---|---|---|
| Wales only | 4,744 | $5.69 |
| Every fleet with 10+ vehicles | 13,992 | $16.79 |
| Top 1,000 fleets by size | 1,000 | $1.20 |
| Every licence in Great Britain | 69,127 | $82.95 |
| Great Britain, one row per operating centre | 97,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.
- 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-cacheandPragma: no-cacheall returned the identical cached object with an unchangedAge. 👉 The Actor HEAD-probes, rotates the egress IP untilLast-Modifiedis insidemaxSourceAgeDays, 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 withsourceIsStale: trueon every row and names the edges it tried. OCAddressis 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: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 sumsrows == distinct(TM) × distinct(director)NumberOfVehiclesAuthorisedacross 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.- 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 againstContent-Length(orContent-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. - 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:
| Path | Peak | |
|---|---|---|
Whole-GB export, streaming (maxLicences: 0) | 228 MB | run yLc5kaB4znLDDo7wf, 69,127 rows |
| One traffic area, capped | 98 MB | run zIry9h5GE0Oo9k6hE |
Ranking the whole register uncapped (sortBy + maxLicences: 0) | 432 MB | run 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.
| Run | Input | Result |
|---|---|---|
zIry9h5GE0Oo9k6hE | The demo input (Wales, 50 licences) | 4.3 s, 50 rows, 50 charged, 0.2 MB downloaded, 4 requests, peak 98 MB |
YEW98ZhoH4pxc7Mtb | Empty input {} | 8.0 s, 200 rows, 200 charged, would bill $0.24 |
bWxDsKMqg3LvEWBgw | Rank all of GB, minVehiclesAuthorised: 50, top 500 | 293.8 s, 69,127 licences scanned, 2,176 matched, 500 delivered, 81.1 MB, 0 retries |
yLc5kaB4znLDDo7wf | The whole register (maxLicences: 0) | 610 s, 69,127 rows delivered, 69,127 charged, 81.05 MB, 35 requests, 1 retry, corpusComplete: true |
Sj78vkHsfT5hqp6ak | Wales, uncapped, with a $0.06 charge cap | 50 rows delivered, 50 charged — the cap held exactly, and the log named it as the cause |
0j9ZUJ62SwnoIPM1V | Rank the whole register by fleet size, uncapped | 530 s, 69,127 rows, charged 69,127, peak 424 MB of 1,024 |
| Monitor, Wales | baseline then repeat | 4,744 changes then 0 changes / 0 charged, exiting clean with "nothing in the register changed since the last run" |
E0d98nQVnSheu56ay | Monitor, 6 removals, minVehiclesAuthorised: 10 | 3 rows delivered, 3 charged — the 3 removals whose last-seen row ran 10+ vehicles; the other 3 dropped, never charged |
ozqcADUU5PbdVav6B | The same 6 removals, no filter | 6 rows, 6 charged — the removal feature itself is unchanged |
RWUgRdeJDXOEDlGdO | The same 6, minTransportManagers: 1 | 4 rows, 4 charged — and each carries previousTransportManagerCount, previousTransportManagers and the rest of the row it was judged on |
L9ZWuxjblyhEYHQVB | The same, with includePersonNames: false and includeOperatingCentres: false | 4 rows, 4 charged, and the three opted-out previous… lists are absent from the row, not blanked |
HNDZEsVOexURoNOrv | A baseline from an older build + a filter it cannot answer | 0 rows, 0 charged; all 6 licence numbers handed back free in RUN_SUMMARY |
vjj09Le1b19w3QMH5 | The same, with removalsWithUnmatchableFilters: "deliver" | 6 rows, 6 charged, each flagged not-checkable: …, carrying only the 2 previous… fields that baseline could actually answer |
wKdKDQ3nKoMx4jA4N | GB-wide monitor baseline, all 8 traffic areas | 41.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.
⚖️ Legal and fair use
- 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.