# Companies House Monitor — filings, officers, PSC changes (`multiplex/companies-house-monitor`) Actor

Know the moment a UK company you care about files at Companies House, changes a director, changes a PSC, changes its name or is dissolved. Give it company numbers or names; it watches filing history, officers, PSCs and the profile, and emits only what changed — with the previous value.

- **URL**: https://apify.com/multiplex/companies-house-monitor.md
- **Developed by:** [Daniel James](https://apify.com/multiplex) (community)
- **Categories:** Business, Lead generation
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Companies House Monitor

**Know the moment a company you care about files, changes officers or changes PSCs.**

There are dozens of ways to *look up* a UK company. This is not one of them. This actor **watches** a
list of companies and tells you **what changed since last time** — a new set of accounts, a director
appointed, a director resigned, a person with significant control ceasing, a name change, a
dissolution — with the previous value attached so the alert is actionable on its own.

Straight from the official Companies House Public Data API. No scraping, no browser, no proxy.

### Who this is for

- **Accountants** — you have 400 clients and a filing deadline calendar. Get told the night a
  client's confirmation statement lands, or the night one of them appoints a director you have never
  heard of.
- **Lenders and credit teams** — a borrower quietly changing its PSC, changing its name, or slipping
  into liquidation is the event you needed to know about on the day, not at the next review.
- **Compliance and KYB** — ongoing monitoring is the part of KYB everyone skips because it means
  re-checking a spreadsheet by hand. Run this nightly against your book instead.
- **Procurement and supplier risk** — a supplier's directors all resigning is a signal. So is a
  charge, a name change, or a dissolution notice.
- **Journalists and researchers** — one schema, stable IDs, joinable across runs.

### What it does

Give it company numbers, or company names, and pick what to watch:

| `track` | Watches |
|---|---|
| `filings` | filing history — accounts, confirmation statements, appointments, capital, charges |
| `officers` | appointments and resignations |
| `psc` | persons with significant control — notified, ceased, changed |
| `profile` | company name, status, dissolution, next-due dates |
| `all` | all four (the default) |

Every row comes back in **one schema**, whichever part of the register it came from:

```json
{
  "id": "3f5a1c9e0b7d4a2f8c6e1b3d5a7f9c2e4b6d8a01",
  "companyNumber": "00445790",
  "companyName": "TESCO PLC",
  "companyStatus": "active",
  "category": "filings",
  "itemId": "MzM1Njk4NDgyOGFkaXF6a2N4",
  "description": "accounts-with-made-up-date",
  "date": "2026-03-31",
  "status": "filed",
  "filingType": "AA",
  "filingCategory": "accounts",
  "companyUrl": "https://find-and-update.company-information.service.gov.uk/company/00445790",
  "links": {
    "selfUrl": "https://api.company-information.service.gov.uk/company/00445790/filing-history/MzM1Njk4NDgyOGFkaXF6a2N4"
  },
  "query": "Tesco"
}
```

The same shape, from the officers register:

```json
{
  "companyNumber": "00445790",
  "companyName": "TESCO PLC",
  "category": "officers",
  "description": "FIXTURE, Jane Elizabeth, director",
  "date": "2019-05-01",
  "status": "active",
  "officerName": "FIXTURE, Jane Elizabeth",
  "officerRole": "director",
  "appointedOn": "2019-05-01",
  "resignedOn": null,
  "dateOfBirthYear": 1975
}
```

- **Stable IDs** — `sha1(companyNumber:category:itemId)`, built on the register's **own** identifier
  for the item (a transaction id, an appointment id, a PSC notification id). The same filing always
  produces the same `id`, so diffs and joins downstream just work, run after run.
- **`companyName` and `companyStatus` on every row** — including filing and officer rows. An alert
  that says "a director resigned" is useless without the company's name next to it, and you should
  not need a second lookup to get it.
- **`query` on every row** tells you which of your inputs produced it.
- **Names resolve automatically.** Type `Tesco PLC` and it is resolved through the official company
  search — and the match it picked is **written to the log**, because a silent best guess is how a
  monitor ends up watching the wrong company. Supply the number when you need certainty.
- **Short company numbers are padded.** `445790` becomes `00445790`. Unpadded numbers 404 against
  the API, and every spreadsheet in the country holds them unpadded.

### Monitor mode (`changesOnly`) — the actual product

Turn a watchlist into an alert feed. State is remembered between runs in the named key-value store
`companies-house-state`, and only movement is emitted:

| `changeType` | Meaning |
|---|---|
| `new` | a filing that was not there before; an officer newly appointed; a PSC newly notified |
| `updated` | something changed on an existing record — a director now shows as resigned, a PSC as ceased, the company as dissolved or renamed |
| `removed` | the record no longer comes back from the register at all |

Updated rows carry the **before and the after**, which is the part that makes an alert worth having:

```json
{
  "changeType": "updated",
  "changedFields": ["status"],
  "category": "officers",
  "companyName": "TESCO PLC",
  "description": "FIXTURE, Jane Elizabeth, director",
  "previousStatus": "active",
  "status": "resigned",
  "resignedOn": "2026-08-14"
}
```

A company changing its name comes through on the profile row with `previousCompanyName` filled in —
one of the highest-value signals on the whole register, and one a name-keyed watchlist misses
entirely.

**The first run emits everything once**, as the baseline. Every run after that is quiet unless
something moved.

#### Two false alarms this actor refuses to raise

1. **Filings are never reported as `removed`.** The filing history is append-only. A filing that is
   no longer in the fetched window has scrolled past `maxFilings` — it has not been withdrawn from
   the register. Telling an accountant a client had retracted its accounts would be the worst thing
   this actor could say, so it does not say it, and a test enforces that.
2. **Narrowing `track` never mass-reports the rest as gone.** Run once with `track: all` and then
   with `track: filings`, and a naive monitor announces that every director and every PSC has
   vanished from the register. State is category-aware: untracked records are carried forward
   untouched, and only a category this run actually fetched can produce a `removed` row.

### Getting your free API key (2 minutes, no payment step)

The Companies House Public Data API requires a key on **every** request. It is free.

1. Go to **https://developer.company-information.service.gov.uk/** and sign in (or create a
   Companies House account — the same one used for filing).
