# UK HGV Operator Licence Applications & Decisions (`scrapersdelight/traffic-commissioner-applications-scraper`) Actor

From $3.00 per 1,000 rows, no start fee. Every HGV operator-licence application, variation, grant, refusal, surrender and revocation the UK Traffic Commissioners publish weekly: licence number and class, operator, directors, address, operating centre, vehicles and trailers, objection deadline.

- **URL**: https://apify.com/scrapersdelight/traffic-commissioner-applications-scraper.md
- **Developed by:** [Scrapers Delight](https://apify.com/scrapersdelight) (community)
- **Categories:** Lead generation, Business, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$3.00 / 1,000 per licence entry returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## UK HGV Operator Licence Applications & Decisions

Every week each of the eight GB traffic areas publishes an **Applications and Decisions** booklet —
who has applied for a goods-vehicle operator's licence, who has been granted or refused one, who is
adding vehicles or moving an operating centre, and who has surrendered or lost a licence. It is a
PDF. This Actor turns it into rows.

***

### What you actually get — counted, not estimated

Measured on **24 live editions** (3 per traffic area: January, May and September 2026), **480
pages**, **1,741 licence entries**. Every percentage below is a count over those 1,741 rows, not a
sample or an estimate. A literal `"N/A"`, `"-"` or `""` was counted as **not** filled.

| Field | Fill | Notes |
|---|---|---|
| `licenceNumber` | **100.0%** | `OM2091163` — the operator's O-licence number |
| `operatorName` | **100.0%** | |
| `entryType` / `section` / `sectionGroup` | **100.0%** | what kind of entry this is |
| `licenceClass` | **98.7%** | R / SN / SI, plus the spelled-out `licenceClassName` |
| `correspondenceAddress` | **96.4%** | full postal address |
| `postcode` | **96.4%** | normalised, e.g. `EH45 9DP` |
| `people` | **89.0%** | named directors, partners or owners — 3,136 names over the 1,741 entries |
| `authorisationText` | **66.8%** | the authorisation line verbatim |
| `operatingCentres` | **65.1%** | 1,317 centres, each with its own address, postcode and change type |
| `vehiclesAuthorised` | **59.0%** | integer, or `null` — never a fabricated `0` |
| `trailersAuthorised` | **34.8%** | same |
| `transportManagers` | **21.5%** | 403 named transport managers |
| `publicInquiry*` | 7.3% | hearing reference, venue, date and time |
| `decisions` | 6.7% | the S26 / S27 / S28 / Sch.3 lines |

**Fill is much higher on the rows most people are buying.** For the 656 entries in the
*applications received* group (sections 1.1 and 1.2 — the new and variation applications):

| Field | Fill over the 656 application entries |
|---|---|
| `licenceNumber`, `licenceClass`, `operatorName`, `correspondenceAddress`, `postcode` | **100.0%** |
| `authorisationText` | **97.9%** |
| `operatingCentres` | **95.9%** |
| `people` (directors / partners) | **93.3%** |
| `vehiclesAuthorised` | **85.2%** |
| `trailersAuthorised` | 51.2% |
| `transportManagers` | 33.4% |

`objectionDeadline` is on **100%** of rows — it comes from the edition's own cover page, and it is
the date an objector, a competitor or a salesperson is actually working to.

### What you do NOT get, and why

**There is no email column and no phone column.** Not sparse ones — none at all.

Counted over the 1,741 entries: **0 operator email addresses** and **0 operator telephone numbers**.

There are exactly **13 distinct e-mail addresses in the entire 24-edition corpus** and every one of
them is the Traffic Commissioner's own — `enquiries@otc.gov.uk`, `Environmental@otc.gov.uk`,
`scotlandhc@otc.gov.uk` and ten more, all `gov.uk`, all sitting in the boilerplate that prefaces each
section. The only telephone number in the documents is the OTC switchboard. The Office of the Traffic
Commissioner does not publish operator contact details, so there is nothing to scrape and the column
would have been 0% full in every row. It has been cut rather than shipped empty.

**What this is instead:** a very strong enrichment key and a timing signal. Company name + full
postal address + postcode + named directors matches cleanly against Companies House, and the
objection deadline tells you when the application was live. Pair it with a contact-finding step; do
not expect to dial from it.

### The other honest limits

- **The window is ~8.5 months and it rolls.** Each regional page lists the most recent **37 weekly
  editions** — measured 2026-09-17: 296 editions total, covering 01 Jan 2026 to 16 Sep 2026. Older
  editions are delisted, and their asset URLs carry an unguessable random id
  (`/media/695b986a6a4ea67a402a83ff/`), so once an edition drops off there is **no archive endpoint
  to reach back into**. If you need history, run this weekly and keep the rows.
- **The section-5.3 and 5.4 transport-manager inquiry notices are deliberately not emitted.** They
  name a *person* under investigation, not a licence, and carry no licence number — a different unit
  of data. 318 of them were seen across the 24 editions; the count is reported in `RUN_SUMMARY` and
  you are never charged for them. The two entry types stay in the filter list in case the source
  ever prints a licence-bearing entry under those headings.
- **An address with no recognisable UK postcode is reported as `null`, not guessed.** That covers a
  handful of foreign correspondence addresses and the ~3.6% of entries (mostly refusals and
  withdrawals) that the booklet prints with directors but no address.
- **1.2% of entries repeat a licence number within one edition** — the source really does list the
  same licence in two sections. They are delivered as published; set `oneRowPerLicence` to collapse
  them, and the dropped rows are not charged for.
- **The parse was never tested through Apify's proxy from this machine.** It was verified against
  the live source over a home connection: 16 PDFs fetched in parallel, 16× HTTP 200 in 1.36 s, zero
  truncated. gov.uk has no anti-bot on this path. Every download is nonetheless byte-checked against
  the `file_size` gov.uk publishes and re-fetched on a fresh session if it falls short.
- **Licence-number cross-references are excluded.** A licence quoted inside a sentence
  (`OM0002637 has been surrendered as part of this application.`) begins a line whenever the
  paragraph wraps there. 10 of the 1,750 reference lines in the sample are these, and none of them
  is delivered or charged for.

### Scope and rights

- Source: the Office of the Traffic Commissioner's weekly *Applications and Decisions* booklets,
  published on gov.uk under the **Open Government Licence v3.0**.
- `https://www.gov.uk/robots.txt` allows this path — for `User-agent: *` it disallows only
  `/*/print$` and `/search/all*`. `https://assets.publishing.service.gov.uk/robots.txt` returns
  **HTTP 404**: there is no robots file on the asset host at all.
- These documents name individuals (directors, partners, transport managers) because the statutory
  publication does. If you process them, that is personal data under UK GDPR and the lawful basis is
  yours to establish.

### Filters

Every filter is applied **before** delivery, so an excluded row is never charged for.

- **Traffic areas** — any of the eight, or all of them.
- **Weekly editions per area** — 1 (this week) up to 37 (everything addressable). Date windows
  (`publishedFrom` / `publishedTo`) are applied to the catalogue *before anything is downloaded*.
- **Section groups** — applications received (656 of 1,741 measured) · applications decided (558) ·
  withdrawn and public inquiries (178) · surrendered, revoked, terminated (349).
- **Entry types** — the 20 finer types underneath those groups.
- **Licence class** — R, SN, SI.
- **Postcode prefixes** — matched against the correspondence postcode *or* any operating-centre
  postcode. `"G"`, `"EH"`, `"ML4"`.
- **Minimum vehicles authorised** — note that a `null` vehicle count is excluded by this filter
  rather than treated as zero.
- **Free-text keyword**, **require an address**, **one row per licence**.

### Pricing

**$0.003 per licence entry delivered — $3.00 per 1,000 rows. No run-start fee.**

You are charged once per row that lands in your dataset. Entries excluded by your filters are never
delivered and never charged. An edition that could not be downloaded, one that failed the integrity
check, and one that genuinely published nothing are each reported separately in `RUN_SUMMARY` and
charged for at zero. A run that matches nothing costs nothing.

A single week across all eight areas is about **580 rows ≈ $1.74**.

### RUN\_SUMMARY

Written to the key-value store on every run, successful or not. It keeps the three kinds of nothing
apart, because they mean different things:

- `unreachableEditions` — the download failed, or returned something that was not a PDF.
- `unparseableEditions` — the PDF arrived but failed the integrity floor.
- `editionsWithNoEntries` — the edition parsed cleanly and genuinely published nothing.

plus `editionsAddressable`, `editionsRead` (with page and byte counts per edition), `delivered`,
`charged`, and the measured address and director coverage of what was actually delivered.

### How the integrity floor works

A short parse looks exactly like a quiet week. The smallest real edition in the sample carried **11
entries** and was perfectly healthy. So the check is never on the entry count — it is on the shape of
the document:

1. the number of pages extracted must reach the `number_of_pages` gov.uk declares for that file;
2. the cover page must parse — *Office of the Traffic Commissioner* heading plus a publication number;
3. at least 8 section headings must be present (live editions carry 15–18);
4. at least 300 characters per page (live minimum: 1,653);
5. at least 90% of the licence-reference lines seen must have become entries (live minimum: 97.0%).

An edition that fails any of these is recorded as unparseable, not reported as a quiet week. If
every attempted edition fails — or more than a quarter of them in a larger run — the whole run
fails rather than reporting a partial read as complete.

### Notes for developers

The PDF text extractor in `src/pdf.js` is **dependency-free**: no pdf.js, no pdfplumber, no OCR. It
handles PDF 1.6 cross-reference streams, compressed object streams (every font and page dictionary
in these files lives inside one — a naive `N 0 obj` scan finds 77 objects of 1,449 and no fonts at
all), FlateDecode with PNG predictors, TrueType fonts with `/ToUnicode` CMaps, and the per-glyph
kerned `TJ` arrays these files use, where word breaks are decided from measured advance widths
rather than from the presence of a kern.

That is why `offline_validate.mjs` can run the whole pipeline — real PDF bytes in, real rows out —
on `node offline_validate.mjs` with no network and no `node_modules`.

```
node offline_validate.mjs
  261 passed, 0 failed
```

# Actor input Schema

## `regions` (type: `array`):

Leave empty for all eight. The Actor reads the live gov.uk collection page to confirm the list before it runs.

## `maxEditionsPerRegion` (type: `integer`):

How many of the most recent weekly editions to read per area. 1 (the default) gives you this week across the areas you chose - measured mean 72.5 licence entries per edition, so all eight areas is about 580 rows. 0 means every edition still addressable, which is 37 per area.

## `publishedFrom` (type: `string`):

Optional ISO date, e.g. 2026-09-01. Filters editions by their publication date BEFORE anything is downloaded, so a narrow window costs nothing to skip. Measured oldest addressable edition on 2026-09-17: 2026-01-01.

## `publishedTo` (type: `string`):

Optional ISO date, e.g. 2026-09-16.

## `sectionGroups` (type: `array`):

Leave empty for every section. The default keeps the two application groups, which is what a dealer, broker or compliance consultancy is buying.

## `entryTypes` (type: `array`):

A finer filter than section groups, applied on top of them. Leave empty to keep whatever the section groups let through.

## `licenceClasses` (type: `array`):

Measured: 98.7% of entries carry a class. The 1.3% that do not are public-inquiry entries where the booklet omits it; selecting any class here excludes them.

## `postcodePrefixes` (type: `array`):

Match the operator's correspondence postcode OR any operating-centre postcode. "G" = Glasgow, "EH" = Edinburgh, "ML4" = one district. Spaces and case are ignored; multiple entries are OR-ed. Measured: 96.4% of entries carry a postcode.

## `minVehicles` (type: `integer`):

Keep only entries whose vehicle authorisation is at least this. Measured: 59.0% of entries state a vehicle count - a missing count is reported as null, never as 0, so setting this filter excludes the entries where the booklet does not say.

## `keyword` (type: `string`):

Case-insensitive substring match across operator name, directors, correspondence address, operating centres and transport managers.

## `requireAddress` (type: `boolean`):

Filter out the 3.6% of entries the booklet prints without a full address (mostly refusals and withdrawals that list the operator and its directors only), so you never pay for a row you cannot post to.

## `oneRowPerLicence` (type: `boolean`):

OFF (default): one row per published entry, which is the source's own grain - measured 1.2% of entries within one edition repeat a licence number because it appears in two sections. ON: the first entry for each licence number in each traffic area, everything after it dropped and not charged.

## `maxApplications` (type: `integer`):

Hard cap on rows delivered, and therefore on rows charged, counted against rows PRODUCED rather than rows flushed. Editions are read newest first in batches, so a small cap also stops the download early. 0 means no cap.

## `proxyConfiguration` (type: `object`):

Optional. Measured 2026-09-17: gov.uk and assets.publishing.service.gov.uk have no anti-bot, no user-agent gate and no rate limit on this path - 16 PDFs fetched in parallel returned 16x HTTP 200 in 1.36 s with no truncation - so the default datacenter proxy is plenty. Every PDF's byte count is checked against the size gov.uk declares and re-fetched on a fresh session if it falls short, so a half-delivered download can never be parsed as a short week.

## Actor input object example

```json
{
  "regions": [
    "scotland"
  ],
  "maxEditionsPerRegion": 1,
  "sectionGroups": [
    "applications-received"
  ],
  "entryTypes": [],
  "licenceClasses": [],
  "postcodePrefixes": [],
  "requireAddress": false,
  "oneRowPerLicence": false,
  "maxApplications": 25,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per published licence entry: licence number and class, operator, named directors or partners, correspondence address with postcode, every operating centre named in the entry, vehicles and trailers authorised, transport managers, objection deadline, and a link to the page of the source PDF it came from. There is no email or phone column - the Office of the Traffic Commissioner publishes neither.

## `runSummary` (type: `string`):

RUN\_SUMMARY: which weekly editions were addressable, which were read (with page and byte counts), which could not be downloaded, which failed the integrity floor, which genuinely published nothing, how many rows were delivered and charged, and the measured address and director coverage of what was delivered.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "regions": [
        "scotland"
    ],
    "maxEditionsPerRegion": 1,
    "sectionGroups": [
        "applications-received"
    ],
    "maxApplications": 25
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/traffic-commissioner-applications-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "regions": ["scotland"],
    "maxEditionsPerRegion": 1,
    "sectionGroups": ["applications-received"],
    "maxApplications": 25,
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/traffic-commissioner-applications-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "regions": [
    "scotland"
  ],
  "maxEditionsPerRegion": 1,
  "sectionGroups": [
    "applications-received"
  ],
  "maxApplications": 25
}' |
apify call scrapersdelight/traffic-commissioner-applications-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapersdelight/traffic-commissioner-applications-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/vbrpb4qSbjZv5PdiV/builds/MgemqcPD7PnjJ1pnK/openapi.json
