# UK Company KYB: Companies House, Officers, PSC & Insolvency (`datacraft-studio/uk-company-enrichment`) Actor

UK company data for KYB and KYC. Enrich a company number (CRN) or name from Companies House: status, registered office, SIC, directors, PSC beneficial owners, charges, overdue accounts and insolvency, plus The Gazette.

- **URL**: https://apify.com/datacraft-studio/uk-company-enrichment.md
- **Developed by:** [Yahya Nouali](https://apify.com/datacraft-studio) (community)
- **Categories:** Lead generation, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 company enricheds

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 Company KYB: Companies House, Officers, PSC & Insolvency

Look up a UK company by **company number (CRN)** or registered name and get a KYB-ready record: status, registered office, SIC codes, directors, **persons with significant control (beneficial owners)**, charges, accounts and confirmation-statement dates, overdue flags, insolvency cases, and — when the register already shows distress — insolvency notices from **The Gazette**.

Built for onboarding checks, supplier screening, and lead enrichment. Misses are free. A monitor mode stores a baseline and bills only when the register changes.

### Why this is enough for a first-line KYB check

Companies House is the UK registrar. The public profile already answers the questions a reviewer asks before opening a filing PDF: is the company active, where is the registered office, what does it do, are accounts overdue, and is there an insolvency flag? Officers and PSCs name the people and entities in control. Charges show outstanding security. The Gazette search adds winding-up and other insolvency notices that cite the company number.

`riskFlag` is the field to sort on. It is true for dissolution, liquidation, receivership, administration, voluntary arrangement, insolvency proceedings, an insolvency case, overdue accounts, a strike-off proposal, or a matching Gazette insolvency notice.

### Input

```json
{
  "companies": ["00445790", "SC123456", "NORTHWIND TRADING LIMITED"],
  "mode": "enrich",
  "includeOfficers": true,
  "includePsc": true,
  "includeCharges": false,
  "includeFilings": false,
  "includeInsolvency": false,
  "gazetteNotices": "auto",
  "snapshotStoreName": "uk-company-monitor"
}
```

| Field | Default | What it does |
| --- | --- | --- |
| `companies` | required | Company numbers or names. One item each. |
| `mode` | `enrich` | `monitor` writes a snapshot and emits only changes. |
| `includeOfficers` | `true` | Directors, secretaries, LLP members. |
| `includePsc` | `true` | Beneficial owners (PSC register). |
| `includeCharges` | `false` | Also fetched when `has_charges` or `links.charges` is set. |
| `includeFilings` | `false` | Latest 25 filing-history items. Due dates already come from the profile. |
| `includeInsolvency` | `false` | Also fetched when the profile indicates insolvency. |
| `gazetteNotices` | `auto` | `auto`, `always`, or `off`. |
| `companiesHouseApiKey` | empty | Secret override of `COMPANIES_HOUSE_API_KEY`. |
| `snapshotStoreName` | `uk-company-monitor` | Named key-value store used in monitor mode. |
| `activeOfficersOnly` | `false` | Hides resigned officers in the output list. |
| `maxCompanies` | `1000` | Hard cap 5000. |

Company numbers are normalised to 8 characters. `445790` becomes `00445790`. `sc 1234` becomes `SC001234`. Prefixes include `SC` (Scotland), `NI` (Northern Ireland), `OC` (LLP), `LP` (limited partnership), and the other two-letter series used on the register. Older Northern Ireland numbers such as `R123456` are padded to `R0123456`. A value that is not a number is searched by name. An exact normalised name is `EXACT`. A weaker hit is returned with `matchQuality: "REVIEW"` and up to three alternates.

### Output

Each dataset item is one JSON object. Scalar fields are listed first so a CSV export stays usable. `riskFlag` is the first property.

The fragment below is the shape of a row. Names and addresses come from the live register when you run the Actor; this sample is only the field layout.

```json
{
  "riskFlag": false,
  "riskLevel": "NONE",
  "riskSummary": "No insolvency, dissolution, or overdue-accounts flag in the data retrieved.",
  "recordType": "company",
  "query": "12345678",
  "matchQuality": "NUMBER",
  "found": true,
  "companyNumber": "12345678",
  "companyName": "NORTHWIND TRADING LIMITED",
  "companyStatus": "active",
  "registeredOffice": "1 High Street, London, EC1A 1AA",
  "sicCodesFlat": "62012",
  "accountsOverdue": false,
  "officerNames": "DOE, Jane",
  "pscNames": "HOLDING LTD",
  "charged": true,
  "billingEvents": ["company-enriched", "officers-psc"],
  "companiesHouseUrl": "https://find-and-update.company-information.service.gov.uk/company/12345678"
}
```

Nested arrays — `officers`, `personsWithSignificantControl`, `charges`, `insolvencyCases`, `filings`, `gazetteNotices`, `changes` — are on the same object. `sourceLinks` points at the Companies House company, officers, PSC, filing history, and charges pages, and at a Gazette notice search for that number.

`riskLevel` is `HIGH` when `riskFlag` is true, `REVIEW` for a weaker signal (overdue confirmation statement, undeliverable office, office in dispute, outstanding charge, ceased-but-not-dissolved, or an inexact name match), and `NONE` otherwise.

Monitor rows use `recordType: "change"` and include `changedFields` plus `changeSummary`. Compared fields:

- status and status detail
- name
- registered office
- SIC codes
- officers appointed or resigned
- PSC added, ceased, or with a control change
- new charges
- new insolvency cases
- accounts or confirmation statement becoming overdue, or ceasing to be overdue
- new Gazette notices, when that section was fetched on both runs
- a name query that resolves to a different company number than last time

The first time a section is fetched it is stored and not treated as a change. A later run diffs it. Turning officers on after a profile-only baseline does not bill every current director as a new appointment.

Invalid numbers and failed requests are dataset rows (`invalid`, `invalid_identifier`, `error`) and are not charged. A value that looks like a broken company number, such as `ZZ!!12` or a digit-heavy string that is not a CRN, is `invalid_identifier` and is not sent to the name search. Ordinary names, including names with an ampersand or a single digit such as `3M`, are still searched. In monitor mode, a company that is simply unchanged, or not found on the first run, is counted in the `RUN_SUMMARY` key-value record and is not written to the dataset.

### Pricing

Pay per event. There is no start fee and no per-row dataset fee.

| Event | Price | When |
| --- | --- | --- |
| `company-enriched` | $0.004 | Enrich mode, and the profile was found. Primary event. |
| `officers-psc` | $0.002 | Enrich mode, officers and/or PSCs were requested, and the register answered (including an empty list). |
| `change-detected` | $0.015 | Monitor mode, and this company differs from the saved snapshot. |
| — | $0 | Not found, invalid input, first monitor baseline, unchanged company. |

1,000 enriched companies with directors and PSCs cost $6. A later monitor pass that finds 8 changes costs $0.12. See `PUBLISHING.md` for the console pricing JSON.

### API key

The Actor reads `COMPANIES_HOUSE_API_KEY` from the environment. On Apify, set that variable as a secret. You can also pass `companiesHouseApiKey` on the input; that value wins for the run and is marked secret in the input schema.

Companies House uses HTTP Basic authentication with the key as the username and an empty password. The key is not written to the dataset or the log.

Create a key in the [Companies House developer hub](https://developer.company-information.service.gov.uk/). The published limit is **600 requests per 5 minutes per key**. This Actor shares one token bucket across the run, retries HTTP 429 with `Retry-After` or exponential backoff, and stops the run if the limit still persists. It does not call officers, PSCs, charges, insolvency, or filing history unless you asked or the profile says that resource exists (`has_charges`, `has_insolvency_history`, `has_been_liquidated`, `links.charges`, `links.insolvency`, or an insolvency status). Those boolean flags are deprecated by Companies House in favour of the links; both are honoured.

The Gazette lookup does not use a key.

### Monitor mode

1. Run with `"mode": "monitor"`.
2. The Actor writes one snapshot per found company into the named key-value store (`snapshotStoreName`, default `uk-company-monitor`) on the Apify account that runs it.
3. That first run emits no company rows and charges nothing.
4. The next run diffs the live register against the snapshot. Only changed companies are pushed, at $0.015 each.
5. Deleting the store, or changing its name, starts a new free baseline.

Prefer company numbers for monitoring. A name is resolved again on every run. If the best match changes, the row is a `resolved-company` change so a KYB list does not silently follow a different entity.

### Run locally

```bash
npm install
npm test
npm run build
```

Live check, one profile request, skipped when the key is absent:

```bash
COMPANIES_HOUSE_API_KEY=your_key npm run smoke
```

Full local run (Apify stores results under `storage/`):

```bash
mkdir -p storage/key_value_stores/default
cp examples/input.enrich.json storage/key_value_stores/default/INPUT.json
COMPANIES_HOUSE_API_KEY=your_key npm start
```

Package a source archive without `node_modules`:

```bash
npm run package:actor
```

### Use cases

- **KYB / KYC onboarding.** Confirm the CRN, legal name, status, registered office, SIC, directors, and PSCs before opening an account or paying a supplier.
- **Lead enrichment.** Turn a list of names or numbers into a spreadsheet with status, address, and active officer names. Rows marked `REVIEW` need a human to confirm the name match.
- **Portfolio monitoring.** Re-run the same CRN list weekly. Pay only when status, address, control, charges, or overdue filings change.
- **Insolvency screening.** `riskFlag` collects dissolution, insolvency status, overdue accounts, strike-off proposals, and Gazette notices in one column.

### Integrations: Make, Zapier, n8n

You can call this Actor from no-code tools through Apify's official integrations. Use its Actor ID `tM7A2MKgSLKfMx0ro`, and connect each tool with OAuth or your Apify API token. Never put the token in the Actor input.

- **Make**: in the Apify app, add **Run an Actor** and set *Run synchronously* to Yes. Then add **Get Dataset Items** with the run's default dataset ID, and map the rows to Google Sheets, a CRM or Slack. For long runs, use the **Watch Actor Runs** trigger instead.
- **Zapier**: the Apify app has the **Run Actor** action. A synchronous run has a 30-second hard timeout, so for larger lists start the run and use the **Finished Actor Run** trigger with **Fetch Dataset Items**.
- **n8n**: install the official node `@apify/n8n-nodes-apify`. It is a verified community node on n8n Cloud; on self-hosted n8n, add it under Settings > Community Nodes. Use **Run an Actor and Get Dataset**, or the Apify trigger for finished runs.

Example input:

```json
{"mode": "enrich", "companies": ["00445790"], "includeOfficers": true, "includePsc": true}
```

#### Scheduled monitor to Slack or Sheets

1. Save a monitor input as an Actor task and keep the same store name on every run:

```json
{
  "mode": "monitor",
  "companies": [
    "00445790"
  ],
  "snapshotStoreName": "uk-company-monitor"
}
```

2. In Apify Console > **Schedules**, run the task on a cron such as `0 7 * * 1` (every Monday at 07:00), with your timezone selected.
3. On the task's **Integrations** tab, add Slack or an HTTP webhook on *Run succeeded*. You can also use Make **Watch Actor Runs**, Zapier **Finished Actor Run** or the n8n Apify trigger.
4. Fetch the dataset items and keep only the change rows: `recordType` is `"change"`. The first run only stores the baseline and is free.

#### HTTP API

This call runs the Actor and returns the dataset rows in one request. Synchronous runs time out after 300 seconds, so use `/runs` for big lists.

```bash
curl -X POST "https://api.apify.com/v2/acts/tM7A2MKgSLKfMx0ro/run-sync-get-dataset-items" \
  -H "Authorization: Bearer <YOUR_APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"mode": "enrich", "companies": ["00445790"], "includeOfficers": true, "includePsc": true}'
```

### FAQ

#### Which sources does this use?

- [Companies House Public Data API](https://developer.company-information.service.gov.uk/) at `api.company-information.service.gov.uk`: company profile (including registered office, SIC, status, accounts, and confirmation statement), officers, persons with significant control, filing history, charges, and insolvency.
- [The Gazette](https://www.thegazette.co.uk/) notice feed at `/insolvency/notice/data.json`, which is limited to corporate and personal insolvency notices. No key.

Document images are not downloaded. Accounts *figures* and VAT numbers are not part of the public profile API, so they are not in the output.

#### What licence applies?

**Companies House.** The Public Data API is free and requires a key. Companies House states that it places no restriction on how public register information is used once it has been obtained, apart from three points: customers must take their own advice on third-party copyright in filed documents, the Crown insignia must not be reproduced, and Companies House must be credited when its guidance, website text, or statistics are reused. The Open Government Licence v3.0 applies to Companies House publications where that licence is stated. It is not a blanket licence over every scanned filing. This Actor returns structured register fields only.

The rate limit and the instruction not to embed API keys in source are in the [Companies House developer guidelines](https://developer.company-information.service.gov.uk/developer-guidelines). Reuse of register information is described in [Companies House: public task, copyright and Crown copyright](https://www.gov.uk/government/publications/companies-house-accreditation-to-information-fair-traders-scheme/public-task-copyright-and-crown-copyright).

**The Gazette.** Notice content other than personal data is Crown copyright and may be reused under the [Open Government Licence v3.0](https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/). The Gazette says the same in [Re-using our data](https://www.thegazette.co.uk/data) and in its developer documentation. The OGL does not cover personal data. The paid Gazette Electronic Information Service is a separate subscription and is not called here.

The Gazette [fair-use policy](https://www.thegazette.co.uk/fair-use-policy) for automated tools asks for an identifiable User-Agent, at most 5 requests every 10 seconds, respect for `robots.txt` and `Retry-After`, and — for unattended crawlers — operation between 21:00 and 07:00 UK time. This Actor does one targeted lookup per company when Gazette search is on, and it rate-limits those calls to 5 per 10 seconds. It is not a crawl of the site. Keep large batches inside that policy, and contact The Gazette before a high-volume unattended job. `gazetteNotices: "auto"` (the default) only searches when Companies House already shows insolvency, liquidation, or a proposal to strike the company off.

#### What about GDPR and dates of birth?

Officer names, roles, service addresses, nationality, occupation, country of residence, and month and year of birth are published on the public register under the Companies Act. PSC details are public in the same way, except where Companies House has protected a person. Protected PSCs are returned as `protected: true` with no name and no birth month.

The public API provides **month and year only**. This Actor never outputs a full date of birth. If a payload ever contained a day, that day is discarded and is not written to the dataset.

If you store these rows, or join them to other personal data, you need your own UK GDPR lawful basis. KYB and AML checks are often done as legitimate interests, which still need an assessment. Gazette notice text can name individuals. The Open Government Licence does not authorise reuse of that personal data; handle notice excerpts accordingly.

#### Why was I not charged?

The row was invalid, the company was not found, the monitor run was the first baseline, or nothing had changed since the snapshot. `charged` is false and `billingEvents` is empty on those rows. A failed request (`recordType: "error"`) is also free.

#### Why is the name match marked REVIEW?

The search endpoint returned companies, but none had the same name once punctuation and a trailing “Limited” / “Ltd” / “PLC” were ignored. The best hit is still returned, with alternates, so you can pick the right CRN. An exact match is not marked REVIEW. Prefer passing the company number when you have it.

### Limitations

- One Companies House key allows 600 requests in each 5-minute window. This Actor cannot raise that quota. A company with profile, officers, and PSCs costs three requests; charges, insolvency, filings, and a name search add more, and only when needed.
- Officers, PSCs, and charges are capped at the first 100 items. Filings are capped at 25. `warnings` says when the list was truncated.
- Gazette search is one page of 10 notices for the company number, then filtered to notices that mention that number or the registered name. It will miss a notice that cites neither.
- No accounts figures, no VAT number, no document PDFs, no shareholder register beyond what the PSC record contains.
- Monitor snapshots live in the caller’s named key-value store. Another Apify user has a separate store. Reset the baseline by deleting the store.
- A restarted run uses delivery markers to avoid a second charge for the same company in the same run id. A crash in the moment after a push and before that marker can duplicate a row; it should not duplicate the charge.

### Development

Node.js 20 or newer. Tests use mocked HTTP and fixtures shaped like the published Companies House and Gazette JSON. They do not call the live APIs.

```bash
npm test
npm run typecheck
```

CI runs both on Node 20.

# Changelog

This Actor's version history is a separate document: https://apify.com/datacraft-studio/uk-company-enrichment/changelog.md

# Actor input Schema

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

UK company numbers (CRN) or registered names, one per item. Numbers are normalised to 8 characters. Examples: 00445790, SC123456, NI012345, OC123456, LP123456.

## `mode` (type: `string`):

enrich returns a row per company. monitor stores a snapshot in the named key-value store and returns a row only when something material changed. The first monitor run is a free baseline.

## `includeOfficers` (type: `boolean`):

Directors, LLP members, and secretaries. Adds the officers-psc charge in enrich mode when the register responds. Required to detect appointments and resignations in monitor mode.

## `includePsc` (type: `boolean`):

Persons with significant control. Protected (super-secure) PSCs are flagged without personal details. Shares the officers-psc charge with officers.

## `includeCharges` (type: `boolean`):

Mortgage charge register. Also fetched when the profile has has_charges or links.charges, even if this is off. Included in the company-enriched price.

## `includeFilings` (type: `boolean`):

Latest filing-history page (25 items). Accounts and confirmation-statement dates and overdue flags come from the profile either way.

## `includeInsolvency` (type: `boolean`):

When off, the insolvency endpoint is still called if the profile shows has_insolvency_history, has_been_liquidated, links.insolvency, or an insolvency status.

## `gazetteNotices` (type: `string`):

The Gazette public notice feed needs no API key. auto searches only when Companies House already shows insolvency, liquidation, or a strike-off proposal. always searches every company. off skips The Gazette. Fair use is 5 requests per 10 seconds.

## `companiesHouseApiKey` (type: `string`):

Optional. Overrides the COMPANIES_HOUSE_API_KEY secret for this run. Sent as the HTTP Basic username. Leave blank to use the Actor secret.

## `snapshotStoreName` (type: `string`):

Named key-value store for monitor snapshots. Persists across runs on the Apify account that executes the Actor.

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

Caps how many input items are processed in one run.

## `activeOfficersOnly` (type: `boolean`):

Hide resigned officers from the output list. Monitor diffs still use the full fetched page.

## Actor input object example

```json
{
  "companies": [
    "00445790"
  ],
  "mode": "enrich",
  "includeOfficers": true,
  "includePsc": true,
  "includeCharges": false,
  "includeFilings": false,
  "includeInsolvency": false,
  "gazetteNotices": "auto",
  "snapshotStoreName": "uk-company-monitor",
  "maxCompanies": 1000,
  "activeOfficersOnly": false
}
```

# Actor output Schema

## `results` (type: `string`):

Found companies, monitor changes, invalid input, and request errors. Not-found and invalid rows are not charged.

## `summary` (type: `string`):

Counts for found, not found, baselines, and changes.

# 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("datacraft-studio/uk-company-enrichment").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("datacraft-studio/uk-company-enrichment").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 datacraft-studio/uk-company-enrichment --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,datacraft-studio/uk-company-enrichment"
        }
    }
}
```

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/tM7A2MKgSLKfMx0ro/builds/kP7k3cPG5fg7f0RV6/openapi.json
