# UK Tenders Scraper - Find a Tender & Contracts Finder (`automatic/uk-procurement-tenders-scraper`) Actor

Scrape UK public procurement from Find a Tender (above threshold) and Contracts Finder (below threshold). One row per procurement joins tender, award and contract notices - buyer, CPV codes, procedure, deadline, value, lots, suppliers, documents and notice link. Daily changes feed, JSON/CSV/Excel.

- **URL**: https://apify.com/automatic/uk-procurement-tenders-scraper.md
- **Developed by:** [Philip Kirkbride](https://apify.com/automatic) (community)
- **Categories:** Lead generation
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 procurements

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 Procurement Tenders Scraper

UK public bodies publish their buying on two national services: **Find a
Tender** for above-threshold procurements, and **Contracts Finder** for
below-threshold ones. A procurement usually appears more than once — first as
a tender notice, later as an award or contract notice — and the useful picture
only appears when those notices are put together.

This actor reads both services and saves **one clean row per procurement**. It
joins the tender, award and contract notices that share the same Open
Contracting ID (OCID), so a row carries the buyer, the CPV codes, the
procedure, the status, the submission deadline, the value, the lots, the
suppliers and contract values when they are published, the documents and the
link to the official notice.

**Cost.** $0.0015 per procurement plus a $0.005 run start, so the Console
sample of 20 open procurements costs about $0.035 and 100 procurements about
$0.155. See [Pricing](#pricing).

### Who it is for

- **UK suppliers and bid teams** looking for work they can still bid on,
  filtered by what is being bought (CPV) rather than by title wording alone.
- **Tender-alert and monitoring services** that need one row per procurement
  with a stable id, plus only what changed since yesterday.
- **Market research and analytics** teams measuring what the public sector
  buys, from which buyers, at what value and from which suppliers.
- **Award and contract tracking**: turn on awards and read the suppliers,
  award values and contract dates once they are published.

### Quick start

Open procurements from both services, 20 rows (the Console default):

```json
{"maxItems": 20}
```

IT services on Find a Tender:

```json
{"sources": ["fts"], "cpvPrefixes": ["72"], "maxItems": 50}
```

Construction on Contracts Finder, the below-threshold service:

```json
{"sources": ["cf"], "cpvPrefixes": ["45"], "maxItems": 50}
```

A daily changes feed for one search — same input, on a schedule:

```json
{"cpvPrefixes": ["72"], "mode": "changes", "stateKey": "it-services", "maxItems": 100}
```

Everything published in a date window, whatever its status:

```json
{"publishedFrom": "2026-11-01", "publishedTo": "2026-11-30", "status": "all", "maxItems": 200}
```

### What you get

- **Source and OCID** — whether the procurement is from Find a Tender (`FTS`)
  or Contracts Finder (`CF`), and its stable Open Contracting ID
- **Title and description** — as published, in full
- **Buyer** — name and identifier (for example `GB-FTS-9129` or a company
  number when the buyer published one)
- **CPV codes** — every code the notice uses, with the published description
- **Procedure and status** — for example "Open procedure", `active`,
  `complete` or `cancelled`
- **Open flag** — whether bids can still be submitted at the moment the run
  started
- **Tender period** — the start and the submission deadline, in the source's
  own time zone
- **Value and currency** — when the notice states one
- **Lots** — each lot with its description, status, value and contract period
- **Awards and contracts** — suppliers (name and identifier), award value,
  award date, contract value and contract dates, when the source has published
  them
- **Documents** — each published document with its type, description, link,
  date and format
- **Release history** — every release of the procurement with its id, date and
  tags, the latest release, and any amendments the source records
- **Notice link** — the official notice page
- **How complete the row is** — Find a Tender rows (with awards on) are built
  from the procurement's full published record. Contracts Finder has no full
  record to read, so its rows are joined from the notices the run read, and
  older notices of a long-running procurement may be missing; `joinedFrom`
  and `historyComplete` say which case a row is. If a Find a Tender record
  cannot be read, only that row falls back to the notices in the window and
  `recordIssue` says why; the run keeps reading the other records. A single
  run reads the full published record for at most 300 Find a Tender
  procurements; any beyond that are joined from the notices the run read, and
  `recordIssue` names the reason.
- **Possible matches on the other service** — listed separately, never merged

Everything is passed through as published, including buyer and supplier names.
A field the notice leaves out is `null`; nothing is guessed, translated or
filled in.

### How to search

All filters are optional. By default the actor reads both services and keeps
open procurements whose submission deadline is still ahead.

| Filter | What it does |
| --- | --- |
| Services | Find a Tender, Contracts Finder, or both (default) |
| Keyword | Every word must appear in the title, description, buyer, CPV descriptions, lots or award suppliers |
| CPV code prefixes | Keep procurements whose CPV codes start with any listed prefix, for example `72` for IT or `45` for construction |
| Buyer | Case-insensitive buyer name or identifier match |
| Published from / to | Notice publication date range |
| Deadline from / to | Submission deadline date range |
| Status | Open only (default) or all statuses |
| Maximum procurements | How many rows to save (50 by default, up to 1,000) |
| Include awards and contracts | Join award and contract notices to the row (on by default) |

**Narrow filters search a limited window.** Find a Tender and Contracts Finder do
not offer keyword or CPV search, so the actor reads the newest notices in order
and applies those filters itself. It stops after five pages in a row with no
match, and it never reads more than 40 pages per service. A narrow keyword or
CPV search therefore looks at the most recent releases rather than the whole
archive. Add a **Published from / to** range to search a particular period; the
date range switches the five-page stop off (the window already bounds the
search), so only the 40-page cap applies.

### Change tracking

Set **Mode** to *Changes since the last run* and the actor stops repeating the
whole result set. It remembers what it last delivered for each procurement,
and each later run saves only what moved:

- **new** — a procurement this state has not delivered before
- **amended** — a tracked value changed; `changedFields` names the fields.
  `previous` gives the earlier value of each changed single value (title,
  status, deadline, value, buyer, latest notice). For text and lists
  (description, CPV codes, lots, awards, contracts, documents, amendments) the
  field is named, and `previous` shows the earlier content only when it was
  empty — for example `"awards": []` when a first award appears
- **closed** — it was open at the previous run and is no longer open: the
  notice was awarded, cancelled or withdrawn, or its submission deadline has
  passed. A tracked procurement whose deadline passes is reported as closed
  even if no new notice is published for it; that row is rebuilt from the
  values delivered earlier and says so (`joinedFrom: "earlier runs"`)

A run only reads the most recent notices, so it may not see every notice of
an older procurement again. A value the notices read this run do not carry is
treated as unknown, never as a change: single values keep the value
delivered earlier, and the field is named in `fieldsNotInScan`. A missing
deadline never turns into a false "closed".

The first run of a state has nothing to compare against, so every row it
delivers is new; the run status says so. Use the same **State key** on every
run of the same search. If you leave it empty, the name is derived from the
search filters, so the same filters resume the same state and a different
search starts its own.

The state lives in a named store in your own Apify account. Only rows that
were actually saved are remembered, so a run that stops early does not hide
what it did not deliver. If the state cannot be read or is not one this actor
wrote, the run fails before delivering or charging anything, instead of
starting over and repeating every procurement as new. If the state cannot be
saved, the run fails and its status says which rows the next run will deliver
again. Procurements not seen for a year are dropped, and the store keeps at
most 5,000 procurements, the most recently seen first; the status says when
any were dropped.

When a run reaches **Maximum procurements**, it stops and says so in the
status; in Changes mode the remaining changes are delivered by a later run.

Change rows also carry the source's own version markers: the release ids,
dates and tags, the latest release tag, and any amendments the notice records.

### Possible matches across the two services

Find a Tender and Contracts Finder cover different thresholds, so most
procurements appear on only one of them. When the same buyer publishes a very
similar title with overlapping dates on both services, each row lists the other
as a **possible match** with a plain reason. These are hints for a human to
check. The two procurements are never merged into one row, and no match is
asserted as certain.

### Good to know

- Dates keep the time zone the source published them in (UK local time, with
  the offset stated).
- Awards and contract values appear only when the source has published them;
  they are usually absent while a tender is still open.
- A search that matches nothing finishes successfully with zero rows and a
  plain status message.
- A narrow keyword or CPV search looks at a limited window of the newest
  notices; add a publication date range to target a particular period.
- If one Find a Tender record cannot be read — because it is missing, gone,
  rejected by the source, empty or malformed — only that procurement falls back
  to the notices in the window; the rest of the run continues, and
  `recordIssue` names the reason on the affected row.
- A source that cannot be read is reported as an error, never as "no results".
- If one service is unavailable, the other service's rows are still delivered
  and the status says the run stopped early.
- If a run reaches **Maximum procurements**, the status says so: more
  procurements may match.
- A run that runs out of time before delivering anything fails, so an empty
  result never looks like "no matches".
- Contains public sector information licensed under the Open Government
  Licence v3.0.

### Pricing

- **$0.0015 per delivered procurement** — one charge per row, whether the row
  is a tender, an amended tender or a closed one. A procurement that appears
  as a tender notice and later as an award notice is one row and one charge.
- **$0.005 per run start**, charged per GB of memory, so the default 512 MB
  run is one start charge. It applies even when a search matches nothing.
- Reading CPV codes, notices and awards, and filtering, adds run time, not
  charges.

| Run | Rows | Cost |
|---|---:|---:|
| Console sample: both services, `maxItems` 20 | 20 | $0.035 |
| A CPV or keyword search on one service | 50 | $0.08 |
| A daily changes feed that finds 12 changes | 12 | $0.023 |
| Everything published in one month | 1,000 | $1.505 |

See the Pricing tab for the current price.

### Output example

One row per procurement. This example is a Changes-mode row shortened to show
the shape; repeated array entries are shortened too.

```json
{
  "recordId": "FTS:ocds-h6vhtk-0323a9",
  "ocid": "ocds-h6vhtk-0323a9",
  "source": "FTS",
  "title": "Provision of Conversion Support Services for Student Recruitment for Glasgow Caledonian University",
  "description": "The University is seeking a Contractor for the Provision of a Conversion Partner who is equipped to provide a bespoke data driven, measurable series of conversion solutions…",
  "buyerName": "Glasgow Caledonian University",
  "buyerId": "GB-FTS-9129",
  "buyerIdentifierScheme": null,
  "buyerIdentifierId": null,
  "cpvCodes": [
    {"code": "79411100", "description": "Business development consultancy services", "scheme": "CPV"}
  ],
  "procedure": "Open procedure",
  "procedureCode": "open",
  "status": "active",
  "isOpen": true,
  "tenderPeriodStart": null,
  "tenderPeriodEnd": "2026-12-04T12:00:00+00:00",
  "valueAmount": 800000,
  "valueCurrency": "GBP",
  "lots": [
    {"id": "1", "title": null, "description": "The University is seeking an experienced Higher Education conversion partner…", "status": "active", "valueAmount": null, "valueCurrency": null, "contractPeriodStart": null, "contractPeriodEnd": null, "hasRenewal": true}
  ],
  "awards": [],
  "contracts": [],
  "documents": [
    {"id": "104855-2026", "documentType": "tenderNotice", "title": null, "description": "Tender notice on Find a Tender", "url": "https://www.find-tender.service.gov.uk/Notice/104855-2026", "datePublished": "2026-11-04T16:19:41+00:00", "format": "text/html", "language": null, "source": "tender"}
  ],
  "releases": [
    {"id": "104700-2026", "date": "2026-11-01T08:30:00+00:00", "tags": ["tender"]},
    {"id": "104855-2026", "date": "2026-11-04T16:19:41+00:00", "tags": ["tenderUpdate"]}
  ],
  "releaseIds": ["104700-2026", "104855-2026"],
  "releaseDates": ["2026-11-01T08:30:00+00:00", "2026-11-04T16:19:41+00:00"],
  "latestReleaseId": "104855-2026",
  "latestReleaseDate": "2026-11-04T16:19:41+00:00",
  "latestReleaseTag": "tenderUpdate",
  "latestReleaseTags": ["tenderUpdate"],
  "releaseTags": ["tender", "tenderUpdate"],
  "amendments": [],
  "noticeUrl": "https://www.find-tender.service.gov.uk/Notice/104855-2026",
  "joinedFrom": "full record",
  "historyComplete": true,
  "recordIssue": null,
  "fieldsNotInScan": [],
  "observedAt": "2026-11-04T21:17:15+00:00",
  "changeType": "amended",
  "changedFields": ["tenderPeriodEnd", "latestReleaseId", "latestReleaseDate", "latestReleaseTag", "latestReleaseTags", "noticeUrl"],
  "previous": {
    "tenderPeriodEnd": "2026-11-27T12:00:00+00:00",
    "latestReleaseId": "104700-2026",
    "latestReleaseDate": "2026-11-01T08:30:00+00:00",
    "latestReleaseTag": "tender",
    "latestReleaseTags": ["tender"],
    "noticeUrl": "https://www.find-tender.service.gov.uk/Notice/104700-2026"
  },
  "firstSeenAt": "2026-11-01T09:00:00+00:00",
  "lastSeenAt": "2026-11-04T21:17:15+00:00",
  "contentHash": "…"
}
```

In Full mode, `changeType`, `previous`, `fieldsNotInScan`, `firstSeenAt`,
`lastSeenAt` and `contentHash` are `null`, and `changedFields` is an empty
list.

### FAQ

**How much does a run cost?** $0.0015 per delivered procurement plus a $0.005
run start: the 20-row Console sample costs $0.035 and 100 procurements cost
$0.155. Filters do not add a charge; they only change how much of the sources
has to be read.

**How do I get only new tenders each day?** Set `"mode": "changes"` with a
`stateKey` and schedule the run with the same input. The first run is the
starting point; later runs deliver only new, amended and closed procurements,
with the fields that moved and their previous values.

**Why is my keyword or CPV search slow?** Neither service offers keyword or
CPV search, so a narrow filter has to be applied to the newest notices in
order, and it stops after five pages in a row with no match. Narrow the source
(`sources`), add a `publishedFrom` / `publishedTo` window, or keep the filter
broad; a search that matches nothing finishes successfully with zero rows and
the run start charge.

**Why do Contracts Finder rows have fewer fields than Find a Tender rows?**
Find a Tender publishes a full procurement record; Contracts Finder publishes
notice by notice, so its rows are joined from the notices read. `joinedFrom`
and `historyComplete` say which case a row is, and `recordIssue` names a
record that could not be read.

**Why did I get fewer rows than Maximum procurements?** Either the filters
matched less than you asked for, a service stopped after five pages with no
match, or the run reached its time limit. The run status says which, and the
next run continues from where it stopped in Changes mode.

**Are the tender documents downloaded?** No. Each row lists the published
documents with their type, description, link, date and format; the files stay
on the official notice page.

**What happens when nothing matches?** The run succeeds with an empty dataset
and a plain status message. A source that cannot be read is reported as an
error, never as "no results".

**Can I call it from the API or on a schedule?** Yes. Every Apify Actor runs
from the API with the same JSON input and can be scheduled; see the API tab of
this Actor.

**Where does the data come from and what licence applies?** Find a Tender
(above threshold) and Contracts Finder (below threshold), both published under
the Open Government Licence v3.0. Everything is passed through as published.

# Actor input Schema

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

Which UK services to read. Find a Tender carries above-threshold notices; Contracts Finder carries below-threshold notices. Both are read by default.

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

Words to look for in the title, description, buyer, CPV descriptions or lots. Every word must appear. Leave empty for everything.

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

CPV prefixes to keep, for example 72 (IT services) or 45 (construction). A procurement matches when any of its CPV codes starts with any prefix. Leave empty for all codes.

## `buyer` (type: `string`):

Buyer name or buyer identifier to keep, for example a council name. Case-insensitive contains match. Leave empty for all buyers.

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

Earliest notice publication date, YYYY-MM-DD.

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

Latest notice publication date, YYYY-MM-DD.

## `deadlineFrom` (type: `string`):

Earliest tender submission deadline, YYYY-MM-DD.

## `deadlineTo` (type: `string`):

Latest tender submission deadline, YYYY-MM-DD.

## `status` (type: `string`):

Open keeps procurements whose submission deadline is still ahead; All keeps every status, including awards and cancellations.

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

Full saves every matching procurement. Changes saves only what moved since the last run for the same state: new, amended and closed procurements, with the fields that changed and their previous values.

## `stateKey` (type: `string`):

In Changes mode, a name for this search's saved state. Use the same key on every scheduled run of this search. Leave empty to derive it from the search filters.

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

How many procurements to save, from 1 to 1000.

## `includeAwards` (type: `boolean`):

Join each procurement's award and contract notices to its row, including suppliers and contract values. On by default.

## Actor input object example

```json
{
  "sources": [
    "fts",
    "cf"
  ],
  "keyword": "",
  "status": "open",
  "mode": "full",
  "maxItems": 20,
  "includeAwards": true
}
```

# Actor output Schema

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

No description

# 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": [
        "fts",
        "cf"
    ],
    "keyword": "",
    "status": "open",
    "mode": "full",
    "maxItems": 20,
    "includeAwards": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("automatic/uk-procurement-tenders-scraper").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": [
        "fts",
        "cf",
    ],
    "keyword": "",
    "status": "open",
    "mode": "full",
    "maxItems": 20,
    "includeAwards": True,
}

# Run the Actor and wait for it to finish
run = client.actor("automatic/uk-procurement-tenders-scraper").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": [
    "fts",
    "cf"
  ],
  "keyword": "",
  "status": "open",
  "mode": "full",
  "maxItems": 20,
  "includeAwards": true
}' |
apify call automatic/uk-procurement-tenders-scraper --silent --output-dataset

```

## MCP server setup

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

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/Lkm7AOwdFKCFf0tpV/builds/ZwCjbMsaIdesKhzd7/openapi.json
