# UK Public Tenders & Contract Expiry Feed (`larches/uk-public-tenders-feed`) Actor

One clean, deduplicated feed of UK public sector tenders, awards and soon-to-expire contracts from the official Find a Tender and Contracts Finder OCDS APIs. Contact-person data is stripped by default.

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

## Pricing

$4.00 / 1,000 tender notices

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 & Contract Expiry Feed

This Actor gives you one clean, deduplicated feed of UK public sector procurement notices from the two official government platforms:

| Source | Covers | API |
|---|---|---|
| **Find a Tender (FTS)** | Above-threshold UK tenders and awards (Procurement Act 2023 / PCR 2015) | `find-tender.service.gov.uk/api/1.0/ocdsReleasePackages` |
| **Contracts Finder (CF)** | Below-threshold contracts from English buyers (over £12k / £30k) | `contractsfinder.service.gov.uk/Published/Notices/OCDS/Search` |

It does no HTML scraping and needs no login or API key. All data comes from the official, open OCDS APIs.

### What it is for

- **Open opportunities.** These are new tenders still open for bids, plus pipeline/planning notices, filtered by CPV, keyword, value, region and SME suitability.
- **Contract awards.** See who won what, for how much, and when. Useful for competitor intelligence.
- **Expiring contracts (renewal radar).** These are awarded contracts whose end date falls in the next N months. You can approach the buyer before the re-tender. Paid tools such as Stotles sell this as "Upcoming expiring contracts" from £75/month.
- **Only-new mode.** On a daily schedule you get, and pay for, each notice once.
- **Presets.** `it` covers CPV 72/48/302. `cyber` is a security keyword set covering SOC, SIEM, pentest, MDR, Cyber Essentials and similar. Construction and facilities presets are also included.

### Privacy by default (UK GDPR)

Notices often name a buyer's contact person and give their email and phone number. **By default, this Actor drops those details.**

- The `contactPoint` block is removed.
- Email addresses and UK phone numbers in titles and descriptions are replaced with `[email removed]` / `[phone removed]`.
- Supplier names that may belong to a sole trader are withheld. A name is kept when the supplier has a company or charity number, is "large", or has an organisation word in its name; otherwise it is withheld.

You can turn on `includeContacts` only if you have a lawful basis for processing that data.

### Input (main fields)

| Field | Default | Notes |
|---|---|---|
| `mode` | `opportunities` | `opportunities`, `awards`, `expiringContracts` or `all` |
| `sources` | `["fts","cf"]` | |
| `lookbackDays` | 2 | Use 365+ for `expiringContracts` |
| `keywords` / `excludeKeywords` / `keywordScope` | – | Whole-word matching; `cyber*` matches as a prefix |
| `cpvPrefixes` / `cpvPreset` | – | Matches main and additional CPV codes |
| `minValueGbp`, `requireValue`, `smeSuitableOnly`, `openOnly`, `regions` | | |
| `expiringWithinMonths` | 12 | |
| `onlyNew` | false | State is kept in the named key-value store `uk-tenders-feed-state` |
| `maxItems` / `maxRequestsPerSource` | 500 / 50 | Each request returns up to 100 notices |

### Output

Each notice is one row. Fields include:

- source, `noticeUrl`, stage (`planning` / `tender` / `award`)
- title, description, buyer (name, locality, postcode, region, website)
- CPV codes with labels, procurement method, estimated value
- deadline, contract start and end, delivery regions, SME/VCSE suitability
- awards (value, dates, suppliers with Companies House number and scale)
- the licence statement

See `.actor/dataset_schema.json` and `sample-output.json`. A run summary is saved to the key-value store as `RUN_SUMMARY`.

### Politeness

- At most about 1 request per second per host, with a declared User-Agent.
- Backs off on 429/403/5xx and honours `Retry-After`.
- Follows cursor pagination and hard-caps pages per source.
- Neither host publishes a robots.txt (both returned 404 when checked on 2026-10-28).

### Licence and attribution

