# UK Public Tenders Monitor (Contracts Finder + Find a Tender) (`keepable_injera/uk-tenders-monitor`) Actor

Merged UK public procurement notices from Contracts Finder and Find a Tender (OCDS): filter by keyword, CPV, value, region and stage, with onlyNew change detection. Unofficial; not affiliated with the UK Government. Contains public sector information licensed under the Open Government Licence v3.0.

- **URL**: https://apify.com/keepable_injera/uk-tenders-monitor.md
- **Developed by:** [Chee Chiaw Lim](https://apify.com/keepable_injera) (community)
- **Categories:** Business, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$3.00 / 1,000 notice results

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 Tenders Monitor (Contracts Finder + Find a Tender)

### What it does

Collect UK procurement notices from the public Contracts Finder and Find a Tender OCDS APIs. Filter notices by keywords, CPV prefixes, GBP value, region, and notice stage. Track new and changed notices between runs without an API key or login.

The Actor follows pagination, keeps the latest release per OCID across both sources, and prefers Find a Tender when release dates tie. Filters apply to the selected release.

### Input examples

Collect up to five notices from the last three days, including unchanged notices.

```json
{
  "sources": ["contractsFinder", "findATender"],
  "lookbackDays": 3,
  "maxItems": 5,
  "onlyNew": false
}
```

Monitor cleaning tenders in Manchester.

```json
{
  "keywords": ["cleaning"],
  "cpvPrefixes": ["909"],
  "regions": ["Manchester"],
  "noticeStages": ["tender"],
  "minValue": 10000,
  "maxValue": 500000,
  "onlyNew": true,
  "maxItems": 100,
  "stateStoreName": "cleaning-tenders-state"
}
```

### Input fields

| Field | Default | Meaning |
| --- | --- | --- |
| `sources` | Both sources | `contractsFinder`, `findATender`, or both. |
| `lookbackDays` | `3` | Lookback window in days, clamped to 1 through 365. CF uses publication dates. FTS uses update dates. |
| `keywords` | `[]` | Case-insensitive substring in title or description. |
| `cpvPrefixes` | `[]` | Prefix of an item CPV classification or additional classification ID. |
| `minValue`, `maxValue` | Unset | Inclusive GBP value bounds. Unknown values and other currencies do not match a value filter. |
| `regions` | `[]` | Case-insensitive substring in item or tender delivery regions, or buyer address region. |
| `noticeStages` | `[]` | Case-insensitive OCDS tag prefixes such as `tender`, `award`, `planning`, or `contract`. |
| `maxItems` | `100` | Maximum notice pushes, clamped to 1 through 10000. |
| `onlyNew` | `true` | Emit new and changed notices. Set false to include unchanged notices. |
| `stateStoreName` | `uk-tenders-monitor-state` | Named KV store for independent monitoring history. |

Values within each list use OR. Different filter fields use AND. Empty lists do not filter. Description matching uses the trimmed output description, limited to 2000 characters.

### Output sample

```json
{
  "ocid": "ocds-b5fd17-11111111-2222-3333-4444-555555555555",
  "releaseId": "11111111-2222-3333-4444-555555555555-20261019",
  "source": "contractsFinder",
  "title": "Library cleaning services",
  "description": "Cleaning of council libraries.",
  "tag": ["tender"],
  "status": "active",
  "buyerName": "Example Borough Council",
  "buyerRegion": "Greater Manchester; North West",
  "valueAmount": 120000,
  "valueCurrency": "GBP",
  "cpvCodes": ["90911200"],
  "publishedDate": "2026-10-19T10:00:00Z",
  "tenderDeadline": "2026-11-12T12:00:00Z",
  "awardDate": null,
  "suppliers": [],
  "noticeUrl": "https://www.contractsfinder.service.gov.uk/Notice/11111111-2222-3333-4444-555555555555",
  "changeType": "new",
  "changedFields": [],
  "checkedAt": "2026-10-20T12:00:00.000Z",
  "license": "OGL-UK-3.0"
}
```

`tag` is the notice stage array. Missing scalar values are null. Arrays are empty when unavailable. Value uses tender value first, then the first available award value. Award date is the latest award date. Suppliers contain distinct award supplier names. Notice URLs use recognizable CF UUIDs or FTS notice numbers from the release ID or OCID. Otherwise the URL is null.

The default dataset contains notices and uncharged errors. Error rows have `error`, `checkedAt`, and a `source` when known. The default KV store contains `RUN_SUMMARY`, with timestamps, `onlyNew`, `errors`, `counts`, and `stoppedAtChargeLimit`. `counts.fetched` counts deduplicated notices that passed filters. `counts.eligible` counts notices considered for a push before stopping. `counts.pushed` counts successful notice pushes.

### Change tracking

The named state store contains `NOTICES`, an object keyed by OCID. Each entry holds a SHA-256 fingerprint and the meaningful field values. The stable JSON fingerprint covers `title`, `status`, `tag`, `valueAmount`, `tenderDeadline`, `awardDate`, and `suppliers`. Array order does not affect the fingerprint.

First observations have `changeType` equal to `new` and empty `changedFields`. Later observations are `changed` with the changed field names, or `unchanged` with an empty list. Changes outside the fingerprint fields do not trigger emission when `onlyNew` is true.

State is saved only after a successful notice push. Charge limits and failed pushes leave notices eligible for retry. Dataset pushes and state saves are separate operations. A crash between them can cause a duplicate on retry. Avoid concurrent runs with the same state store. Use a different store name for independent histories. Delete `NOTICES` to reset history.

### Pricing

The PPE event is `notice` at $0.003 per emitted notice, or $3 per 1,000 notices, with no run fee. This includes unchanged notices when `onlyNew` is false. Skipped notices and error records are not charged. Local runs do not bill. Store pricing must be configured as described in [PUBLISHING.md](PUBLISHING.md).

### Limits

- Requests start at most once per second per host. All HTTP uses `politeFetch` with 30-second timeouts, up to three retries for HTTP 429 and 5xx, exponential backoff, and `Retry-After` support.
- Each API page requests up to 100 releases. Pagination follows `links.next` or a returned cursor. Repeated pagination and links outside the source host produce uncharged errors.
- Both sources are collected before deduplication and output. `maxItems` caps emitted notices, not HTTP requests or releases read. Broad lookbacks can require substantial memory and requests.
- A source failure produces an error row. Notices already read and the other source can still be processed. Results can be incomplete during source outages.
- Lookback windows can miss older releases or updates outside the window. This Actor is not a complete procurement archive.
- History grows with unique emitted OCIDs. Public data can omit amounts, regions, deadlines, or classifications.

### Terms and attribution

[TERMS.md](TERMS.md) records the source terms check. Contracts Finder responses identify the Open Government Licence. Find a Tender documents notice data as available under that licence.

Contains public sector information licensed under the Open Government Licence v3.0.

[Open Government Licence v3.0](https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/) permits commercial reuse subject to its terms and attribution requirements. Source data remains subject to stated exceptions and third-party rights.

### Disclaimer

This Actor is unofficial and is not affiliated with or endorsed by Contracts Finder, Find a Tender, or the UK government. It uses public documented APIs only. It uses no logos, login, or API keys. Check the original notice before making procurement decisions.

### Local development

Node 20 or newer is required. Tests use `node:test`, stubbed fetch, small OCDS fixtures, and local storage.

```sh
npm test
```

Run the real Actor locally with fixture-backed HTTP and inspect its dataset and summary.

```sh
NODE_OPTIONS="--import=./scripts/offline-fetch.mjs" npm start
```

The sample input includes at most five notices and sets `onlyNew` to false. Plain `npm start` uses the live public APIs. No login is needed for local execution.

# Actor input Schema

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

Sources.

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

Lookback days.

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

Keywords in title or description.

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

CPV code prefixes.

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

Delivery or buyer regions.

## `noticeStages` (type: `array`):

Notice stages, such as tender, award, planning, contract.

## `minValue` (type: `number`):

Minimum value in GBP.

## `maxValue` (type: `number`):

Maximum value in GBP.

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

Maximum notices.

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

Emit only new or changed notices.

## `stateStoreName` (type: `string`):

Named state store.

## Actor input object example

```json
{
  "sources": [
    "contractsFinder",
    "findATender"
  ],
  "lookbackDays": 3,
  "keywords": [],
  "cpvPrefixes": [],
  "regions": [],
  "noticeStages": [],
  "maxItems": 5,
  "onlyNew": true,
  "stateStoreName": "uk-tenders-monitor-state"
}
```

# Actor output Schema

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

No description

## `runSummary` (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": [
        "contractsFinder",
        "findATender"
    ],
    "maxItems": 5
};

// Run the Actor and wait for it to finish
const run = await client.actor("keepable_injera/uk-tenders-monitor").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": [
        "contractsFinder",
        "findATender",
    ],
    "maxItems": 5,
}

# Run the Actor and wait for it to finish
run = client.actor("keepable_injera/uk-tenders-monitor").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": [
    "contractsFinder",
    "findATender"
  ],
  "maxItems": 5
}' |
apify call keepable_injera/uk-tenders-monitor --silent --output-dataset

```

## MCP server setup

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

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/FKXfvdcXftM19QbmM/builds/0YpCCi0qLKy8mcuIZ/openapi.json
