# UK Public Tenders — Find a Tender + Contracts Finder (`seralifatih/uk-public-tenders`) Actor

One reliable feed of UK public procurement notices. Merges Find a Tender and Contracts Finder (the Procurement Act 2023 split) into a single schema, with CPV, value, region and keyword filters and a new-notices-only mode.

- **URL**: https://apify.com/seralifatih/uk-public-tenders.md
- **Developed by:** [Fatih İlhan](https://apify.com/seralifatih) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.10 / 1,000 results

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

## UK Public Tenders — Find a Tender + Contracts Finder

**One feed for UK public procurement notices.** Since the Procurement Act 2023 came into force (24 February 2025), UK tenders are split across two government systems: **Find a Tender** (the above-threshold service, now with the new Act's notice types) and **Contracts Finder** (below-threshold and many central-government notices). This Actor reads both official OCDS APIs and returns them in **a single, identical schema**: planning/pipeline notices, tenders, awards and contracts, with buyer, value, CPV codes, region, deadline and supplier. No API key, no scraping, no HTML parsing.

Filter by keyword, CPV code, value, region and notice type, or turn on **only-new mode** and put it on an Apify schedule to get a daily feed of fresh notices.

### Why it is built the way it is

- **Both sources, one schema.** The same 24 fields for every notice, whichever service it came from. Missing data is `null`, never guessed.
- **If one source is down, you still get the other.** A failing source is retried with exponential backoff and `Retry-After` handling; if it still fails, the other source's notices are returned, the run does not fail, and the failure is written to `RUN_SUMMARY`.
- **Complete, not "first page".** Every page is read until the feed is exhausted. A run that stops early says so in `RUN_SUMMARY` (and `onlyNew` will not skip what was not read).
- **Server-side filters that lose notices are not used.** Both APIs have a `stages` filter that silently drops notices (checked against the live feeds), so all filtering is done after fetching.
- **One record per call, even across the two services.** The same tender is often published on both with different ids and slightly different titles; those are detected (buyer + title + deadline) and returned once, listing both sources.
- **Pay only for results.** No placeholder rows, no error rows. Errors live in `RUN_SUMMARY`, not in your dataset.

### Example inputs

**IT and software opportunities from the last 24 hours (the default window; by default you get tender and planning notices)**

```json
{
  "cpvCodes": ["72"]
}
```

**Who won? Awarded contracts are opt-in**: add `award` (and `contract` for contract details) to `noticeTypes`

```json
{
  "cpvCodes": ["72"],
  "noticeTypes": ["award", "contract"],
  "publishedFrom": "2026-10-01"
}
```

To get open tenders, pipeline **and** awards together, list every type: `"noticeTypes": ["planning", "tender", "award", "contract", "implementation"]`. In the Console, pick them in the "Notice types" box.

**Everything from specific buyers** (case-insensitive, partial match on the buyer name)

```json
{
  "buyers": ["Ministry of Defence", "Transport for London"],
  "publishedFrom": "2026-09-01"
}
```

**Contracts worth £100k or more**

```json
{
  "minValue": 100000,
  "noticeTypes": ["tender", "award"]
}
```

**Daily new IT tenders (schedule this Actor to run every morning)**

```json
{
  "cpvCodes": ["72"],
  "onlyNew": true
}
```

Create an Apify **Schedule** (for example cron `0 7 * * *`) for this input. The first run returns the last 24 hours (or whatever `publishedFrom` you set); every later run returns only what was published since. State is stored in the named key-value store `uk-public-tenders-state` under your account.

### Input

All fields are optional.

| Field | Type | Meaning |
|---|---|---|
| `keywords` | string list | Title or description contains **any** of these (case-insensitive, from the start of a word: `cyber` matches `cybersecurity`). |
| `cpvCodes` | string list | CPV prefix match, e.g. `72` = IT services, `72212` = application software, `45` = construction. Checks the notice's main and additional CPV codes. |
| `publishedFrom` / `publishedTo` | string | `YYYY-MM-DD` or ISO-8601 datetime (UTC if no zone). A bare `publishedTo` date includes the whole day. Default: the last 24 hours. |
| `minValue` / `maxValue` | integer (GBP) | Keep notices whose value range overlaps this range. Notices that state no value are excluded when a value filter is set. |
| `noticeTypes` | list | `planning`, `tender`, `award`, `contract`, `implementation`. **Default: `tender` + `planning`**; awards and contracts are opt-in (see the example above). |
| `buyers` | string list | Buyer (contracting authority) name contains **any** of these texts: case-insensitive, partial match (`nhs`, `transport for london`, `council`). Notices with no buyer name never match. |
| `regions` | string list | UK region names or NUTS codes (`London`, `Scotland`, `UKI`, `UKD7`). Names and codes match each other (against both `region_code` and `region_name`); `England` covers all nine English regions. |
| `onlyNew` | boolean | Only notices not returned by an earlier run with the same filters (new notices and new versions of known notices). |
| `sources` | list | `fts`, `contracts_finder`. Default: both. |

To cap the number of results (and your spend), use the Apify **"Maximum charged results"** run option. The Actor returns at most that many notices, **newest** first, and reports in `RUN_SUMMARY.truncation` that it truncated and how many matches were left out.

### Output

One dataset item per procurement (`ocid`). If the same procurement appears several times in the window (tender, then award, then amendments), the latest matching version is returned.

| Field | Description |
|---|---|
| `notice_id` | Notice identifier (`097437-2026` on Find a Tender, a UUID on Contracts Finder). |
| `related_notice_ids` | Notice ids of same-source copies folded into this record (see "Known limitations"). Empty list when there are none. |
| `source` | List: `["fts"]`, `["contracts_finder"]`, or both when the same `ocid` is in both services. |
| `ocid` | Open Contracting process id; stable across the notices of one procurement. |
| `title` | Title exactly as published. |
| `title_clean` | The title without leading reference prefixes such as `CA18567 - `, `RFQ 2026/43 - `, `GB-Glasgow: ` or `UK4 Tender Notice: ` (the original is in `title`). |
| `description` | Notice text. On Contracts Finder, an appended "Additional information:" section is dropped when it only repeats the main description, and kept when it adds something. |
| `buyer_name`, `buyer_id` | Contracting authority and its identifier. |
| `notice_type` | `planning`, `tender`, `award`, `contract` or `implementation`. |
| `status` | Procurement status as published (`active`, `complete`, `planned`, ...). |
| `published_date` | UTC ISO-8601 publication time of the notice version. |
| `deadline_date` | Tender submission deadline (UTC ISO-8601). **`null` on award notices**: a closed deadline is not an action date for an award. |
| `tender_deadline_date` | The published submission deadline, kept on every notice type, including awards. |
| `value_min`, `value_max`, `currency` | Value as published (see "Known limitations"). |
| `cpv_codes` | All CPV codes on the notice, main code first. |
| `region_code` | List of NUTS codes, e.g. `["UKK14"]`. Contracts Finder region names are mapped to their NUTS1 code (`Scotland` -> `UKM`); non-UK codes such as `CY` are left as published. Empty list if unknown. |
| `region_name` | List of region names (NUTS1), e.g. `["South West"]`; `United Kingdom` for a nationwide `UK`; `England` appears here only (it has no NUTS1 code). Empty list if unknown. |
| `procedure_type` | For example "Open procedure", "Call-off from a framework agreement". |
| `award_supplier` | Awarded supplier name(s), if any (several are separated by `; `). |
| `url` | The notice page on Find a Tender / Contracts Finder, never an attachment/download link. On Find a Tender it is built from the notice id; on Contracts Finder it comes from the notice's own page link, so it is `null` in the rare case none is published. |
| `source_url` | The API record the data was read from. |
| `fetched_at` | When the Actor fetched it (UTC). |

#### Example record (real, from a live run)

```json
{
  "notice_id": "cab39ee0-c192-4fb2-9253-472214e58ca7",
  "related_notice_ids": [],
  "source": [
    "fts",
    "contracts_finder"
  ],
  "ocid": "ocds-b5fd17-4321ebe7-2ddb-47fa-81f9-fa095553b41b",
  "title": "CA18567 - RFQ 2026/43 - Development of a Dublin Belfast Economic Corridor Investment Land Guide",
  "title_clean": "Development of a Dublin Belfast Economic Corridor Investment Land Guide",
  "description": "The DBEC Partnership wishes to appoint a suitably qualified and experienced consultant to produce an Investment Land Guide that will strengthen the Dublin Belfast Economic Corridor's investment proposition.\n\nTo access this competition: \n\nRegistered:\nLogin to https://suppliers.multiquote.com and v...",
  "buyer_name": "Newry, Mourne and Down District Council (NMD)",
  "buyer_id": "GB-CFS-333373",
  "notice_type": "tender",
  "status": "active",
  "published_date": "2026-10-14T13:42:19Z",
  "deadline_date": "2026-10-30T12:00:00Z",
  "tender_deadline_date": "2026-10-30T12:00:00Z",
  "value_min": null,
  "value_max": 25000,
  "currency": "GBP",
  "cpv_codes": [
    "79411100",
    "72221000",
    "73210000"
  ],
  "region_code": [
    "UK"
  ],
  "region_name": [
    "United Kingdom"
  ],
  "procedure_type": "Open procedure",
  "award_supplier": null,
  "url": "https://www.contractsfinder.service.gov.uk/Notice/cab39ee0-c192-4fb2-9253-472214e58ca7",
  "source_url": "https://www.contractsfinder.service.gov.uk/Published/Notices/OCDS/Search?publishedFrom=2026-10-12T12:00:00Z&publishedTo=2026-10-15T14:06:46Z&limit=100&cursor=cHVibGlzaGVkRnJvbT0yMDI2LTEwLTEyVDEyOjAwOjAwWnxwdWJsaXNoZWRUbz0yMDI2LTEwLTE1VDE0OjA2OjQ2WnxsaW1pdD0xMDB8bmV4dEN1cnNvcj03ODY3OTg=",
  "fetched_at": "2026-10-15T14:06:49Z"
}
```

#### RUN_SUMMARY

Every run writes a `RUN_SUMMARY` record to the key-value store, including runs that return nothing or in which a source failed. For each source it reports pages and requests, releases fetched, how many were filtered out and why, how many were de-duplicated, retries and rate-limit responses, errors and duration; plus whether the run was truncated by the item limit, and the `onlyNew` state. A zero-result run can therefore always be told apart from "a source was down".

### Known limitations

- **Long date ranges run slowly.** The government APIs rate-limit clients: in testing, a burst of about 20 requests, then roughly 5 pages (up to 500 notices) a minute, and a 120-second pause each time the limit is hit. About 700 notices a day are published across both services, so one day takes seconds, a 3-day pull of everything took under a minute in testing, and a week or more can take several minutes or longer. **Recommended use:** a daily Apify schedule with `onlyNew: true` (see the example above). Each run then reads only what is new and finishes quickly. Use wide date ranges only for one-off backfills and narrow them with filters where you can.
- **Contracts Finder often has no region.** Roughly half of its notices give only a postcode or country, so `region_code` / `region_name` are empty and they can never match a `regions` filter.
- **Some Find a Tender records have no CPV code.** They cannot match a `cpvCodes` filter, so they will not appear in CPV-filtered results even if they concern that sector. (In testing about 3% of Find a Tender records had none, after reading CPV from lots, items and awards; Contracts Finder records always had one.)
- **Contracts Finder award values are tender values.** For an award notice, `value_min` / `value_max` are the notice's tender-level figures (typically the estimated or framework value), not necessarily the amount finally awarded. Find a Tender award notices without a tender value fall back to the award or contract value, as a range, never summed.
- **Notices with no value never pass a value filter** (`minValue` / `maxValue`); a non-GBP value does not either.
- **Notices re-published under a new id are folded into the newest one.** Buyers sometimes publish the same notice twice within minutes (new notice id, new `ocid`, identical text). Within one service, notices with the same buyer id, title, description and type that were published within 48 hours of the newest one are returned as a single record; the older notice ids are listed in `related_notice_ids`. Deadlines and values are not compared, so two genuinely separate calls that reuse one text within 48 hours (for example lots published as separate notices) are also folded: check `related_notice_ids` if that matters to you.
- **Cross-source de-duplication is a match, not an id lookup.** The two systems never share process ids, so a call published on both is recognised by the same buyer, the same title once reference prefixes are removed, the same tender-deadline day and the same notice type. This is deliberately strict: it applies to tender and planning notices only (awards repeat too often to be matched safely), and a copy whose title was reworded, or that has no deadline (most planning notices), is not matched and appears twice. A merged record keeps the later copy's fields and fills its gaps from the other.
- **Personal data is not output.** Procurement officers' contact names and e-mails in the source data are deliberately left out.

### Data source and licence

Data comes from the official OCDS APIs of:

- [Find a Tender Service](https://www.find-tender.service.gov.uk/) (Cabinet Office)
- [Contracts Finder](https://www.contractsfinder.service.gov.uk/) (Crown Commercial Service / Cabinet Office)

Contains public sector information licensed under the [Open Government Licence v3.0](https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/). This Actor is not affiliated with or endorsed by the UK Government. Notice content remains the responsibility of the publishing authority; check the notice itself before bidding.

# Actor input Schema

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

Return notices whose title or description contains ANY of these words or phrases (case-insensitive, matched from the start of a word, so 'cyber' finds 'cybersecurity'). Leave empty for no keyword filter.

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

Common Procurement Vocabulary codes, matched as a prefix. '72' = all IT services, '72212' = application software development, '45' = construction. A full 8-digit code such as 72000000 is treated as its division ('72'). Matches the notice's main and additional CPV codes, including those on items and lots.

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

Start of the publication window, as YYYY-MM-DD or an ISO-8601 datetime (UTC if no zone is given). Default: 24 hours before 'Published to'.

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

End of the publication window, as YYYY-MM-DD (the whole day is included) or an ISO-8601 datetime. Default: now.

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

Keep notices whose value range reaches at least this amount, e.g. 100000 for £100k+. Notices that state no value are excluded when a value filter is set.

## `maxValue` (type: `integer`):

Keep notices whose value range starts at or below this amount. Notices that state no value are excluded when a value filter is set.

## `noticeTypes` (type: `array`):

Which stages of procurement to return. Default: tender + planning (open opportunities and pipeline). Awards are opt-in: add 'award' (and 'contract', 'implementation') to also get awarded contracts, or list all five for everything. 'planning' = pipeline / early engagement, 'tender' = open opportunities, 'award' = awarded contracts, 'contract' = contract details, 'implementation' = contract delivery updates.

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

UK region names or NUTS codes, e.g. 'London', 'Scotland', 'UKI' (London), 'UKD7' (Merseyside). Names and NUTS codes are matched to each other, so 'London' also finds notices coded 'UKI31'. 'England' covers all nine English regions. Notices that only say 'UK' (nationwide) match only if you enter 'UK'.

## `buyers` (type: `array`):

Keep notices whose buyer (contracting authority) name contains ANY of these texts, case-insensitive, partial match: 'transport for london', 'nhs', 'council'. Notices with no buyer name never match.

## `onlyNew` (type: `boolean`):

Return only notices (and new versions of notices) not returned by a previous run with the same filters. State is kept in the named key-value store 'uk-public-tenders-state'. Combine with an Apify schedule for a daily feed. The first run behaves normally.

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

Which services to read. Default: both.

## Actor input object example

```json
{
  "keywords": [],
  "cpvCodes": [
    "72"
  ],
  "noticeTypes": [
    "tender",
    "planning"
  ],
  "regions": [],
  "buyers": [],
  "onlyNew": false,
  "sources": [
    "fts",
    "contracts_finder"
  ]
}
```

# Actor output Schema

## `notices` (type: `string`):

One record per procurement (ocid). Missing values are null.

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

Per-source fetched / filtered / de-duplicated counts, errors, item-limit truncation, onlyNew state and duration. Written on every 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 = {
    "keywords": [],
    "cpvCodes": [
        "72"
    ],
    "noticeTypes": [
        "tender",
        "planning"
    ],
    "regions": [],
    "buyers": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("seralifatih/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 = {
    "keywords": [],
    "cpvCodes": ["72"],
    "noticeTypes": [
        "tender",
        "planning",
    ],
    "regions": [],
    "buyers": [],
}

# Run the Actor and wait for it to finish
run = client.actor("seralifatih/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 '{
  "keywords": [],
  "cpvCodes": [
    "72"
  ],
  "noticeTypes": [
    "tender",
    "planning"
  ],
  "regions": [],
  "buyers": []
}' |
apify call seralifatih/uk-public-tenders --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,seralifatih/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/C27PLP788m9Wpvu3G/builds/pyxFlw08J7YMK7bUq/openapi.json