2. Open **Manage applications** → **Create an application**. Give it a name; choose the **Live**
   environment.
3. In the application, choose **Create new key** → **REST API key** (sometimes labelled "API key").
4. Copy the key into this actor's `apiKey` field. It is stored as a secret input.

**It must be a REST API key.** The hub also issues *stream keys* (for the streaming API) and *OAuth
web clients* (for acting on behalf of a signed-in user). Neither authenticates these endpoints, and
pasting one produces a `401` that looks exactly like a wrong key — which is why this actor's 401
message says so in words.

Under the bonnet the key is sent as HTTP Basic with the key as the **username** and a **blank
password**, exactly as Companies House documents it. The actor's test suite asserts its header
matches Companies House's own published example byte for byte.

### Input

| Field | Example | Notes |
|---|---|---|
| `apiKey` | — | **required**, secret. Free — see above |
| `companies` | `00445790`, `SC123456`, `Tesco PLC` | **required**, one per line. Numbers or names |
| `track` | `all` | `all`, `filings`, `officers`, `psc`, `profile` |
| `changesOnly` | `false` | monitor mode, as above |
| `maxCompanies` | `25` | per run — keeps a long watchlist inside the rate limit |
| `maxFilings` | `25` | how far back into the filing history each run looks |

### Rate limits, and how this actor behaves inside them

Companies House publishes a limit of **600 requests in any 5-minute period**, returns `429` when you
exceed it, and reserves the right to **ban an application without notice** for regularly exceeding
or trying to bypass it.

This client therefore **paces itself below the limit** rather than sprinting into 429s: one request
every 550 ms, process-wide, which works out at roughly 545 requests per 5 minutes. It honours
`Retry-After` exactly when the API does send a 429, and it backs off rather than hammering on 5xx.

