# Changelog of UK Company Data — Directors, Owners & Firmographics (`foxlabs/uk-company-data`) Actor

- **URL**: https://apify.com/foxlabs/uk-company-data/changelog.md
- **Full Actor documentation**: https://apify.com/foxlabs/uk-company-data.md

## Changelog

### 0.1.2 — 2026-09-10 — name look-ups are matched, not taken blindly

- **Fixed: a name could return an unrelated company.** The name search used Companies House's first result whatever it was: in platform run NLa4YGOAww6PIudKH, `Zzqx Nonexistent Widgets Qwv` came back as AESTHETIC WIDGETS LTD (15172442) with `matched: true` and was charged. The new `src/name-match.js` scores each of the 5 results Companies House returns. Both names are normalised — upper case, a possessive 's dropped, `&` read as `and`, punctuation removed, a leading THE and trailing LTD / LIMITED / PLC / LLP / LP / CIC ignored — and a result is used only if it contains every query word or its Dice score is at least 0.8.
- **Ranking and ties.** Results are ranked by the share of the query's words they contain, then by fewest extra words, then an active company before any other status, then Companies House's order — so an exact name still beats a longer active one. Companies House lists some names more than once, and the first listing can be a closed overseas registration: for `British Airways` NF002699 (closed) comes before 01777777 (active), for `Marks and Spencer` NF001553 (closed) before 00214436 (active). Both now resolve to the active company. With `statusFilter` set, the best acceptable result with that status is taken first, so a filtered run no longer picks a record of another status and then skips it.
- **A name without a real match is a failed lookup, not charged.** Its row is `matched: false` with `error: No company name matching "…"; the closest result was … (score …)`, pushed without a charge. A name search that got no answer now says so (`Companies House did not answer the name search …`) instead of reporting the name as not found.
- **New fields on name rows:** `query` (the name searched — before, only failed name rows had it) and `name_match_score` (the Dice score, 0–1, of the chosen company, or of the closest result on a no-match row). Rows looked up by number or found by SIC discovery do not have them. No field was renamed or removed; apart from these two fields the Tesco row is identical to the 0.1.1 output (checked 2026-09-10).
- **Measured 2026-09-10:** offline, 27 of 27 cases pass on synthetic and real Companies House result lists — `Tesco` → TESCO PLC, `Tesco Stores` → TESCO STORES LIMITED, `British Airways` → 01777777, `Marks and Spencer` → 00214436, `Marks & Spencer` → MARKS & SPENCER OUTLET LIMITED, the nonsense name, a typo (`British Airwayz`, closest 0.50), extra words, a leading "The", and five tie / statusFilter cases. Known limit: `Sainsbury's` resolves to SAINSBURY'S LIMITED (SC171417), because J SAINSBURY PLC is not among Companies House's top 5 results for that spelling (the old code returned the same company). No legitimate query in the test set is rejected. Live: a 10-request Actor run (`Tesco` → TESCO PLC, score 1; the nonsense name → a no-match row naming AESTHETIC WIDGETS LTD, score 0.33, not charged), then 2 name searches through the Actor's own client and matcher (`British Airways` → 01777777, `Marks and Spencer` → 00214436).
- README: says exactly how names are matched and how ties are broken, and that a name without a match returns a failed-lookup row, not charged from 25 September 2026.

### 0.1.1 — 2026-09-10 — failed lookups become free (per-record billing) + README corrected + charges and company-number formats fixed + no false empty sections + run-wide rate limit

