# UK Public Tenders Scraper - FTS + Contracts Finder (`knotty_mistveil/uk-public-tenders`) Actor

UK tenders, contract awards and pipeline notices from the official Find a Tender and Contracts Finder OCDS APIs: buyer contacts, CPV, value, deadline.

- **URL**: https://apify.com/knotty\_mistveil/uk-public-tenders.md
- **Developed by:** [Jason](https://apify.com/knotty_mistveil) (community)
- **Categories:** Business, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$3.00 / 1,000 notice returneds

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?

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

## UK Public Tenders Scraper - FTS + Contracts Finder

Pulls UK public procurement notices from the two official government services, **Find a Tender** (above-threshold and Procurement Act 2023 notices for the whole UK) and **Contracts Finder** (below-threshold and legacy notices, mainly England), through their public OCDS APIs. No API key, no browser automation, no login. You get one flat JSON record per notice release with the buyer and its contact email, CPV codes, estimated value, submission deadline, award suppliers and value, and a link to the notice page, filtered by date window, stage (tender, award, planning), keyword, CPV prefix, region and minimum value. Both feeds are read newest-first and merged into one de-duplicated stream of new UK tenders, awards and pipeline notices. The two services together publish roughly 500 to 620 releases per weekday, so set `maxItems` to match the window you ask for - see [Limits and freshness](#limits-and-freshness).

### What you get

Every record contains:

- `id` - `<source>:<ocid>:<release id>`, `source` (`find-a-tender` or `contracts-finder`), `url` - the public notice page, `ocid`, `releaseId` (Find a Tender notice number such as `084162-2026`, or the Contracts Finder notice GUID plus sequence)
- `stage` - the OCDS release tag, `stageGroup` (`tender`, `award`, `planning`, or `other` for the rare `implementation` release), `tags` (the raw array). Tags seen across 1,695 consecutive releases (2-4 September 2026): `award` 1,108, `tender` 261, `planning` 130, `awardUpdate` 69, `tenderUpdate` 62, `planningUpdate` 32, `tenderCancellation` 17, `contractAmendment` 6, `contractTermination` 5, `tenderAmendment` 4, `implementation` 1
- `title`, `description` (first 5,000 characters), `status` (`active`, `complete`, `planning`, `cancelled`, ...), `language`
- `buyerName`, `buyerId` (Companies House, charity or PPON identifier when published), `buyerRegion` (NUTS/ITL code such as `UKG33`, Find a Tender only), `buyerRegionName` (`West Midlands`, `Scotland`, ...), `buyerLocality`, `buyerPostcode`, `buyerCountry`, `buyerEmail`, `buyerPhone`, `buyerContactName`, `buyerWebsite`
- `deliveryRegions` - where the contract is performed (NUTS codes on Find a Tender, region names on Contracts Finder)
- `publishedDate` - release date, ISO 8601 with UK offset; `deadline` - tender submission deadline (`tenderPeriod.endDate`), or the expression-of-interest / early-engagement due date for planning notices
- `valueAmount`, `valueCurrency` - the buyer's estimated value (net amount when both net and gross are published, else gross; sum of lot values when no total is given; the award total when no estimate exists)
- `cpvCodes` (array, de-duplicated across lots and awards), `cpvLabels` (matching descriptions), `mainCategory` (`goods`, `services`, `works`)
- `procurementMethod` (`open`, `selective`, `limited`, `direct`), `procurementMethodDetails` (`Open procedure`, `Call-off from a framework agreement`, ...), `aboveThreshold`, `suitableForSme`, `suitableForVcse`, `lotCount`
- `awardSuppliers` (array of supplier names), `awardValue`, `awardCurrency`, `awardDate`, `contractStartDate`, `contractEndDate`
- `fetchedAt` - ISO timestamp of the run

The dataset has two views: **Overview** (title, buyer, region, CPV, deadline, value, suppliers, link) and **Buyer contacts** (contact name, email, phone, town, postcode, website).

### Input

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `sources` | string\[] | both | `find-a-tender`, `contracts-finder`, or both. |
| `daysBack` | integer | `3` | Look-back window in days ending now. Find a Tender is filtered on the notice's last update time, Contracts Finder on its publication time. Ignored when `dateFrom`/`dateTo` are set. |
| `dateFrom` | string | - | Start of the window, `YYYY-MM-DD` (00:00 UTC). |
| `dateTo` | string | now | End of the window, `YYYY-MM-DD` (23:59:59 UTC). |
| `keywords` | string\[] | none | Case-insensitive terms matched against title, description and buyer name; any match keeps the record. |
| `stages` | string\[] | all | `tender` (opportunities, including updates and cancellations), `award` (awards and contract changes), `planning` (pipeline and early engagement). A raw OCDS tag such as `tenderUpdate` is also accepted. |
| `cpvPrefixes` | string\[] | all | CPV codes or prefixes, 2 to 8 digits. `72` = IT services, `45` = construction, `33` = medical, `48` = software, `79` = business services, `85` = health and social care. |
| `regions` | string\[] | all | Case-insensitive substrings matched against the buyer's region code and name, town, postcode, country **and the contract's delivery regions**, plus the UK nation implied by any NUTS code: `"West Midlands"`, `"London"`, `"Scotland"`, `"England"`, `"Manchester"`, `"UKG"`. This is a *coverage* filter, not a buyer-address filter - see the note below the table. |
| `minValue` | integer | `0` | Keep only notices whose estimated value (or award value when no estimate exists) is at least this amount, in the notice currency (GBP). Notices with no value are dropped when set. |
| `maxItems` | integer | `500` | Stop after this many **kept** notices, newest first across both sources. Anything smaller than the window's volume drops the oldest notices in that window - size it to the window (see [Limits and freshness](#limits-and-freshness)). |

Example - IT and software tenders from the last week, England only, worth at least GBP 100,000:

```json
{
  "daysBack": 7,
  "stages": ["tender"],
  "cpvPrefixes": ["72", "48"],
  "regions": ["England"],
  "minValue": 100000,
  "maxItems": 1000
}
```

Example - the complete unfiltered feed, scheduled daily (the overlapping window absorbs publication lag, and 2,000 comfortably covers the roughly 1,000-1,250 releases two weekdays produce):

```json
{ "daysBack": 2, "maxItems": 2000 }
```

Bad CPV codes, unknown sources or stages, negative values and malformed dates fail fast: the run stops with the validation message as its status before any request is made.

**How `regions` matches.** A notice is kept when the term appears anywhere in the buyer's own address *or* in the contract's delivery area. That is deliberate - most users want "work I could bid for in Scotland", and a Coventry buyer running a UK-wide framework is genuinely relevant to a London supplier - but it means `regions: ["London"]` also returns buyers based elsewhere whose contract is delivered UK-wide. If you want the buyer's own location only, leave `regions` empty and filter the output on `buyerRegionName`, `buyerLocality` or `buyerPostcode` (Find a Tender publishes a NUTS code for every buyer; Contracts Finder publishes the town and postcode).

### Output example

A real Find a Tender record from a live run (2026-09-07), description shortened:

```json
{
  "id": "find-a-tender:ocds-h6vhtk-06bd70:084162-2026",
  "source": "find-a-tender",
  "url": "https://www.find-tender.service.gov.uk/Notice/084162-2026",
  "ocid": "ocds-h6vhtk-06bd70",
  "releaseId": "084162-2026",
  "stage": "tender",
  "stageGroup": "tender",
  "tags": ["tender"],
  "title": "Private Medical Insurance",
  "description": "Orbit is seeking a suitable supplier to provide Private Medical Insurance (PMI) as part of Orbit's employee benefits package to its employees. The awarded supplier will be expected to deliver a high-quality, cost-effective and accessible scheme for eligible employees ...",
  "buyerName": "ORBIT GROUP LIMITED",
  "buyerId": "GB-COH-IP28503R",
  "buyerRegion": "UKG33",
  "buyerRegionName": "West Midlands",
  "buyerLocality": "Coventry",
  "buyerPostcode": "CV3 2SU",
  "buyerCountry": "United Kingdom",
  "buyerEmail": "group.tenders@orbit.org.uk",
  "buyerPhone": null,
  "buyerContactName": null,
  "buyerWebsite": "https://www.orbit.org.uk",
  "deliveryRegions": ["UKC", "UKD", "UKE", "UKF", "UKG", "UKH", "UKI", "UKJ", "UKK"],
  "publishedDate": "2026-09-05T06:25:48+01:00",
  "deadline": "2026-09-22T10:00:00+01:00",
  "valueAmount": 2172000,
  "valueCurrency": "GBP",
  "cpvCodes": ["66512200"],
  "cpvLabels": ["Health insurance services"],
  "mainCategory": "services",
  "procurementMethod": "open",
  "procurementMethodDetails": "Open procedure",
  "aboveThreshold": true,
  "suitableForSme": true,
  "suitableForVcse": null,
  "status": "active",
  "lotCount": 1,
  "awardSuppliers": [],
  "awardValue": null,
  "awardCurrency": null,
  "awardDate": null,
  "contractStartDate": null,
  "contractEndDate": null,
  "language": "en",
  "fetchedAt": "2026-09-07T06:19:46.085Z"
}
```

A Contracts Finder award from the same run looks like this (fields common to both sources omitted):

```json
{
  "id": "contracts-finder:ocds-b5fd17-4a336c05-e4fd-4530-86b6-b037c17f1acd:883ce98b-3552-4303-bd02-5a00c8d66519-913088",
  "source": "contracts-finder",
  "url": "https://www.contractsfinder.service.gov.uk/Notice/883ce98b-3552-4303-bd02-5a00c8d66519",
  "stage": "award",
  "title": "National Roads Telecommunications Service 3 (NRTS3) Independent Assurance Review",
  "buyerName": "National Highways Limited",
  "buyerLocality": "Birmingham",
  "buyerPostcode": "B4 6GA",
  "buyerEmail": "James.mayer@nationalhighways.co.uk",
  "deliveryRegions": ["North East", "North West", "Yorkshire and the Humber", "East Midlands", "West Midlands", "East of England", "London", "South East", "South West"],
  "publishedDate": "2026-09-05T22:20:36+01:00",
  "valueAmount": 65000,
  "cpvCodes": ["79411000"],
  "procurementMethodDetails": "Call-off from a framework agreement",
  "suitableForSme": false,
  "awardSuppliers": ["Turner & Townsend Infrastructure Limited"],
  "awardValue": 65000,
  "awardCurrency": "GBP",
  "awardDate": "2026-08-07T00:00:00+01:00",
  "contractStartDate": "2026-08-10T00:00:00+01:00",
  "contractEndDate": "2026-09-18T23:59:59+01:00"
}
```

`valueAmount` and `awardValue` are `null` when the buyer published no figure. The run also writes a `SUMMARY` record to the key-value store with the number of releases scanned and kept per source, how many were dropped by each filter, and the resolved date window.

### Pricing

Pay per result: **US$0.003 per notice delivered** (US$3 per 1,000 notices). You are charged only for records pushed to the dataset; empty runs cost nothing. Runs on the Apify free plan are limited by the platform's free-tier usage allowance.

### Data source and licence

- Find a Tender OCDS API: `GET https://www.find-tender.service.gov.uk/api/1.0/ocdsReleasePackages?updatedFrom=...&updatedTo=...&limit=100`, published by the Cabinet Office. Documentation: <https://www.find-tender.service.gov.uk/apidocumentation>.
- Contracts Finder OCDS API: `GET https://www.contractsfinder.service.gov.uk/Published/Notices/OCDS/Search?publishedFrom=...&publishedTo=...&limit=100`, published by the Cabinet Office. Documentation: <https://www.contractsfinder.service.gov.uk/apidocumentation/home>.
- Both feeds are OCDS 1.1 release packages whose `license` field is the **Open Government Licence v3.0** (<http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/>), which permits commercial reuse with attribution. Attribute as "Contains public sector information licensed under the Open Government Licence v3.0" and link to the notice. Publication policy: <https://www.gov.uk/government/publications/open-contracting>.
- Buyer contact details are published by the buyers themselves in the notice; use them in line with UK GDPR / PECR and your own marketing rules.

### Limits and freshness

- **Volumes.** Measured on 1-4 September 2026 by exhausting each 24-hour window: Find a Tender published 457, 371, 436 and 471 releases on those four weekdays (**about 370 to 470 per weekday**), Contracts Finder 107, 129, 138 and 148 (**about 105 to 150**) - together **roughly 500 to 620 releases per weekday**. Weekends are far quieter. Find a Tender's mix is about two thirds awards (58-68% across those days), a quarter tender notices (21-27%) and a tenth planning notices (10-15%); on Contracts Finder more than 9 in 10 releases are awards.
- **Size `maxItems` to the window - the defaults are a sample, not a complete archive.** `maxItems` counts records *kept after filtering*, so a keyword, CPV, region or value filter normally keeps a run well under the default 500. An **unfiltered** run is different: three working days hold about 1,700 releases (2-4 September 2026 returned exactly 1,695), so the default `maxItems: 500` returns the newest 500 and stops, logging `limitReached: true` (also recorded in the `SUMMARY` key-value record). Because the merged stream is newest-first, that truncation drops the **oldest** notices in the window. For a gap-free unfiltered feed, schedule `{"daysBack": 2, "maxItems": 2000}` daily.
- Notices appear in the APIs shortly after publication and both services publish seven days a week (weekends are quiet). The Find a Tender window is on update time, so amended notices come round again as `tenderUpdate` / `awardUpdate` releases with a new `releaseId`; a daily run with `daysBack: 2` overlaps enough to absorb any publication lag.
- Stage filtering is done locally on the OCDS `tag`. The APIs' own `stages` parameter is deliberately not used: in testing it dropped every Procurement Act 2023 (UK4) tender notice on Find a Tender (for 2026-09-03 it returned 3 releases where tag-based filtering finds about 100).
- **Rate limit and run time.** Pages are 100 releases (the APIs' maximum) and the Actor follows `links.next` until the window is exhausted or `maxItems` is reached. Find a Tender enforces an undocumented allowance of about 12 requests per client that refills only a couple of requests a minute; beyond it the API answers `429 Too Many Requests` with the body "Rate limit of 12 exceeded" and `Retry-After: 120` (measured 7 September 2026). The Actor spaces Find a Tender pages 5 seconds apart, so a default run (about 6 pages, 500 notices, roughly 35 seconds) never touches the limit; a run of several thousand notices will hit it once or twice and waits out the two-minute `Retry-After` automatically instead of failing. Both services also get retries with exponential back-off on 429 and 5xx.
- **Window edges.** Both services read the offset-less timestamps their docs specify as UK local time, and Find a Tender ignores an explicit `Z` or `+01:00` offset, so the Actor sends the London clock reading of the instant you asked for. `daysBack` therefore really does end at the moment the run starts (no missing final hour during British Summer Time), and `dateFrom`/`dateTo` are exact UTC midnights.
- Each OCDS release is one record. The same procurement (`ocid`) can appear several times over its life (planning, tender, update, award); use `ocid` to group them.
- Dates are returned as the source publishes them (ISO 8601 with the UK offset). Values are not converted between currencies; virtually all are GBP.
- Contracts Finder buyer addresses carry no region code, so `buyerRegion` / `buyerRegionName` are `null` there; the `regions` filter still works through the buyer's town and the delivery regions.

### Use cases

- Daily lead feed of new UK tenders in your CPV codes and regions, with the buyer's procurement email for bid/no-bid outreach.
- Competitor and incumbent tracking: award notices show which suppliers win what, at what value, and when the contract ends (`contractEndDate`) so you can prepare for the re-tender.
- Market research and compliance: archive every notice for a sector or buyer, or rebuild a history with `dateFrom`/`dateTo`.

### Support

Open an issue on the Actor's Issues tab on Apify Store. Replies within 14 days.

# Actor input Schema

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

Which official feeds to read. Find a Tender carries above-threshold and Procurement Act 2023 notices; Contracts Finder carries below-threshold and legacy notices from England and other UK buyers. Default: both.

## `daysBack` (type: `integer`):

Look-back window in days, ending now. Find a Tender is filtered on the notice's last update time, Contracts Finder on its publication time. Ignored when Date from / Date to are set.

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

Start of the window, YYYY-MM-DD (inclusive, from 00:00 UTC). Overrides Days back.

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

End of the window, YYYY-MM-DD (inclusive, to 23:59:59 UTC). Defaults to now.

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

Case-insensitive terms matched against the notice title, description and buyer name; a record is kept when any term matches. Empty = no keyword filter.

## `stages` (type: `array`):

Keep only these stages: tender (new opportunities, including updates and cancellations), award (contract awards, including updates), planning (pipeline and early-engagement notices). Empty = all stages.

## `cpvPrefixes` (type: `array`):

Common Procurement Vocabulary codes or prefixes, 2 to 8 digits. 72 matches every IT-services code; 45 = construction, 33 = medical, 48 = software, 79 = business services, 85 = health and social care. A notice is kept when any of its CPV codes starts with any prefix. Empty = all.

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

WARNING: this matches the contract's delivery area as well as the buyer's own address, so a UK-wide contract matches every region and you will see out-of-area buyers. Case-insensitive substrings matched against the buyer's region, town, postcode and country, the notice's delivery regions, and the UK nation implied by any NUTS code. Use names ("West Midlands", "London", "Scotland", "England", "Manchester") or NUTS/ITL codes ("UKG", "UKM"). For buyer location only, filter the output on buyerRegionName / buyerLocality / buyerPostcode. Empty = all regions.

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

Keep only notices whose estimated value (or award value when no estimate was published) is at least this amount in the notice currency, normally GBP. Notices without any value are dropped when this is set. 0 = no minimum.

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

Stop after this many notices. You are charged per notice delivered. Newest notices come first: the two feeds are merged on publication date, so a small value returns whichever service published most recently rather than a fixed split, and anything smaller than the window's volume drops the OLDEST notices. Both services together publish roughly 500-620 releases per weekday, so a gap-free 3-day window needs about 2000.

## Actor input object example

```json
{
  "sources": [
    "find-a-tender",
    "contracts-finder"
  ],
  "daysBack": 3,
  "keywords": [],
  "stages": [
    "tender",
    "award",
    "planning"
  ],
  "cpvPrefixes": [],
  "regions": [],
  "minValue": 0,
  "maxItems": 500
}
```

# Actor output Schema

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

All notices returned by the run, one JSON object each. Append ?format=csv or ?format=xlsx to download a spreadsheet.

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

Counts, the date window used, source freshness and any per-source warnings, written at the end of the run.

# 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 = {
    "sources": [
        "find-a-tender",
        "contracts-finder"
    ],
    "daysBack": 3,
    "keywords": [],
    "stages": [
        "tender",
        "award",
        "planning"
    ],
    "cpvPrefixes": [],
    "regions": [],
    "maxItems": 500
};

// Run the Actor and wait for it to finish
const run = await client.actor("knotty_mistveil/uk-public-tenders").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 = {
    "sources": [
        "find-a-tender",
        "contracts-finder",
    ],
    "daysBack": 3,
    "keywords": [],
    "stages": [
        "tender",
        "award",
        "planning",
    ],
    "cpvPrefixes": [],
    "regions": [],
    "maxItems": 500,
}

# Run the Actor and wait for it to finish
run = client.actor("knotty_mistveil/uk-public-tenders").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 '{
  "sources": [
    "find-a-tender",
    "contracts-finder"
  ],
  "daysBack": 3,
  "keywords": [],
  "stages": [
    "tender",
    "award",
    "planning"
  ],
  "cpvPrefixes": [],
  "regions": [],
  "maxItems": 500
}' |
apify call knotty_mistveil/uk-public-tenders --silent --output-dataset

```

## MCP server setup

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

```

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/LAbCH3nQMvQbpMoRk/builds/4XCx0chF4W7F0kK9p/openapi.json