**Budget your runs.** `track: all` costs **4 requests per company** (profile, filings, officers,
PSC); a single narrower `track` costs 2 (the profile is always fetched, because every row carries the
company's name and status). A name that has to be resolved costs 1 more, once. So:

| Watchlist | `track` | Requests | Wall time |
|---|---|---|---|
| 25 companies | `all` | 100 | ~1 minute |
| 25 companies | `filings` | 50 | ~30 seconds |
| 100 companies | `all` | 400 | ~4 minutes |

The limit is **per API key**, not per run — so if you schedule several runs, stagger them, and
remember anything else you have built on the same key is spending the same 600.

### Two things about this data that will bite you elsewhere

**1. A filing's `description` is an enumeration key, not a sentence.** Companies House sends
`accounts-with-made-up-date`, not "Accounts made up to 31 December 2025". The sentence lives in the
[`companieshouse/api-enumerations`](https://github.com/companieshouse/api-enumerations) file and is
interpolated client-side. This actor emits the **key verbatim** — resolve it yourself if you want the
prose. It does not fabricate a sentence from the key, because a fabricated sentence that looks
official is worse than an honest identifier. `filingType` (`AA`, `AP01`, `CS01`, `SH03`) and
`filingCategory` (`accounts`, `officers`, `capital`, …) are there for filtering and are stable.

**2. A `404` from the PSC endpoint is normal.** A company that has never filed PSC information has no
PSC resource at all. This actor reports "no PSC records" and carries on. Anything that treats that as
an error will mark healthy companies as failed on every single run.

### Personal data — what this actor deliberately does not publish

The officer and PSC registers are, by design of the public register, records about **named natural
persons**. Publication by Companies House is lawful; republishing every field of it is a separate
decision, and this actor makes a conservative one.

**Emitted:** name, role / PSC kind, natures of control, appointment / resignation / notification /
cessation dates, and the **year** of birth.

**Never emitted:** the service address (which for a great many small companies *is* the person's
home), nationality, occupation, country of residence, former names, the cross-company officer
identifier that follows a person between companies, and the filing `description_values` object —
which for appointment filings carries the officer's name.

Two details worth stating plainly:

- Companies House already suppresses the **day** of an officer's date of birth and publishes month +
  year. This actor reduces it again to the **year alone**. The year is enough to tell two directors
  of the same name apart, which is the only thing a business monitor legitimately needs it for;
  month + year is a much stronger token for matching a person against other datasets.
- **Super-secure PSCs** — people whose details the register withholds for their personal safety —
  still produce a row, so you can see one appear or cease, but with no name, and none is invented.

This is enforced by construction: every row is built field by field from an allow-list, with no
object spread anywhere in the mapper, so a new personal field appearing in a future API version
cannot leak by default. Tests assert that planted addresses, nationalities, occupations, former names
and birth months never reach the output.

The **company's registered office** IS emitted, on the profile row — that is the company's official
address for service and a company attribute, not an officer's personal one. Note that for a
single-director company it is frequently that director's home; Companies House publishes no flag
distinguishing the two, so no automated redaction is possible and none is claimed.

### Licence and terms

Companies House publishes this data through its own public API and this actor uses it as documented,
within the published rate limit. **No Open Government Licence statement was found on any Companies
House page read during development**, so none is asserted here — if you intend to republish the data
rather than consume it, confirm the terms with Companies House yourself. See
`tests/fixtures/SOURCES.md` for exactly what was and was not verified.

### Running it locally

```bash
npm install
npm test                      # fixture-driven, no network, no key needed
APIFY_CH_KEY=your-key npm test   # additionally runs one live smoke call
npm start                     # reads storage/key_value_stores/default/INPUT.json
```

Every claim this actor makes about the API — the auth mechanism, the rate limit, the endpoints, the
404-on-PSC behaviour, the enumeration-key `description` — is recorded with its evidence in
**`tests/fixtures/SOURCES.md`**, including the things that could **not** be verified and what was
done about them instead.

# Actor input Schema

## `apiKey` (type: `string`):

Free REST API key from the Companies House developer hub. Sign in at https://developer.company-information.service.gov.uk/, open **Manage applications**, create an application, then create a **REST API key** for it. A stream key or an OAuth web client will NOT authenticate these endpoints. The key is sent as the HTTP Basic username with a blank password, exactly as Companies House documents.

## `companies` (type: `array`):

One per line. A company number (`00445790`, `445790`, `SC123456`) is used as typed — short numbers are zero-padded to the register's eight characters. Anything else is treated as a name and resolved through the official company search; the match is written to the log, so check it. Supply numbers when you need certainty.

## `track` (type: `string`):

Which part of the register to watch. `all` is one profile call plus three per company.

## `changesOnly` (type: `boolean`):

Emit only what is new, changed or gone since the previous run of this actor on the same company. State is kept in the named key-value store `companies-house-state`. Changed rows carry `changedFields`, `previousStatus` and `previousDescription`. The first run emits everything once as the baseline. Filings are never reported as removed — the filing history is append-only.

## `maxCompanies` (type: `integer`):

Caps how many companies one run touches, so a long watchlist cannot walk into the API's 600-requests-per-5-minutes limit. `track: all` costs 4 requests per company.

## `maxFilings` (type: `integer`):

How far back into the filing history each run looks. 25 is plenty for a nightly monitor; raise it for a first-run backfill.

## Actor input object example

```json
{
  "companies": [
    "00445790"
  ],
  "track": "all",
  "changesOnly": false,
  "maxCompanies": 25,
  "maxFilings": 25
}
```

# Actor output Schema

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

One item per filing, officer, PSC or company profile, in one normalised schema (monitor mode adds changeType, changedFields and the previous status/description).

## `overview` (type: `string`):

The same results as a ready-to-read table view in the Apify console.

# 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 = {
    "companies": [
        "00445790"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("multiplex/companies-house-monitor").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 = { "companies": ["00445790"] }

# Run the Actor and wait for it to finish
run = client.actor("multiplex/companies-house-monitor").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 '{
  "companies": [
    "00445790"
  ]
}' |
apify call multiplex/companies-house-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,multiplex/companies-house-monitor"
        }
    }
}

```

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/AgSubePrx1jPtPBA1/builds/PSqX5E46TFNm4NyBr/openapi.json
