# UK Trading Disclosure Scraper - Company No, VAT & Contact (`scrapersdelight/uk-trading-disclosures-contact-scraper`) Actor

Turn UK company domains into registry-grade B2B leads from each site's statutory trading disclosure (Companies Act 2006 s.82): registered name, Companies House number, jurisdiction, registered office, mod-97-checked VAT number, charity and ICO numbers, regulator, email, phone. $0.006 per disclosure.

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

## Pricing

$6.00 / 1,000 per trading disclosure 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 Trading Disclosure Scraper — Company Number, VAT, Registered Office & Contacts

Turn a plain list of **UK company domains** into registry-grade B2B leads, read straight from the
**statutory trading disclosure** every UK company is legally required to publish on its own website.

Paste domains. Get back the **registered name**, the **Companies House number**, the **part of the UK
it is registered in**, the **registered office address**, a **checksum-validated VAT number**, plus
email, phone, charity number, ICO registration and regulator — one flat row per company.

***

### ⚖️ Why this data exists, and why it is clean

**Companies Act 2006 s.82** and **The Company, Limited Liability Partnership and Business (Names and
Trading Disclosures) Regulations 2015 (SI 2015/17), reg. 25** require every UK company and LLP to
disclose, **on its websites**:

- its **registered name**
- the **part of the United Kingdom** in which it is registered
- its **registered number**
- the **address of its registered office**

Regulations 24–25 extend the name and address-for-service disclosure to unincorporated businesses
trading under a name that is not the proprietor's own.

So this is not scraped inference and it is not a private profile. **It is a statement the business is
legally obliged to publish about itself**, in public, on its own site. That is what makes it the
cleanest UK B2B starting point there is — and the same shape that already works in Germany
(Impressum), France (mentions légales) and Spain (aviso legal).

> **The corpus is the whole UK commercial web**, not one directory with a member list. If a UK company
> has a website, it is in scope.

***

### 🔍 What it does, per domain

1. **Fetches the homepage and reads its footer.** On the UK corpus the disclosure is often right
   there — measured over 598 real domains, the homepage footer carried it for **53.4% of all
   delivered rows** (14.9% homepage-only, 38.5% homepage plus a second page). That is the opposite
   of the German/French pattern, where the legal notice is always its own page.
2. **If fields are still missing, ranks the site's own links** and fetches the best ones —
   terms & conditions > legal > company information > privacy policy > about > contact — then
   **merges field by field**, because UK sites routinely split the disclosure across a terms page
   (company number, registered office) and a contact page (email, phone).
3. Falls back to the **XML sitemap**, the **WordPress REST page index** and a list of
   **conventional paths** when a site publishes no footer link at all.
4. **Validates what it found**: the GB VAT number against the published **mod-97 checksum** (both
   variants), the company number against the Companies House format and prefix table, and derives the
   jurisdiction from the prefix.

No browser. Plain HTTP + cheerio, on the Apify datacenter proxy, with a residential retry only when a
host actually refuses us.

***

### 📦 What you get — every field

One flat row per company. `null` means the company did not publish it; nothing is ever invented.

#### Identity

| Field | What it holds |
|---|---|
| `registeredName` | The registered company name as the disclosure states it (`Arrow Fresh Ltd`) |
| `tradingName` | The "trading as" / `t/a` name where one is declared |
| `companyName` | Best available name — the registered name, else JSON-LD `legalName`, else the site name |
| `legalForm` | `Private limited company`, `PLC`, `LLP`, `CIC`, `CIO`, `Company limited by guarantee`, `Limited partnership`, `Registered society`, `Sole trader` |
| `entityKind` | What the number's prefix implies: `LLP`, `Limited partnership`, `Overseas company`, `Registered society`, `Charitable incorporated organisation`, `Royal charter`… |

#### Registry

| Field | What it holds |
|---|---|
| `companyNumber` | The Companies House number, **normalised to its canonical 8-character form** — `00032762`, `SC216217`, `OC301234`, `R1234567` |
| `companyNumberRaw` | Exactly as the page printed it (`SC 216217`), for audit |
| `companyNumberSource` | Which statement shape it came from: `label-alt`, `jurisdiction`, `jurisdiction-label`, `label`, `bare` |
| `companyNumberFormatValid` | Format + known-prefix check. **Companies House numbers carry no checksum** — see Honest limits |
| `companyNumberScheme` | `GB-COH` |
| `jurisdiction` | `England and Wales`, `Scotland`, `Northern Ireland` or `Overseas`, from the prefix and the wording |
| `jurisdictionStated` | The words the page actually used, verbatim |
| `registerUrl` | A **direct deep link into the public Companies House register** for that number |
| `registeredOffice` | The registered office address line |
| `registeredOfficePostcode` / `registeredOfficeCity` | Parsed out of that address block, never by a page-wide postcode scan |
| `vatNumber` | Normalised VAT id (`GB372138852`), including the 12-digit branch form and the checksum-free `GBGD###` / `GBHA###` government forms |
| `vatNumberValid` | **The mod-97 checksum result**, `true` / `false` / `null` |
| `vatChecksumVariant` | Which published algorithm it satisfied: `mod-97`, `mod-97-55`, `government-department`, `health-authority`, or `non-GB (no GB checksum applies)` |
| `vatCountry` / `vatBranch` | Country code and the 3-digit branch identifier when present |
| `charityNumber` | Registered charity number, **in its own field** — a Scottish charity number looks exactly like a Scottish company number |
| `icoRegistration` | ICO data-protection registration (`ZA######`) where published |
| `regulator` / `regulatorNumber` | FCA, SRA, RICS, GDC, GMC, CQC, GPhC, Ofsted, Gambling Commission, ATOL, Gas Safe, NICEIC, ICAEW, ACCA and others, plus the reference number where published |

