# UK Public Contract Awards + Supplier Health (`oval_opera_myg/uk-contract-awards-supplier-health`) Actor

UK public contract awards from Find a Tender and Contracts Finder, with every supplier matched to Companies House (confidence + evidence) and register signals to review.

- **URL**: https://apify.com/oval_opera_myg/uk-contract-awards-supplier-health.md
- **Developed by:** [Joe Sande](https://apify.com/oval_opera_myg) (community)
- **Categories:** Business, Lead generation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 award records

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 Public Contract Awards + Supplier Health

**Who is winning UK public contracts, and what does the Companies House register say about them?** This Actor pulls award notices from the two official UK procurement portals, **Find a Tender** and **Contracts Finder**. It matches every supplier to **Companies House**, gives each match a confidence score and the evidence behind it, and adds **register signals** about the company the notice named or numbered: register status (liquidation, dissolved, proposal to strike off), overdue accounts, a very young company with a large award, dormant accounts. Each row gets a **review priority** saying how soon it is worth checking the register.

> **Register signals are indicators to investigate, not findings.** They describe a company's public register record, not its finances or performance, and they depend on the match being right. **Do not rely on this output to decline a supplier, terminate a contract, or make a credit decision without checking the register** (and the original notice) yourself.

It is built for:

- **bid and sales teams** doing competitor and market intelligence;
- **public-sector buyers** deciding which suppliers to look up on the register first;
- **B2B teams** who want leads on recently funded suppliers.

### What makes it different

- **Award data joined to company data.** Each award × supplier row includes the matched company number, register status, age, SIC codes, accounts type, overdue filings and outstanding charges.
- **Honest matching.** Every match has a `matchConfidence` (0–1), a `matchMethod` and `matchEvidence`: the candidate company, name similarity, postcode agreement and any review reasons. Matches below your threshold come back as `review` or `unmatched` and are **not charged** as enriched.
- **No forced matches.** Public bodies, charities, overseas firms and sole traders are labelled with `entityType` / `matchStatus: not_a_company` instead of being forced onto a lookalike company.
- **Confidence next to every signal.** Each flag repeats the `matchConfidence`, `matchMethod`, `companyNumber` and data date it depends on, and every row has a plain-English `riskBasis`.
- **No high priority on a guess.** `riskLevel` / `reviewPriority` can only be `high` when the match used a company number published in the notice or scored ≥ 0.95. Name-only matches below 0.95 are capped at `medium`.

### Accuracy (measured, not promised)

Tested on 30 days of notices (5,190 awards, 3,979 unique suppliers, 21 Sep–21 Oct 2026) against the Companies House snapshot of 1 Oct 2026:

| Measure | Result |
|---|---|
| Company-type suppliers matched at ≥ 0.85 | **94.7%** |
| All suppliers matched at ≥ 0.85 | 77.1% (many suppliers are not companies) |
| Weighted precision of matches at ≥ 0.85 | about **98%**: posterior median **98.3%**, 95% interval **95.7%–99.4%** |

How the precision figure was made, and why it is optimistic:

- **AI-assisted labels, not a hand check.** A stratified sample of 364 matches (every match that drove a risk flag, plus a sample of the rest) was labelled right/wrong by the AI assistant that built the Actor, by comparing each notice with the Companies House record. The labels were not made by a person. At least one label was later found to be wrong (*Made by Allen* was labelled a wrong match and is in fact correct).
- **In-sample.** The matching rules were tuned on those same 364 labels, so the figure is not an independent test. Expect real-world precision to be somewhat lower.
- The stratum-weighted point estimate is 99.7%; we quote the posterior median and interval because the sample has few errors and the point estimate overstates certainty.

Known residual errors:

- brand names that match an unrelated or dormant company;
- buyer-supplied company numbers that point to a related company.

### Input examples

Last 7 days of cleaning awards over £25k in the North West:

```json
{ "mode": "awards", "lookbackDays": 7, "keywords": ["cleaning"], "cpvCodes": ["90910000"], "regions": ["North West"], "minValueGBP": 25000, "maxItems": 500 }
```

Health check of specific companies, plus the awards they won in the last 30 days:

```json
{ "mode": "suppliers", "supplierCompanyNumbers": ["02183240", "SC188426"], "lookbackDays": 30 }
```

| Field | Default | Notes |
|---|---|---|
| `mode` | `awards` | `awards` or `suppliers` |
| `sources` | both | `find-a-tender`, `contracts-finder` |
| `dateFrom` / `dateTo` / `lookbackDays` | last 30 days | Window of notice publication (Contracts Finder) or update (Find a Tender). `dateFrom` after `dateTo` fails the run with a clear message |
| `keywords`, `cpvCodes`, `buyerNameContains`, `regions`, `minValueGBP`, `maxValueGBP` | none | `cpvCodes` match on prefix |
| `supplierCompanyNumbers` | none | Suppliers mode only; up to 500 |
| `scanAwards` | `true` | Suppliers mode: also scan notices in the window for awards to these companies. Turn off for a register-only check |
| `enrichSuppliers` | `true` | |
| `minMatchConfidence` | `0.85` | Or `0.9` / `0.95` |
| `includeUnmatched` | `true` | |
| `includeOfficerData` | `false` | **Personal data, off by default.** When on: an aggregate officer-churn count, and the names of officers shared with possible successor companies (`successorCandidates[].sharedOfficerNames`). When off, rows only say whether officers are shared and how many (`sharedOfficer`, `sharedOfficerCount`). Needs the live API |
| `useLiveCompaniesHouse` | `auto` | `auto`: live API only where the monthly index is not enough (see below). `always`: refresh every match (slower; **only with your own `companiesHouseApiKey`**, otherwise treated as `auto` with a warning). `never`: index only |
| `companiesHouseApiKey` | none | Optional, **secret** (stored encrypted, never logged). Your own free Companies House key; used instead of the Actor's shared key |
| `maxItems` | `1000` | |

#### When the live Companies House API is used

Most suppliers are matched and profiled from the monthly Companies House bulk snapshot. The live API is called only for suppliers where it adds something:

- the supplier is **unmatched** or in the **review** bucket (for example a dissolved company missing from the snapshot, or a weak name match);
- the matched company's snapshot record is **not Active** (liquidation, administration, proposal to strike off, …);
- its **accounts or confirmation statement are past due** in the snapshot, so the live record may already show them filed.

Each supplier carries `liveCheck.status` (`checked`, `not_needed`, `skipped_rate_limited` or `unavailable`) and `liveCheck.reasons`. In suppliers mode, every requested company is looked up live.

**Rate limits.** Companies House allows 600 requests per 5 minutes per key. The Actor keeps under 500 per 5 minutes per run.

- **Shared key:** the Actor's built-in key is shared by all users. If Companies House returns HTTP 429 on it, the run does not stall. It stops live checks and finishes from the monthly index. You will see:
  - a warning in the run's `OUTPUT` summary (`warnings`, `companiesHouseApi.disabled`);
  - `liveCheck.status: "skipped_rate_limited"` on the affected rows.
- **Your own key:** supply `companiesHouseApiKey` and your run gets its own budget. Free keys come from https://developer.company-information.service.gov.uk/ (create an application, then a REST key). On a 429 with your key, the Actor backs off and retries as Companies House asks, honouring `Retry-After`.
- **Rejected key:** if your key is rejected (401), the run warns you and uses the shared key instead.

### Output (one row per award × supplier)

```json
{
  "source": "find-a-tender", "noticeUrl": "https://www.find-tender.service.gov.uk/Notice/099364-2026",
  "rowKey": "ocds-h6vhtk-05a82c:099364-2026-1:N:KITSUNGILTD|WV4 5HD",
  "title": "Recurrent Redistribution of Orthodontic UOAs in Northampton", "buyer": { "name": "NHS Northamptonshire Integrated Care Board" },
  "awardDate": "2026-10-16", "valueGBP": 3031914.5, "numberOfSuppliers": 1, "contractStart": null, "contractEnd": null,
  "cpvCodes": ["85131100", "85130000"], "regions": ["East Midlands"],
  "supplier": {
    "nameAsPublished": "KITSUNGI LTD", "entityType": "company", "matchStatus": "matched", "matchConfidence": 0.98, "matchMethod": "id_unschemed",
    "matchEvidence": { "candidateName": "KITSUNGI LTD", "nameSimilarity": 1.0, "identifierUsed": true, "postcodeAgreement": "different" },
    "companyNumber": "12392509", "status": "Active", "incorporated": "2020-01-08", "ageYears": 6.7, "sicCodes": ["86230"],
    "accountsType": "MICRO ENTITY", "accountsOverdue": false, "confirmationStatementOverdue": false, "outstandingCharges": 1,
    "companyDataAsOf": "2026-10-01", "companyDataSource": "companies-house-bulk-2026-10-01",
    "liveCheck": { "status": "not_needed", "reasons": null }
  },
  "riskFlags": [],
  "riskLevel": "low", "reviewPriority": "low",
  "riskBasis": "Register facts about the matched company (Companies House monthly snapshot, data as of 2026-10-01); match confidence 0.98 by a company number in the notice (no identifier scheme given). No register signals found.",
  "attribution": "Contains public sector information licensed under the Open Government Licence v3.0 (Find a Tender, Contracts Finder). Company data from Companies House.",
  "retrievedAt": "2026-10-21T17:43:57Z"
}
```

This micro-entity supplier is not flagged: the notice gives no contract period, so the award cannot be turned into a per-year value to compare with the £1m micro-entity turnover limit (earlier builds wrongly flagged it against the whole £3m value).

Row fields worth knowing:

- `riskLevel`: severity of the register signals on the row (`low`, `medium`, `high`, or `unknown` when no company was matched).
- `reviewPriority`: **how soon to check this supplier on the register**. Same scale as `riskLevel` (`not_assessed` instead of `unknown`). It ranks rows for human review; it is not a rating of the supplier.
- `riskBasis`: one sentence saying which company the signals are about, the data source and date, and the match behind them (and why a level was capped).
- `rowKey`: stable id of the row, used to avoid duplicate rows and charges if a run is restarted.

`matchStatus` takes one of these values:

- `matched`: confidence ≥ threshold;
- `review`: a candidate scored 0.80 to just under the threshold, shown in `matchEvidence`;
- `unmatched`;
- `not_a_company`.

### Register signals (risk flags)

These are **indicators to investigate, not findings**. They are about **the company the notice named or numbered**, as matched, and its **register status** on the date shown in `dataAsOf`. The thresholds are product choices. A flag with severity `high` only makes the row `high` priority when the match is identifier-based or ≥ 0.95 confidence.

| Flag | Rule |
|---|---|
| `DISSOLVED_OR_IN_LIQUIDATION` / `STATUS_NOT_ACTIVE` | Companies House status is not "Active" (liquidation, administration, proposal to strike off, voluntary arrangement, …) |
| `ACCOUNTS_OVERDUE` | Next accounts due date passed more than 30 days before the data date (or the API's `overdue` flag) |
| `CONFIRMATION_OVERDUE` | The same rule for the confirmation statement |
| `YOUNG_COMPANY_LARGE_AWARD` | Under 2 years old with a single-supplier award over £250k |
| `MICRO_ENTITY_HIGH_VALUE_AWARD` | Micro-entity accounts, single-supplier award, and the award's **value per year** (total ÷ contract years, from the notice's contract start and end dates) is over **£1m**, the micro-entity turnover limit for financial years from 6 April 2025. No flag when the notice gives no contract period. Contracts under a year are not scaled up |
| `DORMANT_COMPANY_ACCOUNTS` | Latest accounts filed as dormant. Often means the notice named a dormant group company. |
| `INSOLVENCY_HISTORY`, `HIGH_OFFICER_CHURN` | Only with the live Companies House API (officer churn = 4 or more changes in 12 months; a count only) |
| `NAMED_COMPANY_DISSOLVED_CHECK_CONTRACTING_ENTITY` (medium, row in `review`) | The company the notice named or numbered was dissolved before the award, **and** an active company is linked to it: a current officer in common plus another link, or the same registered postcode plus a distinctive name token. Typical causes are a re-incorporation or a group sibling, or a stale company number in the buyer's supplier record. The candidates and their evidence are in `matchEvidence.successorCandidates` (shared officers as a yes/no and a count; names only with `includeOfficerData`). A dissolved company with no linked active company keeps the high `DISSOLVED_OR_IN_LIQUIDATION` flag |
| `UNMATCHED_SUPPLIER` | Informational: no confident match |

Framework and dynamic purchasing system (DPS) awards list the whole framework value against every supplier. Value-based flags therefore use single-supplier awards only.

### Pricing (pay per event)

| Event | Price |
|---|---|
| Actor start | $0.00005 |
| `award-record` (each row returned) | $0.003 |
| `supplier-enriched` (only when the match is ≥ your threshold) | $0.010 |
| `supplier-profile` (suppliers mode, per company) | $0.02 |

Example: 100 award rows, 68 of them enriched, costs $0.98. The Actor stops cleanly at your maximum-charge limit. Each row and each event is charged at most once, even if the platform restarts or migrates the run: progress is checkpointed to the run's key-value store and rows already returned are skipped on restart.

### Limitations

- **Coverage depends on what buyers publish.** About 57% of suppliers in our sample had no company number of any kind (61.5% had no Companies House-schemed number), no postcode, or only a trading name.
- **Company data is mostly a snapshot.** It comes from the monthly Companies House snapshot, which covers live companies only. The live API fills in dissolved companies, insolvency history and same-day status only for the suppliers listed under "When the live Companies House API is used". `companyDataSource` and `companyDataAsOf` on each row tell you which source was used. With `useLiveCompaniesHouse: never`, or after a shared-key 429, everything comes from the snapshot.
- **Award history is limited to the date window you set.** There is no 24-month history yet.
- **Delivery regions are only as good as the notice.** Find a Tender NUTS/ITL codes are mapped to UK regions; Contracts Finder region names are used as published.
- **Name-based heuristics are imperfect.** For example, "Royal Mail" can be typed as a person's name. When a near candidate exists, such suppliers are returned as `review`.

### Data sources and licences

- **Find a Tender and Contracts Finder** (Cabinet Office) OCDS APIs. Both declare the **Open Government Licence v3.0** in every release package. Attribution is included in each row.
- **Companies House:**
  - the Free Company Data Product (bulk snapshot, free of charge);
  - optionally, the Companies House Public Data API, used under its developer terms with one key and within its rate limit.
- **No personal data by default.** Only company-level fields are output. Officer names appear only if you turn on `includeOfficerData` (shared officers between a dissolved company and a possible successor); otherwise officer information is reduced to yes/no flags and counts.

This Actor provides data, not legal, financial or procurement advice. Register signals are indicators to investigate, not findings: do not rely on this output to decline a supplier, terminate a contract, or make a credit decision without checking the Companies House register and the original notice.

### Use with AI agents

The Actor can be called through Apify's MCP server or API like any other Actor. The `review` dataset view puts `reviewPriority`, the signals and their basis first. To get only rows worth reviewing, filter on `reviewPriority` in `high`/`medium`.

# Actor input Schema

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

awards: one row per award x supplier. suppliers: one profile per company number in supplierCompanyNumbers, with the awards they won in the date window.

## `sources` (type: `array`):

Official OCDS APIs. No login or key needed.

## `dateFrom` (type: `string`):

Notices published (Contracts Finder) or updated (Find a Tender) on or after this date. Default: lookbackDays before dateTo.

## `dateTo` (type: `string`):

Default: now.

## `lookbackDays` (type: `integer`):

Used when dateFrom is empty.

## `keywords` (type: `array`):

Any keyword in the title or description (case-insensitive).

## `cpvCodes` (type: `array`):

Prefix match: 90910000 matches 9091xxxx.

## `buyerNameContains` (type: `array`):

Any of these substrings in the buyer name (case-insensitive).

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

UK region names, e.g. North West, London, Scotland. Find a Tender NUTS/ITL codes are mapped to these names.

## `minValueGBP` (type: `integer`):

Awards without a value are excluded when set.

## `maxValueGBP` (type: `integer`):

Awards without a value are excluded when set.

## `supplierCompanyNumbers` (type: `array`):

Up to 500 Companies House numbers, e.g. 02183240 or SC188426.

## `scanAwards` (type: `boolean`):

Scan notices in the date window for awards to these companies. Turn off for a company-only health check.

## `enrichSuppliers` (type: `boolean`):

Match each supplier to Companies House and add company-level health fields and flags.

## `minMatchConfidence` (type: `string`):

Below this, the supplier is returned as review/unmatched with the best candidate in matchEvidence, and is not charged as enriched.

## `includeUnmatched` (type: `boolean`):

Also return rows whose supplier is unmatched, in review, or not a company (labelled, not force-matched).

## `includeOfficerData` (type: `boolean`):

Off by default. When on: adds an aggregate count of officer appointments/resignations in the last 12 months, and adds the names of officers shared with possible successor companies (matchEvidence.successorCandidates\[].sharedOfficerNames). Personal data: only turn on if you have a lawful basis to process it. When off, output only says whether officers are shared and how many. Needs the live Companies House API.

## `useLiveCompaniesHouse` (type: `string`):

auto: the live Companies House API is called only for suppliers that are unmatched or in the review bucket, not in the monthly index (e.g. dissolved), or whose index record is not Active or has accounts / confirmation statement past due. Everyone else uses the monthly bulk index. always: refresh every matched supplier (many more requests; only with your own companiesHouseApiKey, otherwise treated as auto). never: index only. In suppliers mode each requested company is always looked up live when a key is available.

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

Optional. A free key from developer.company-information.service.gov.uk (REST API key). If supplied it is used instead of the Actor's shared key, so your run gets its own 600 requests / 5 min budget and is never cut back to index-only by other users' traffic. Stored encrypted; never logged or returned.

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

Maximum dataset items to return.

## Actor input object example

```json
{
  "mode": "awards",
  "sources": [
    "find-a-tender",
    "contracts-finder"
  ],
  "lookbackDays": 30,
  "scanAwards": true,
  "enrichSuppliers": true,
  "minMatchConfidence": "0.85",
  "includeUnmatched": true,
  "includeOfficerData": false,
  "useLiveCompaniesHouse": "auto",
  "maxItems": 1000
}
```

# Actor output Schema

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

All dataset rows: one row per award and supplier (awards mode) or one profile per supplier (suppliers mode), with the Companies House match and register signals.

## `review` (type: `string`):

The same rows in the review view: review priority, match confidence and register signals.

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

JSON summary of the run: mode, date window, item counts, charged events, source request stats, Companies House API stats and warnings.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("oval_opera_myg/uk-contract-awards-supplier-health").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("oval_opera_myg/uk-contract-awards-supplier-health").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 '{}' |
apify call oval_opera_myg/uk-contract-awards-supplier-health --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,oval_opera_myg/uk-contract-awards-supplier-health"
        }
    }
}
```

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/3Lb82YrjXMv2foQPx/builds/adRQV7jcVIhyoowCa/openapi.json
