# UK Contracts Finder Scraper - Tenders & Awards (`dataio/uk-public-contract-awards`) Actor

Scrape UK public contracts from Contracts Finder, one row per supplier: who won, for how much, from which buyer, the buyer's named contact, and the days until the contract expires, which tells you what goes back to tender soon. Filter by keyword, category, date.

- **URL**: https://apify.com/dataio/uk-public-contract-awards.md
- **Developed by:** [Tom Awake](https://apify.com/dataio) (community)
- **Categories:** Lead generation, MCP servers
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 contract awards

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

### What does UK Contracts Finder Scraper do?

Who won which UK public contract, for how much, from which buyer — and how
many days until that contract expires.

Contracts Finder publishes UK public procurement in OCDS format. A notice
nests a `tender`, an array of `awards`, and a separate `parties` array
where the supplier's address and the buyer's contact live. Nothing is
joined, and the supplier's address arrives as a single line-broken blob.

This Actor returns **one row per supplier per award**, joined, with the
things the source leaves implicit.

No API key. No account.

***

### The four things this does that the source does not

**1. One row per supplier.**

An award can name sixteen suppliers. The source stacks them in an array;
we explode them, so every row is one contactable party with its own share
of the record. Measured: 319 supplier rows out of 300 awards.

**2. The `parties` join — 100%, no orphans.**

`awards[].suppliers` carries only an id and a name. The address, the
company size, and the buyer's named contact all live in a separate
`parties` array. We join them. Measured on 300 recent awards: **every
supplier resolved, zero orphans**.

That join is what produces the buyer contact — **name, email and phone on
100% of notices**. It is the most valuable field here and it is not in the
award object at all.

**3. Days until the contract ends.**

This is the sales signal: a contract expiring in six months is a
procurement about to reopen, and the incumbent is named. The source gives
an end date, never a countdown. Measured on recent awards, 73 of 305
contracts expire within 180 days.

**4. The supplier's postcode, parsed out of the address blob.**

`postalCode` is populated on **0%** of supplier parties. The postcode is
nevertheless there, buried in `streetAddress` between line breaks:
`"IW HOUSE\n QUARRY ROAD\n CHIPPING SODBURY\n BS37 6JL"`. We extract it
and normalise the whitespace — **88.7% coverage**.

***

### Getting past the 300-result ceiling

The API's cursor pagination stops after three pages. It simply stops
returning a `next` link beyond 300 notices — about four days of
publications — no matter what you ask for.

This Actor walks backwards through time in date windows, sizing each
window to the density it actually finds: narrower where notices are dense,
wider where they are sparse. When a window hits the 300 ceiling it resumes
from the oldest notice actually returned, rather than stepping over the
ones it could not see.

Filters are applied *during* pagination, not after it. Asking for 300 rows
with a selective filter returns 300 rows — an earlier version returned 44,
because the loop counted rows before filtering.

***

### Field coverage

Measured on a run of 300 awards with a 180-day expiry filter, and on 305
unfiltered awards.

| Field | Coverage |
|---|---|
| `supplierName` | 100% |
| `buyerName`, `buyerContactEmail`, `buyerPostcode` | 100% |
| `contractEndDate`, `daysUntilContractEnd`, `contractDays` | 100% |
| `cpvCode`, `cpvDescription` | 100% |
| `awardValue` | 96.7% |
| `supplierPostcode` | 88.7% |
| `supplierScale` (large / sme) | 83.3% |
| `daysToPublication` | 79.0% |
| `buyerContactPhone` | 47.5% |
| `supplierCompanyNumber` | 37.7% |

Re-measured 17 September 2026 on a 500-row run: `daysUntilContractEnd`
was present on 500 rows out of 500 and agreed with `contractEndDate` on
**all 500** — zero discrepancies. Median 74 days; 289 of the 500
contracts expire within 90 days.

`supplierCompanyNumber` is the Companies House number, extracted from ids
of the form `GB-COH-06472932`. The other ids (`GB-CFS-…`) are internal to
Contracts Finder and resolve to nothing, so that field is deliberately
empty rather than filled with an identifier that looks useful and is not.

#### There is no supplier city field, on purpose

Contracts Finder never populates `locality` for suppliers — the field
came back empty on 500 rows out of 500, so it was removed rather than
shipped as a column of blanks.

Deriving it from the address was tried and measured: 66% of addresses
yielded something, but the results included `England` 39 times,
`United Kingdom` 24 times, counties (`Staffordshire`, `Hampshire`) and
street names (`70 Chancery Lane`). A field that is right two thirds of
the time is worse than an absent one, because it gets filtered and
counted as though it were reliable.

Use `supplierPostcode` for geography: it is validated against the UK
postcode format and present on 86-89% of rows. The buyer side is
unaffected — `buyerCity` comes from a field the source does fill, and is
present on 100% of rows.

***

### What it is for

- **Selling to the public sector.** Filter `endingWithinDays` to 180, and
  each row is a contract about to be re-tendered — with the incumbent, the
  value, and the buyer's direct email.
- **Competitive intelligence.** Every award your competitors have won, by
  value, buyer and category.
- **Market sizing.** 96.7% of awards carry a value. Aggregate by CPV code
  or by buyer.
- **Supply chain and credit.** Suppliers with Companies House numbers can
  be joined to any UK company dataset.
- **SME policy research.** `smeSuitable` and `vcseSuitable` are the
  buyer's own flags; `supplierScale` is what the supplier actually is.
  Comparing the two is a question the source cannot answer on its own.

Three dataset views ship with the Actor: **Awards**, **Coming up for
renewal**, and **Suppliers**.

***

### Running this as a scheduled watch

Contract award notices appear continuously, and the value of this Actor
is time-sensitive: a contract expiring in six months is a procurement
about to reopen, and you want to know before your competitor does.

Set `publishedWithinDays` instead of a fixed date, then schedule the
Actor in Apify. The window moves with each run.

```json
{
  "stage": "award",
  "publishedWithinDays": 7,
  "cpvCodes": ["72000000"],
  "maxItems": 500
}
```

Measured 17 September 2026: `publishedWithinDays: 7` returned 308 rows
published between the 12th and the 16th; `30` returned 500 rows going
back to the 4th.

**Why not a fixed `publishedFrom` date?** A schedule replays the same
input every time. With a fixed date the window never moves, so every run
returns the same rows — and charges you for them again. The rolling
window is the only setting that makes a weekly watch honest.

***

### Limits

Stated plainly, because they affect what you can conclude.

- **Rate limiting is real.** The service returns HTTP 429 under sustained
  querying. Requests are paced and retried with a long backoff, so large
  runs take minutes rather than seconds. If a run is throttled part-way
  through, the rows already collected are returned rather than discarded.
- **Publication dates, not award dates.** The date windows filter on when
  a notice was published. `daysToPublication` shows the gap: median 14
  days, but up to 1,732 observed. A notice published this week can concern
  an award made years ago.
- **Negative publication delays are dropped.** 10 of 300 awards had a
  publication date earlier than the award date. That has no business
  meaning, so `daysToPublication` is left empty rather than negative.
- **Contract end dates are as filed.** Some are wrong, some contracts get
  extended without a new notice, and 12 of 305 had already ended.
  `contractExpired` flags those.
- **Values are what the buyer published.** Median £63,000, largest
  £135M observed, and some are £0. Framework call-offs sometimes carry the
  framework's value rather than the call-off's.
- **`sourceUrl` is for a browser.** The link points at the notice page on
  Contracts Finder, which rejects automated requests with HTTP 403. It
  opens normally in a browser; it will not fetch from a script. The `ocid`
  field is the canonical OCDS identifier if you need one.
- **UK only, GBP only.** Every value observed is in GBP. Scotland,
  Northern Ireland and Wales also run their own portals; contracts
  published only there do not appear here.

***

### How much does it cost?

You pay per contract award returned: **$0.006 each**, that is $6.00 per 1,000. There is no start fee, and subscription plans pay less per contract award.

The example input below asks for up to 500 contract awards, so it costs **$3.00 at most**.

If a run reaches the spending limit you set, the output stops at that limit and never goes past it. You are never charged for rows that were not delivered.

### Use UK Contracts Finder Scraper as an API

Call it from your own code with the Apify client, here in Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("DataIO/uk-public-contract-awards").call(run_input={'stage': 'award', 'endingWithinDays': 180, 'maxItems': 500})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)
```

It also works from JavaScript, Make, Zapier, n8n, and from AI agents through the Apify MCP server.

### Other actors you might like

- [TED Tenders Scraper](https://apify.com/DataIO/ted-eu-tenders-procurement): EU Public Procurement
- [USAspending Scraper](https://apify.com/DataIO/usaspending-federal-awards): Federal Contract Awards
- [Federal Register Scraper](https://apify.com/DataIO/federal-register-comment-deadlines): Comment Deadlines

### FAQ

#### Is it legal to use this data?

The actor reads public data from its official source, without logging in and without bypassing any access control. What you do with the data, for example contacting people listed in it, is your responsibility under the laws that apply to you, such as GDPR in Europe.

#### Can I run it on a schedule?

Yes. See *Running this as a scheduled watch* above: use the rolling-window setting so each run returns only what is new.

#### Can AI agents use it?

Yes. It is available through the Apify MCP server, and every input field is described in its input schema, so an agent can call it directly.

### Source

[Contracts Finder](https://www.contractsfinder.service.gov.uk), published
by the Cabinet Office under the Open Government Licence v3.0, in OCDS 1.1.
Public, no authentication. This Actor is not affiliated with the Cabinet
Office.

# Actor input Schema

## `stage` (type: `string`):

Awards name the winning supplier and the amount paid. Tenders are opportunities still open to bid on.

## `keyword` (type: `string`):

Searches the notice title and description, for example software, catering, scaffolding.

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

EU procurement classification, for example 72000000 for IT services, 45000000 for construction. Leave empty for all.

## `category` (type: `string`):

Services, goods or works. Measured split on recent awards: 72% services, 14% goods, 13% works.

## `publishedWithinDays` (type: `integer`):

Rolling window, for a scheduled watch: 7 returns notices published in the last seven days. Unlike a fixed date, this moves on its own, so a weekly schedule returns only what is new instead of re-charging you for the same rows. Overrides Published from.

## `publishedFrom` (type: `string`):

YYYY-MM-DD. Also makes the query much faster than an unfiltered search.

## `publishedTo` (type: `string`):

YYYY-MM-DD.

## `minValue` (type: `integer`):

Filters at the source. Median award value is around £63,000, so £100,000 already removes most of the low-value routine.

## `endingWithinDays` (type: `integer`):

The re-tender signal: a contract expiring soon is a procurement about to reopen. 180 is a typical sales horizon. Excludes contracts that have already ended.

## `smeSuitableOnly` (type: `boolean`):

Keeps only contracts the buyer flagged as suitable for small and medium enterprises — 64% of recent awards.

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

One row per supplier per award. Highest-value awards are kept first, so truncation never removes the largest contracts.

## Actor input object example

```json
{
  "stage": "award",
  "category": "all",
  "publishedWithinDays": 7,
  "smeSuitableOnly": false,
  "maxItems": 1000
}
```

# Actor output Schema

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

Every row produced by the run, in JSON.

# 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 = {
    "publishedWithinDays": 7,
    "maxItems": 1000
};

// Run the Actor and wait for it to finish
const run = await client.actor("dataio/uk-public-contract-awards").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 = {
    "publishedWithinDays": 7,
    "maxItems": 1000,
}

# Run the Actor and wait for it to finish
run = client.actor("dataio/uk-public-contract-awards").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 '{
  "publishedWithinDays": 7,
  "maxItems": 1000
}' |
apify call dataio/uk-public-contract-awards --silent --output-dataset

```

## MCP server setup

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

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/QEX2jwFxCIjU0DmtP/builds/i8v8JduFNGzSFUMku/openapi.json