#### Contact

| Field | What it holds |
|---|---|
| `email` / `emails` | Contact addresses, **including Cloudflare-obfuscated ones** (see below). The company's own domain wins the primary slot |
| `emailConflict` | Set when the primary email is on somebody else's domain and is not a free mailbox — flagged, never silently dropped |
| `phone` / `phones` | Normalised to E.164 (`+441312431453`). Ofcom's reserved drama range and placeholder runs are rejected |
| `addressLine` / `postcode` / `city` | The trading/contact address, which is often **not** the registered office (an accountant's address is a very common registered office) |
| `officerName` / `officerRole` | A named director / proprietor / company secretary where the page labels one |
| `socialLinks` | LinkedIn, Facebook, Instagram, X, YouTube, TikTok |
| `termsUrl` / `privacyPolicyUrl` | The policy pages linked from the site |

#### Provenance & accounting

| Field | What it holds |
|---|---|
| `disclosureUrl` | The page the disclosure was read from |
| `disclosureSource` | `homepage`, `legal-page` or `merged` |
| `discoveryChannel` | `homepage`, `anchor`, `sitemap`, `wpjson`, `guess`, `jsassets`, `input` |
| `pagesParsed` / `pagesChecked` | How many pages were parsed, and how many URLs were tried |
| `fieldSources` | Optional: the exact URL **each individual field** came from |
| `fieldCount` | How many of the 19 value fields are populated |
| `stableId` | The company number when well-formed, else the registrable domain |
| `status` / `missReason` | `ok`, or why nothing was returned |
| `elapsedMs` / `fetchedAt` | Per-row timing and timestamp |

***

### 📊 Measured results — real numbers, not claims

Everything below is from **one real run on the Apify platform** over **598 UK business domains**,
sourced from OpenStreetMap across **18 UK cities** (London, Manchester, Birmingham, Leeds, Glasgow,
Edinburgh, Bristol, Cardiff, Belfast, Newcastle, Liverpool, Sheffield, Nottingham, Brighton,
Aberdeen, Norwich, Reading, Southampton) and picked **without any reference to whether they publish
a disclosure**. Run `wRVb4PWXuKSdMv4sL`, 1,085 s, SUCCEEDED.

```
598 domains attempted
  148 billed rows        24.7% of all domains · 32.7% of the 453 REACHABLE domains
  303 publish no disclosure   never charged
   96 dead hosts (stale website entries)  never charged
   49 refused / challenged                never charged
    2 duplicates collapsed before a request  never charged
```

**Delivered rows == `chargedEventCounts` exactly: 148 and 148.**

**Per-field fill, measured on the 148 billed rows:**

| Field | Fill | | Field | Fill |
|---|---|---|---|---|
| `email` | **88.5%** | | `registeredOfficePostcode` | **66.9%** |
| `companyNumber` | **85.8%** | | `addressLine` / `postcode` | **62.2%** |
| `jurisdiction` | **85.8%** | | `registeredOfficeCity` | **61.5%** |
| `registeredName` | **75.0%** | | `city` | **57.4%** |
| `legalForm` | **75.0%** | | `vatNumber` | **38.5%** |
| `registeredOffice` | **71.6%** | | `regulator` | **20.3%** |
| `phone` | **70.3%** | | `charityNumber` / `tradingName` | **10.8%** |
| `regulatorNumber` | **7.4%** | | `icoRegistration` | **5.4%** · `officerName` **2.0%** |

**VAT checksum: 57 VAT numbers found, 55 pass the GB mod-97 check — 96.5%.** That is the number to
look at. A parser that grabs the wrong nine digits produces checksum failures, so a high pass rate
is evidence the extraction is right, not merely that a column is populated.

**Where the disclosure was found:** the homepage footer carried it for **79 of 148 rows (53.4%)** —
22 homepage-only, 57 homepage plus a second page — and a dedicated legal/terms page was needed for
the other **69 (46.6%)**. By discovery channel: ranked footer links 74, homepage footer 71, XML
sitemap 2, guessed paths 1.

**Transport:** 3,822 requests for 598 domains = **6.4 per domain**; 2,022 OK, 769 fast 404s on the
guessed-path channel, 187 refusals, 105 dead hosts, 104 parked stubs. Residential escalation fired
128 times and recovered 26. Peak memory 691 MB in a 1,024 MB container.

**Speed:** 598 domains in **18 minutes** at concurrency 12. The six-domain demo run finishes in
9–20 seconds.

#### Second run, country-wide corpus

A confirmation run on a different corpus and the current build: **400 domains** drawn country-wide
from OpenStreetMap (`office` + `shop` + `craft`, no high-street `amenity` bias), run
`TbGcBtEMY848MicQk`, 687 s, SUCCEEDED.

```
400 domains -> 110 billed   27.5% of all · 36.7% of the 300 reachable
delivered == charged: 110 and 110
NOT charged: 190 publish no disclosure · 71 dead hosts · 29 refused
fill: email 88.2 · jurisdiction 88.2 · companyNumber 87.3 · phone 80.9 · registeredName 71.8
      legalForm 70.9 · addressLine/postcode 65.5 · registeredOffice 61.8 · vatNumber 41.8
VAT: 46 found, 45 pass mod-97 = 97.8%
```

The numbers hold across two independent corpora, and `vatNumber` fill rose from 38.5% to 41.8%
after the under-collection fix described in the FAQ below.

#### Which lists convert — yield by business type

The same 598-domain run, split by the OpenStreetMap key each domain came from. This is the single
most useful number for planning a job, and it varies by more than 2x:

| Domains came from | Attempted | Billed | Yield of reachable |
|---|---|---|---|
| `office` (agencies, professional services, B2B) | 87 | 35 | **53.8%** |
| `shop` (retail) | 167 | 40 | 32.8% |
| `tourism` | 29 | 7 | 33.3% |
| `craft` (trades) | 14 | 3 | 25.0% |
| `amenity` (cafés, pubs, restaurants, banks) | 283 | 56 | 25.3% |
| **all** | **598** | **148** | **32.7%** |

A café is usually a sole trader or a partnership trading under the owner's own name, and reg. 25's
company disclosure does not bind one — there is no company number for it to publish. A professional
services firm is almost always a limited company or an LLP. **Feed this Actor a B2B list and the hit
rate roughly doubles.**

#### A real row, from that run

Not a mock-up — `rox.co.uk`, exactly as delivered (long arrays trimmed):

```json
{
  "domain": "rox.co.uk",
  "resolvedUrl": "https://www.rox.co.uk/",
  "disclosureUrl": "https://www.rox.co.uk/contactus",
  "disclosureSource": "legal-page",
  "discoveryChannel": "anchor",
  "pagesParsed": 2,
  "pagesChecked": 3,
  "registeredName": "ROX (UK) LTD",
  "registeredNameSource": "suffix",
  "companyName": "ROX (UK) Ltd",
  "legalForm": "Private limited company",
  "companyNumber": "SC216217",
  "companyNumberRaw": "SC 216217",
  "companyNumberSource": "jurisdiction",
  "companyNumberFormatValid": true,
  "companyNumberScheme": "GB-COH",
  "jurisdiction": "Scotland",
  "jurisdictionStated": "Scotland",
  "registerUrl": "https://find-and-update.company-information.service.gov.uk/company/SC216217",
  "registeredOffice": "ROX (UK) LTD, 42-43 Argyll Arcade, Glasgow, G2 8BG",
  "registeredOfficePostcode": "G2 8BG",
  "registeredOfficeCity": "Glasgow",
  "email": "help@rox.co.uk",
  "emails": ["help@rox.co.uk", "marketing@rox.co.uk"],
  "phone": "+448081646448",
  "socialLinks": ["https://www.instagram.com/ROXJewellery", "https://www.tiktok.com/@roxjewellery"],
  "country": "United Kingdom",
  "status": "ok",
  "fieldCount": 12,
  "elapsedMs": 4455
}
```

Note `companyNumberRaw: "SC 216217"` — the page prints the prefix with a space, and the canonical
`SC216217` is what goes to Companies House. That is the kind of detail a digits-only regex loses.

***

### 🧰 Every input

| Input | What it does |
|---|---|
| **UK domains** | Bare domains, homepage URLs or direct legal-page URLs. **Any TLD** |
| **Start URLs** | The same list as `requestListSources`, so Make / Zapier / Clay / Sheets can pass it natively |
| **Enrich an existing dataset** | Dataset ID of a previous run — the real agency workflow |
| **Domain list file URL** | A CSV / TXT / JSON / JSONL file, for lists too big to paste |
| **Suppression list** / **previous dataset** | Never re-deliver, and never re-charge for, a company you already bought |
| **Max results (billed rows)** | Hard cap on rows delivered AND billed |
| **Max domains attempted** | Sample a big list cheaply |
| **Max discovery requests / pages parsed per domain** | The two cost levers |
| **Discovery channels** | Turn off sitemap / wp-json / path-guess to make a big run much cheaper |
| **Proxy, proxy country, residential escalation, Unblocker escalation** | The full transport ladder |
| **TLD filter** | Off by default, deliberately — see below |
| **Only rows with a registry ID / valid VAT / a contact** | Quality gates. Filtered rows are never charged |
| **Jurisdictions** | Keep only England & Wales, Scotland, Northern Ireland or overseas-registered companies |
| **Minimum populated fields** | A hard quality floor |
| **Email policy** | Keep all, role addresses only (`info@`, `enquiries@`), or named mailboxes only |
| **Deduplicate by** | Company number, else domain (recommended) — two domains owned by one company collapse to one billed row |
| **Field extraction toggles** | VAT checksum, regulator, ICO, charity, officer, socials, policy URLs |
| **Include unbilled miss rows** | Coverage accounting: every domain that produced nothing, with the reason |
| **Include per-field provenance** | Which URL each field came from |
| **Flat columns** | Flat CSV-ready row, or grouped `company{} registry{} contact{} people{} policies{}` |

Example:

```json
{
  "domains": ["rox.co.uk", "batchd.co.uk", "nq64.co.uk", "alzscot.org"],
  "maxItems": 1000,
  "requireRegistryId": true,
  "dedupeBy": "company-number-then-domain"
}
```

***

### 💷 Pricing

**Pay per result — $0.006 per trading disclosure returned.**

| You are charged | You are NOT charged |
|---|---|
| Each UK company whose disclosure was found and parsed into a lead carrying at least one registry identifier | A host that does not resolve |
| | A host that refused us (403 / challenge) |
| | A domain that publishes **no** trading disclosure |
| | A duplicate collapsed by your dedupe key |
| | A row your own filters removed |
| | An unbilled coverage row (`includeMissRows`) |

Delivery and billing are **atomic** (`Actor.pushData(record, 'disclosure-scraped')`), so if you hit a
charge cap you can never keep a row you were not billed for, or be billed for a row you did not get.
On the 598-domain run above, **delivered rows and `chargedEventCounts` matched exactly: 148 and 148.**
There is **no start fee** and there are **no stacked auto-events** — one event, one price.

For context, measured: **148 billed rows from 598 cold UK domains**, so a 1,000-domain list
typically costs around **$1.50**, not $6.

***

### ❓ FAQ

**Is this legal to collect?**
The fields come from a disclosure the company is *legally required by the Companies Act* to publish on
its own website. That is about as public as company data gets. You are still responsible for the
target site's Terms of Service and for how you use the output — see Legal, below.

**Does it work on `.com` domains?**
Yes, and this matters more than it sounds. Measured on a **country-wide** OpenStreetMap pull of
every UK `office`, `shop` and `craft` with a website — **46,863 unique business hosts** — only
**66.3% are `.uk` of any kind**. **13,491 are `.com`**, plus `.org`, `.net`, `.co`, `.scot`,
`.london` and more. Filtering the corpus to `.uk` would silently discard **about a third of it**,
skewed toward the larger companies. There is no TLD filter by default; it is an opt-in input.

**Does it handle Scottish, Northern Irish and LLP numbers?**
Yes. `SC` (Scotland), `NI` (Northern Ireland), `OC` / `SO` / `NC` (LLPs), `LP` / `SL` / `NL` (limited
partnerships), `FC` / `SF` / `NF` (overseas), `R` + 7 digits (pre-partition Northern Ireland), plus the
society, assurance, protected-cell, CIO, Royal-charter and government ranges — and bare 8-digit England
& Wales numbers. A digits-only regex under-collects badly: several disclosures in the first sample
were `SC`-prefixed, and one real company publishes a **five-digit** number (`32762`, Companies House
`00032762`) which a `\d{6,8}` regex drops entirely.

**Is the VAT number actually checked, or just extracted?**
Checked. Every GB VAT number is run through the published **mod-97 checksum**, in both the standard
and the "97-55" variant, and the result and the variant are in the row. The 12-digit branch form
validates on its first nine digits; the `GBGD###` and `GBHA###` government forms have no checksum and
are validated on their published ranges instead.

**Why isn't the company number checksum-validated too?**
Because **Companies House numbers do not have a checksum**. `companyNumberFormatValid` is a format and
known-prefix check and the field is named accordingly. Any actor claiming to "validate" a UK company
number without a register call is claiming something that does not exist.

**Does it find emails hidden behind Cloudflare?**
Yes. Cloudflare rewrites `mailto:` links on proxied pages into `/cdn-cgi/l/email-protection#<hex>` and
renders the literal text `[email protected]`. This Actor decodes them. Without that, a large slice of
UK SME sites ship with `email: null` while the address sits right there in the bytes.

**What about the charity number — Scottish charity numbers look like company numbers?**
They do, and that is a real trap: `alzscot.org` is company `SC149069` **and** Scottish charity
`SC022315`, one line apart. They are parsed into separate fields with a nearest-label rule, so the
charity number never ends up in `companyNumber`.

**Why do so many domains return nothing?**
Three different reasons, kept apart in `RUN_SUMMARY` and **none of them charged**. On the 598-domain
run: **96 dead hosts** (16% — any OSM-sourced list carries stale website tags), **49 refused**, and
**303 that publish no disclosure**. That last group is the biggest and it is mostly genuine: sole
traders and partnerships trading under the owner's own name have no company number to publish, and a
long tail of micro-businesses is simply non-compliant with reg. 25.

**How do you know the "publishes no disclosure" ones really publish nothing?**
Because they were re-probed. 60 of those 303 were run again through a deliberately extravagant
cascade — 30 requests, 10 pages, 26 guessed paths, sitemap, WordPress index, JS bundles and the
Unblocker escalation switched on. **4 of the 60 (6.7%) changed their mind**; the other 56 genuinely
publish nothing findable. So the default settings leave roughly 7% of that bucket on the table, on
purpose, because recovering it costs about four times the requests. If you want them, raise
**Max discovery requests per domain** and **Max pages parsed**, and switch on the Unblocker
escalation — the levers are all inputs, and the rows you do not get are not charged either way.

**Can I get only the companies with a VAT number / only Scottish companies / only ones with an email?**
Yes — `requireValidVat`, `jurisdictionFilter` and `requireContact`. Filtered rows are never charged, so
these are cost controls as well as quality controls.

**Can I run it monthly without paying twice for the same company?**
Yes. Pass the previous run's dataset ID as `previousDatasetId`; every id it already delivered is
suppressed before a single request is made.

**How do you know the parser is not just missing fields that are there?**
Because it was tested in the negative direction, which ordinary testing cannot do. 144 of the
delivered pages were re-fetched and re-scanned with a **separately written** implementation of the
GB mod-97 checksum — a 9-weight dot product, where the parser uses a 7-weight sum compared against
the check digits, so the two cannot share a bug. Results: **0 disagreements** on every VAT number
emitted, **0 non-deterministic re-parses**, and **4 values the parser had missed** out of 144 pages.
Three of the four were the same fixable bug (the VAT label separated from its number by a clause —
`"The VAT number of 200 Degrees is: 284361686"`); that is fixed, and those three strings are now
regression assertions. **The sweep was then repeated on a fresh 400-domain run with the fix in
place: 108 pages re-read, 0 VAT numbers missed, 0 company numbers missed, 0 checksum disagreements,
0 non-deterministic re-parses.**

The same sweep measured **conditional** fill — of the pages that actually carry a company-number
label, how many yielded a number:

| Jurisdiction | Pages with a label | Number emitted | Conditional yield |
|---|---|---|---|
| England and Wales | 102 | 102 | **100.0%** |
| Scotland | 15 | 15 | **100.0%** |
| Northern Ireland | 1 | 1 | 100.0% |

That is the number that tells you whether a parser works. The headline `companyNumber` fill of 85.8%
is **not** a parser gap: where a company number is published, it is read, in every jurisdiction. The
rest of the rows qualified on a VAT or charity number instead, because their page carries no company
number at all.

**How do I audit a field I do not trust?**
Turn on **Include per-field provenance**. Every field then carries the exact URL it was read from, so
you can open the page and check it. `disclosureUrl`, `companyNumberRaw` and `jurisdictionStated` are
there for the same reason.

**Does it respect robots.txt?**
There is a toggle, off by default. Measured on a UK sample: of the domains that served a parseable
`robots.txt` at all, **0 disallowed any of `/`, `/terms`, `/terms-and-conditions`, `/legal`,
`/privacy-policy`, `/about-us`, `/contact`** — these are pages sites want indexed. Turn it on if your
own compliance policy requires it; it costs one extra request per domain.

**Will it break on Cloudflare-protected sites?**
Some. The Actor retries a genuine refusal once on a **residential GB** IP — measured on the
598-domain run: 128 escalations, 26 recovered. There is an opt-in second escalation to **Apify
Unblocker**, which recovered **4 of the 5** hardest refusals in ladder testing, where both the
datacenter and the residential IP got a Cloudflare managed 403 (those same hosts also serve a plain
home broadband IP a 202 + 170-byte JS challenge, so the wall is real and is not the Apify proxy).
Blocked hosts are reported as `blocked` with the reason, and are never charged.

**What if I already have a domain list in a Google Sheet / Clay / a previous Actor?**
Use `startUrls` (requestListSources), `domainsFileUrl` (a CSV/TXT/JSON URL) or `sourceDatasetId`
(another Actor's output). They all merge and deduplicate.

***

### ⚠️ Honest limits

- **This is an enrichment tool, not a discovery tool.** It does not find UK companies for you; it
  turns the domain list you supply into company records.
- **It reads what the site publishes, nothing more.** It does not call the Companies House API, so it
  cannot tell you whether a company is still active, who its directors are, or whether the number the
  site prints is genuinely theirs. `registerUrl` is provided so you can check in one click.
- **Companies House numbers have no checksum** (repeated because it matters): `companyNumberFormatValid`
  is a format and prefix check.
- **A named officer is rare — about 1.4% fill.** UK trading-disclosure rules, unlike the German
  Impressum, do not require naming a director on the website. The field is extracted where a page
  labels one and is honestly empty the rest of the time.
- **ICO registration is rare too — about 4.5%.** Kept because when it is there it is unambiguous.
- **`registeredOffice` is often an accountant's address.** That is what the company filed; it is the
  registered office, not necessarily where they work. `addressLine` is the trading address when the
  site publishes a different one.
- **About 1 domain in 4 on a cold list yields a row** (148 of 598 measured; 32.7% of the domains
  that were actually reachable). The three kinds of nothing are itemised in `RUN_SUMMARY` and none
  are charged. If you need a higher hit rate, feed it a list of *limited companies* rather than a
  general business list — sole traders have no company number to publish.
- **A `.com` in a UK list is sometimes not a UK entity.** One sampled site was a Netherlands operator
  with a KvK number and an NL VAT id; the row records `vatCountry: "NL"` honestly rather than forcing
  it into a GB field.

***

### 🧾 Legal & fair use

This Actor reads **statutory public disclosures** — the trading-disclosure statement that
Companies Act 2006 s.82 and the Names and Trading Disclosures Regulations 2015 reg. 25 require UK
companies to publish on their own websites, together with the contact details published alongside them.

- You are responsible for complying with each target site's **Terms of Service**.
- Output may contain **personal data** (a named director, a personal email address). Handling it is
  your responsibility under **UK GDPR / the Data Protection Act 2018**, and any outreach is yours to
  make compliant with **PECR** and the Privacy and Electronic Communications Regulations.
- This is **not** a credit, risk or due-diligence product. It reports what a company published; it does
  not assess that company.
- No login, no paywall and no credential is used or required at any point.

***

*Part of a family of statutory-disclosure scrapers: DACH Impressum, French mentions légales, Spanish
aviso legal — and now the UK.*

# Actor input Schema

## `domains` (type: `array`):

One entry per company. Accepts all three shapes: a bare domain ("rox.co.uk"), a homepage URL ("https://www.nq64.co.uk") or a direct legal/terms page URL ("https://x.co.uk/terms-and-conditions"). ANY TLD is accepted — a third of real UK business domains are .com, and filtering to .uk would silently drop them. The Actor finds each site's statutory trading disclosure and parses it into one lead per domain. Leave empty to run the built-in UK demo batch.

## `startUrls` (type: `array`):

The same domain list handed over as URLs, so Make, Zapier, Clay or a Google Sheet can pass it natively (a link to a text/CSV file of URLs also works). Merged with "UK domains".

## `sourceDatasetId` (type: `string`):

Dataset ID of a previous Actor run. Each item's domain/website column is read and enriched — the real agency workflow: run a directory, Maps or Companies House scraper first, then pipe its output here.

## `domainFieldName` (type: `string`):

Which field of the source dataset (or which CSV column of the list file) holds the domain. Leave empty to auto-detect domain / website / websiteUrl / url / site / homepage. Dotted paths like "company.website" work.

## `domainsFileUrl` (type: `string`):

URL of a CSV, TXT, JSON or JSONL file holding the domains — for lists too big to paste into the editor. The column is picked with "Domain field / CSV column".

## `skipDomains` (type: `array`):

Domains to skip outright — accounts you already own, competitors, do-not-contact entries. Matched on the registrable domain, so "www.x.co.uk/page" and "x.co.uk" are the same entry.

## `previousDatasetId` (type: `string`):

Dataset ID of an earlier run of THIS Actor. Its stableId / companyNumber / domain values are loaded as a suppression list, so a monthly re-run never re-delivers — and never re-charges you for — a company you already bought.

## `maxItems` (type: `integer`):

Hard cap on rows DELIVERED AND BILLED this run. Distinct from "Max domains": measured on 896 random real UK business domains, about 1 domain in 4 published a findable statutory disclosure, so 1,000 domains yield roughly 250 billed rows. 0 = unlimited.

## `maxDomains` (type: `integer`):

Cap on domains ATTEMPTED, applied before any request. Use it to sample a big list cheaply. 0 = attempt every domain supplied.

## `maxDiscoveryRequestsPerDomain` (type: `integer`):

Hard cap on discovery + confirmation requests per domain, counted AFTER the homepage. This is the knob that stops one slow host burning a minute of a run. Measured average across the whole cascade: 6.5 requests per domain.

## `maxPagesParsed` (type: `integer`):

How many fetched pages may be parsed and merged for one domain. The homepage counts as one. Raising it finds more fields on sites that split the disclosure across terms/contact/privacy pages; lowering it to 1 makes the run homepage-only and very cheap.

## `perDomainTimeoutSecs` (type: `integer`):

Wall-clock deadline for one domain, discovery included.

## `requestConcurrency` (type: `integer`):

How many domains are worked in parallel. Higher is faster; keep it modest to stay polite to small business sites, and remember memory scales with it.

## `requestTimeoutSecs` (type: `integer`):

Timeout for a single HTTP request.

## `maxRequestRetries` (type: `integer`):

Retries per request, each on a FRESH proxy IP (got-scraping's own retry reuses the flagged IP, which is useless against a soft block).

## `discoveryChannels` (type: `array`):

Which channels may be used to locate the disclosure, tried in this order. MEASURED over 598 real UK domains on the platform, counting which channel found the winning page: ranked footer links 74, homepage footer 71, XML sitemap 2, guessed paths 1. The homepage footer is free (that page is already fetched). Guessed paths cost 769 of the run's 3,822 requests (fast 404s) and won once — keep them for sites whose footer carries no legal link at all, or switch them off as a speed lever on a big list.

## `followWwwAndRootVariants` (type: `boolean`):

If the homepage fails, retry the other host form (www.x.co.uk to x.co.uk and back) before declaring the domain unreachable.

## `deepJsDiscovery` (type: `boolean`):

Last-resort channel for a footer that only exists after client-side render. No Chromium is launched — this build is deliberately browser-free, because a browser image would multiply the compute cost of every run. Instead the Actor reads what a render would have read FROM: the page's inline JSON payloads and its external JavaScript bundles, searching them for a legal-page URL. Costs up to 6 extra requests per domain and is never charged separately.

## `respectRobotsTxt` (type: `boolean`):

Fetch and honour each site's robots.txt before requesting anything. Off by default because it costs one extra request per domain and these pages exist to be indexed. Turn it on if your own compliance policy requires it.

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

Proxy settings. Apify DATACENTER is the default and is the measured winner on this corpus: usable transport on the large majority of reachable UK domains, with a residential retry only when a host actually challenges us.

## `proxyCountry` (type: `string`):

Pin the exit country for sites that geo-tailor their content. MEASURED: Apify datacenter proxies cannot be pinned to a country (they answer HTTP 407), so choosing a country here switches the run to RESIDENTIAL exit nodes in that country. Leave on None for the cheaper, faster datacenter path.

## `escalateToResidentialOnBlock` (type: `boolean`):

On a 403 / challenge (never on a dead host and never on a 404), retry the request once on RESIDENTIAL + country-GB. Measured: recovers a real slice of the CDN-fronted tail at no cost on a clean SME list, because it only fires on an actual refusal.

## `escalateToUnblockerOnBlock` (type: `boolean`):

A SECOND escalation, after residential, for the Cloudflare-managed-challenge tail. MEASURED on the domains that both datacenter and residential were refused by: Unblocker recovered 4 of 5 (the same hosts also serve the operator's own home IP a 202 challenge, so the wall is real and is not the Apify proxy). Off by default because Unblocker requests are billed to your Apify account on top of the row price.

## `customUserAgent` (type: `string`):

Override the browser User-Agent sent on every request. Leave empty for the built-in Chrome 124 fingerprint.

## `extraHttpHeaders` (type: `object`):

Additional request headers, merged over the defaults (e.g. a From: header identifying your crawler).

## `tldFilterMode` (type: `string`):

No TLD filter is applied by default, deliberately: of 14,041 real UK business domains sourced from OpenStreetMap across 18 UK cities, only 8,398 are .uk of any kind and 4,614 are .com. Filtering to .uk would discard 40% of the corpus. Use this only when your own list really is single-TLD.

## `tldFilter` (type: `array`):

The TLD list the mode above applies to, without the dot: uk, co.uk, com, org.uk, scot, wales.

## `requireRegistryId` (type: `boolean`):

Deliver (and bill) only companies whose disclosure carries a Companies House number, a VAT number or a charity number.

## `requireValidVat` (type: `boolean`):

Deliver (and bill) only rows whose GB VAT number passes the mod-97 checksum. A row with no VAT number, or one that fails the checksum, is filtered out here and never billed.

## `requireContact` (type: `boolean`):

Deliver (and bill) only companies with an email or a phone number.

## `jurisdictionFilter` (type: `array`):

Keep only companies registered in these parts of the UK. Derived from the Companies House number prefix (SC = Scotland, NI = Northern Ireland, no prefix = England and Wales) and from the wording of the disclosure. Leave empty for all.

## `minFieldsRequired` (type: `integer`):

Quality floor: a row must carry at least this many of the 19 value fields before it is delivered and billed. 0 = no floor.

## `emailPolicy` (type: `string`):

Agencies split hard on whether info@ / enquiries@ counts as a lead. "Role only" keeps just those; "Exclude role" keeps only named mailboxes.

## `dedupeBy` (type: `string`):

Which key collapses duplicates BEFORE anything is pushed or charged. The default uses a well-formed Companies House number when the page carries one and the registrable domain otherwise, so two domains owned by the same company collapse to one billed row — and a repeated input line is dropped at queue time, before it costs a request.

## `validateTaxId` (type: `boolean`):

Run the published GB VAT mod-97 checksum (both the standard and the 97-55 variant) on every VAT number found and emit vatNumberValid plus which variant it passed. Note that Companies House numbers carry NO checksum, so companyNumberFormatValid is a format and known-prefix check, never a checksum.

## `extractRegulator` (type: `boolean`):

The UK regulatory body a professional-services site must name (FCA, SRA, RICS, GDC, GMC, CQC, Ofsted, Gambling Commission, ATOL, Gas Safe and others) plus its reference number where published.

## `extractIco` (type: `boolean`):

The Information Commissioner's Office data-protection registration number (ZA######) where the site publishes it.

## `extractCharity` (type: `boolean`):

Registered charity number, including the Scottish OSCR form. Kept in its OWN field: a Scottish charity number has the same SC###### shape as a Scottish company number and must never be mistaken for one.

## `extractOfficer` (type: `boolean`):

A director, proprietor or company secretary named next to a label on the page. Honest expectation: UK sites rarely publish one — measured about 1% fill. Unlike a German Impressum, reg. 25 does not require it.

## `extractSocials` (type: `boolean`):

LinkedIn, Facebook, Instagram, X, YouTube and TikTok links present on the pages read.

## `extractPolicyUrls` (type: `boolean`):

The site's terms and privacy-policy URLs, linked from the pages read.

## `includeMissRows` (type: `boolean`):

Emit a row for every domain that produced no lead — dead host, blocked, publishes no disclosure, filtered out, a duplicate, or suppressed — with its status and missReason, so you can do coverage accounting. These rows are NEVER charged.

## `includeFieldSources` (type: `boolean`):

Add a fieldSources object naming the exact URL each field was read from. Useful when a disclosure is split across a terms page and a contact page and you need to audit which said what.

## `flattenOutput` (type: `boolean`):

On: one flat row, ready for Google Sheets, Clay or a CSV export. Off: fields grouped into company {}, registry {}, contact {}, people {} and policies {} objects.

## Actor input object example

```json
{
  "domains": [
    "rox.co.uk",
    "batchd.co.uk",
    "nq64.co.uk",
    "kickasshostels.co.uk",
    "vovem.co.uk",
    "alzscot.org",
    "shoezone.com",
    "actionforchildren.org.uk",
    "standrewsbrewhouse.com",
    "blundells.com"
  ],
  "maxItems": 1000,
  "maxDomains": 0,
  "maxDiscoveryRequestsPerDomain": 8,
  "maxPagesParsed": 3,
  "perDomainTimeoutSecs": 60,
  "requestConcurrency": 10,
  "requestTimeoutSecs": 25,
  "maxRequestRetries": 2,
  "discoveryChannels": [
    "homepage",
    "anchor",
    "sitemap",
    "wpJson",
    "pathGuess"
  ],
  "followWwwAndRootVariants": true,
  "deepJsDiscovery": false,
  "respectRobotsTxt": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "proxyCountry": "none",
  "escalateToResidentialOnBlock": true,
  "escalateToUnblockerOnBlock": false,
  "tldFilterMode": "none",
  "requireRegistryId": false,
  "requireValidVat": false,
  "requireContact": false,
  "jurisdictionFilter": [],
  "minFieldsRequired": 0,
  "emailPolicy": "all",
  "dedupeBy": "company-number-then-domain",
  "validateTaxId": true,
  "extractRegulator": true,
  "extractIco": true,
  "extractCharity": true,
  "extractOfficer": true,
  "extractSocials": true,
  "extractPolicyUrls": true,
  "includeMissRows": false,
  "includeFieldSources": false,
  "flattenOutput": true
}
```

# Actor output Schema

## `records` (type: `string`):

The dataset of UK company leads (one item per domain that published a statutory trading disclosure).

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

Coverage accounting for the run: domains attempted, rows billed, and the three kinds of nothing (unreachable host, blocked host, publishes no disclosure) kept apart - none of them charged.

# 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 = {
    "domains": [
        "rox.co.uk",
        "batchd.co.uk",
        "nq64.co.uk",
        "kickasshostels.co.uk",
        "vovem.co.uk",
        "alzscot.org",
        "shoezone.com",
        "actionforchildren.org.uk",
        "standrewsbrewhouse.com",
        "blundells.com"
    ],
    "maxItems": 1000,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/uk-trading-disclosures-contact-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 = {
    "domains": [
        "rox.co.uk",
        "batchd.co.uk",
        "nq64.co.uk",
        "kickasshostels.co.uk",
        "vovem.co.uk",
        "alzscot.org",
        "shoezone.com",
        "actionforchildren.org.uk",
        "standrewsbrewhouse.com",
        "blundells.com",
    ],
    "maxItems": 1000,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/uk-trading-disclosures-contact-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 '{
  "domains": [
    "rox.co.uk",
    "batchd.co.uk",
    "nq64.co.uk",
    "kickasshostels.co.uk",
    "vovem.co.uk",
    "alzscot.org",
    "shoezone.com",
    "actionforchildren.org.uk",
    "standrewsbrewhouse.com",
    "blundells.com"
  ],
  "maxItems": 1000,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call scrapersdelight/uk-trading-disclosures-contact-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapersdelight/uk-trading-disclosures-contact-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/1drYtzxfu1mnhLlzk/builds/v0heroL4089qorkuy/openapi.json