The notice data is published under the Open Government Licence v3.0. Every row carries:
"Contains public sector information licensed under the Open Government Licence v3.0."
Keep that statement when you republish.

Always read the official notice before you bid.

### Run locally

```bash
cd /workspace/apify-actors/uk-public-tenders-feed
python3 -m venv .venv && . .venv/bin/activate && pip install -r requirements.txt   # or reuse ../.venv
apify run -i '{"mode":"opportunities","lookbackDays":2,"maxItems":20}'            # no login needed
## or without the CLI:
mkdir -p storage/key_value_stores/default && echo '{"mode":"awards","cpvPreset":"cyber"}' > storage/key_value_stores/default/INPUT.json
APIFY_LOCAL_STORAGE_DIR=./storage python -m src
```

The results land in `storage/datasets/default/`.

# Actor input Schema

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

opportunities = new/open tenders and pipeline (planning) notices; awards = contract award notices (who won, for how much); expiringContracts = awarded contracts whose end date falls in the next N months (re-tender leads).

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

fts = Find a Tender (above-threshold), cf = Contracts Finder (below-threshold, England).

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

Notices published/updated in the last N days. For expiringContracts use a long window (e.g. 365-1095), since contracts are awarded years before they expire.

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

Case-insensitive whole-word match on title + description. Use a trailing \* for prefix match, e.g. cyber\*.

## `excludeKeywords` (type: `array`):

Drop notices whose title or description matches any of these words.

## `keywordScope` (type: `string`):

Title-only matching cuts false positives from long descriptions that mention a keyword in passing.

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

e.g. 72 = IT services, 48 = software, 79 = business services, 45 = construction. Matches main or additional CPV codes.

## `cpvPreset` (type: `string`):

Adds a ready-made CPV prefix list (and, for cyber, security keywords) to your own filters.

## `minValueGbp` (type: `integer`):

Drop notices with a known value below this. Notices with no value are kept unless 'Require value' is on.

## `requireValue` (type: `boolean`):

Drop notices that state no estimated or awarded value.

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

Keep only notices the buyer marked as suitable for SMEs.

## `openOnly` (type: `boolean`):

Opportunities mode: drop notices whose submission deadline has passed.

## `expiringWithinMonths` (type: `integer`):

expiringContracts mode: keep awards whose contract end date is between now and N months from now.

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

Case-insensitive substring match on delivery region, e.g. London, UKI, Scotland.

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

Remembers delivered notice IDs (per input) so scheduled runs only output new rows.

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

Stop after this many output rows (shared fairly across sources).

## `maxRequestsPerSource` (type: `integer`):

Each page = up to 100 notices. Hard cap to control run time.

## `includeContacts` (type: `boolean`):

Off by default. When on, the buyer's contact name/email/phone from the notice is included. These are personal data under UK GDPR: only enable if you have a lawful basis and will comply with the GDPR yourself.

## `includeRaw` (type: `boolean`):

Attach the full OCDS release (contactPoint removed unless contacts are enabled; free text is not scrubbed).

## Actor input object example

```json
{
  "mode": "opportunities",
  "sources": [
    "fts",
    "cf"
  ],
  "lookbackDays": 2,
  "keywords": [],
  "excludeKeywords": [],
  "keywordScope": "titleAndDescription",
  "cpvPrefixes": [],
  "cpvPreset": "none",
  "requireValue": false,
  "smeSuitableOnly": false,
  "openOnly": true,
  "expiringWithinMonths": 12,
  "regions": [],
  "onlyNew": false,
  "maxItems": 500,
  "maxRequestsPerSource": 50,
  "includeContacts": false,
  "includeRaw": false
}
```

# Actor output Schema

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

All notices returned by this run, as a dataset table.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("larches/uk-public-tenders-feed").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("larches/uk-public-tenders-feed").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 '{}' |
apify call larches/uk-public-tenders-feed --silent --output-dataset

```

## MCP server setup

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

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/a9saMchIedEywne9b/builds/fkGhmDF4pNSO1VDUG/openapi.json