- **You will pay only for delivered company records.** The Actor now charges a new `company-record` event for each delivered company (`matched: true`, pushed through `src/billing.js`). Rows that only report a failed lookup — an entry that is not a company number, a number whose profile could not be read (`Company not found in Companies House.`), a name without search results (`Company name not found.`) — are pushed without a charge, and so is the summary row described below. Companies skipped by `statusFilter` still produce no row.
- **Timing:** Apify applies pricing changes after a 14-day notice. From **25 September 2026** the pricing switches from the per-dataset-item event to `company-record` at the same price ($0.004). Until then billing is exactly as before; the code detects the switch by itself.
- **Partial records are charged.** When the company profile was read, the row is a company record and is charged even if one of its sections could not be read; `unavailable_note` says which part is missing (same default as Thailand Company Data).
- **Run-wide rate limit (compliance).** Companies House allows 600 requests per 5 minutes per API key and may ban an application that keeps exceeding it; the built-in key is shared by every user, so a ban would stop the Actor for all of them. Every request of a run — name search, SIC discovery, profile, officers, PSC, filing history, charges and every retry — now takes a token from one bucket (new `src/run-limits.js`): 30 to start with, refilled at 1.5 per second, so no 5-minute window holds more than 480 requests (80% of the limit). With all sections a company takes 5 requests: about 90 companies per 5 minutes; 1,000 companies by number or SIC discovery take about 55 minutes (fits the default 60-minute timeout), 1,000 by name about 66 minutes (does not — the README says to raise the timeout to 75 minutes). Measured offline with a fake clock; `maxConcurrency` no longer changes the speed.
- **Clean stop before the timeout.** Like india-government-tenders, the run stops starting companies 90 s before its timeout (name search and SIC discovery stop too), keeps every row it has, and writes one summary row — `record_type: "summary"`, `message`, `companies_left`, `left_to_look_up` — pushed without a charge; the status message says the same. Retry waits never run past 30 s before the timeout. Before, a run that ran out of time was killed by the platform mid-company.
- **Fixed: charges.** The charges register (`/company/{number}/charges`) is now read for every company. It used to be read only when the profile's `has_charges` flag was `true`, and Companies House returned `false` for all 7 companies measured on 2026-09-10 — including Tesco (9 charges) and ASDA Stores (32) — so every row showed `charges_count: 0`. Reading it for every company exposed a second bug: `secured_details` comes back as an object, the parser treated it as a list and threw for every company with charges, and the throw emptied the filing history too. The parser now accepts either shape. `has_charges` is now `true` when at least one charge is registered, and `charges_outstanding` counts the charges that are neither satisfied nor part-satisfied (it held `part_satisfied_count` before). Checked against the Companies House charges pages on 2026-09-10: Tesco 9 registered / 2 outstanding / 0 part satisfied / 7 satisfied, ASDA Stores 32 / 26 / 0 / 6 — the same numbers as the rows. This adds one request per company.
- **No false zero, no silent cut (charges).** Charge shaping lives in the new `src/charges.js`. If the charges request fails, `has_charges`, `charges`, `charges_count`, `charges_outstanding` and `charges_text` are `null` — unknown, not 0 — and `charges_note` says so. One page of 50 charges is requested, newest first; the counts come from Companies House's own totals, so they cover every charge, and when a company has more than 50, `charges_note` says the list is cut. Run on 2026-09-10: British Airways Plc (01777777) — 898 charges, 168 satisfied → `charges_count` 898, `charges_outstanding` 730, 50 listed, `charges_note` present.
- **No false empty lists (officers, PSC, filing history).** These sections now follow the charges rule (new `src/sections.js`). A 404 from Companies House is a real empty list; a request that got no answer after its retries leaves that section `null` — `officers`, `officers_count`, `officers_truncated`, `officers_text`, `resigned_officers`, `resigned_officers_count`; `persons_with_significant_control`, `psc_count`, `psc_text`; `recent_filings`, `filing_categories`, `total_filings` — and two new fields name it: `unavailable_sections` (e.g. `["officers", "charges"]`) and `unavailable_note` (which requests failed and why). Before, a failed request produced an empty board, no PSCs and no filings, as if the company had none. A response that cannot be read is treated the same way instead of breaking the row.
- **Retries with a bounded back-off.** The HTTP client moved to `src/ch-client.js`. A 429 is retried after Retry-After or 20, 40, 80 and 160 seconds (each wait at most 160 s, all waits of one request at most 300 s — one full window); before, it waited 5 seconds, or any Retry-After value without a cap, and gave up after 3 tries. 5xx, network errors and unreadable bodies are retried after 0.6, 1.2, 2.4 and 4.8 seconds; other 4xx answers (for example 401) are no longer retried. At most 5 tries per request.
- **Fixed: company-number formats.** New `src/company-number.js`: every Companies House format is read — 8 digits, or a letter prefix such as SC, NI, OC, SO, NC, LP, SL, NL, FC or R, and society numbers that end in a letter. Spaces and dashes are removed and leading zeros restored (`445790` → `00445790`, `SC45439` → `SC045439`). Before, letters were stripped and only 8-digit results kept, so every company outside England & Wales was dropped without a row. An entry that cannot be a company number now gets its own row `Not a Companies House company number …` (`matched: false`, plain push). Run on 2026-09-10: `SC117119` → SSE PLC, `NI032978` → SX3 LIMITED, `445790` → TESCO PLC, `HELLO` → error row.
- **Descriptions kept true:** the `.actor/actor.json` description (numbers in any format, charges), the input form's company-number and section texts, and the dataset schema's `company_number`, `has_charges`, `charges_note`, `unavailable_sections`, `unavailable_note`, `record_type`, `message`, `companies_left` and `left_to_look_up` descriptions.
- **Output fields:** seven fields were added, all present only when needed — `charges_note`, `unavailable_sections`, `unavailable_note`, and on the summary row `record_type`, `message`, `companies_left`, `left_to_look_up`. No field was renamed or removed. `has_charges`, `charges[]`, `charges_count`, `charges_outstanding` and `charges_text` now carry real values, `charges_outstanding` changes meaning as described above, the section fields can be `null` when Companies House did not answer, and `total_filings` is `0` rather than missing when a company has no filings. A normal row is unchanged: the Tesco row of 2026-09-10 has the same fields and values before and after these changes.
- **README corrected.** The 2026-09-07 README was generic and partly false. Corrected claims:
  - It documented inputs this Actor does not read — `queries`, `maxResultsPerQuery`, `includeRaw` — with the example `{"queries":["undefined"],"maxResultsPerQuery":50}` in the Quick Start, AI-agent, JS and Python snippets. The real inputs are `api_key`, `companyNumbers`, `companyNames`, `sicCode`, `statusFilter`, `maxResults`, `maxConcurrency` and `proxyConfiguration`.
  - "Whole registry, one call" and "formation / status monitoring": neither is a feature (there is no incorporation-date filter).
  - "Relevance-ranked name search": the first Companies House search result is used, and the row does not repeat the searched name.
  - "The registry's own contact fields": `registered_email` and `website` were empty for every company in our test runs.
  - "Deduplicated camelCase rows" and "every row carries `query`, `scrapedAt`": fields are snake\_case (`scraped_at`) and only rows for unresolved names carry `query`.
  - "Returns the profile plus counts; the full PSC / charges lists are a planned enrichment": the PSC list is returned (up to 100), and the charges list is fixed above.
  - "Empty or failed lookups are never billed": until the pricing switch they are counted as dataset items.
- **The 0.1 entry below is wrong:** none of the twelve listed columns was removed; the Actor still returns all of them. In our 2026-09-10 test `dissolution_date` was filled for 4 of 7 companies, `annual_return` for 3 and `persons_with_significant_control` / `psc_text` for 2.

### 0.1 — 2026-09-07

- **Dropped empty-promise columns.** Removed `company_status_detail, subtype, dissolution_date, foreign_company_details, branch_company_details, annual_return, registered_email, website, persons_with_significant_control, psc_text, charges, charges_text` — UK Companies House register (official open API) does not carry them, so they were shipped as always-null columns. Only fields this source actually fills are now emitted.
- **Enabled AI-agent payments (x402) + rebuilt the README to the full standard** (What-is / when, AI-agents + x402 agentic payments + MCP, Overview, Features, Use cases, Integration, FAQ, Troubleshooting, Support & contact).

### 0.0

- Initial release: data from the UK Companies House register (official open API) by name or registry ID.
