# UK Tenders Monitor: Government Contracts & Find a Tender Alerts (`lindenwerk/uk-tender-matcher`) Actor

UK tenders and government contracts from Find a Tender: UK public procurement notices (Procurement Act 2023, above and below threshold) scored to your CPV codes, keywords, regions and value, with deadlines, SME/VCSE flags and match reasons. Daily tender alerts, 31-day backfill, award tracking.

- **URL**: https://apify.com/lindenwerk/uk-tender-matcher.md
- **Developed by:** [Lindenwerk Data](https://apify.com/lindenwerk) (community)
- **Categories:** Business, Lead generation, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $8.00 / 1,000 scored match 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?

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

### What is UK Tenders Monitor?

**Find UK public sector tenders that actually fit your business, every morning, without reading hundreds of notices.**

- **Source.** This Actor reads **Find a Tender**, the UK government's central digital platform for public procurement
  notices, through its official OCDS API. Since 24 February 2025 (Procurement Act 2023), new UK procurements publish
  both **above- and below-threshold** notices there (except below-threshold contracts in Scotland).
- **Matching.** It merges tender updates into one row per procurement and scores every notice against your profile:
  CPV codes, keywords, UK regions (ITL/NUTS), contract type, value, threshold level and SME/VCSE suitability.
- **Output.** One clean, flat row per opportunity, with the submission deadline, days left, estimated value in GBP,
  a `relevanceScore` from 0 to 100 and plain-English `matchReasons`.

Use it as a **daily bid alert** (monitor mode returns only notices you haven't seen yet), for a **market scan** of the
last 31 days (backfill), or for **competitor tracking** (award notices with winning suppliers and awarded values).

### Who it's for

- **Bid managers and business development teams** selling to UK public buyers (councils, NHS trusts, central
  government, universities, colleges, schools and academy trusts, police, housing associations). They get one scored
  shortlist each morning instead of searching Find a Tender by hand.
- **SMEs and VCSEs** looking for contracts they can realistically win. `thresholdLevels: ["below"]`,
  `smeSuitableOnly` and `vcseSuitableOnly` use the suitability flags buyers set under the Procurement Act 2023.
- **Bid writers and tender consultants** monitoring several client profiles (one saved task and `stateKey` per client).
- **European companies selling into the UK.** The input schema is the same family as our
  [German & EU Tenders Monitor](https://apify.com/lindenwerk/german-eu-tender-matcher) and
  [France Tenders Monitor](https://apify.com/lindenwerk/france-tender-matcher), so one profile carries over.
- **Analysts and AI agents** that need structured UK procurement data (CPV, ITL regions, deadlines, values, award
  suppliers) through an API.

### What it does

- **Reads Find a Tender through its official OCDS release API** (`/api/1.0/ocdsReleasePackages`), one day at a time,
  with cursor pagination. No login, no proxies, no HTML scraping.
- **Respects the API's rate limits.** On HTTP 429 or 503 the Actor waits the full `Retry-After` time before *any*
  further request (all parallel day fetches pause, as the API documentation asks), retries the same page and never
  skips ahead. At most two days are fetched in parallel. Requests carry an identifying User-Agent.
- **Merges versions.** A procurement often has a tender notice plus one or more updates (changed deadline, new
  documents) in the same window. You get the newest version once; the older notice ids go to `relatedNoticeIds`.
- **Scores relevance with transparent rules** (no LLM, no black box):
  - **CPV codes** by prefix: `72` matches all IT services, `48` software, `45` construction works, `909` cleaning.
  - **Keywords**, case-insensitive, in the title (full weight) and the description (half weight). Rare words count
    more. Short terms such as `ICT` or `CCTV` match whole words only; wrap a phrase in double quotes for an exact phrase.
  - Hard filters (filtered notices are never charged):
    - `excludeKeywords`
    - `regions`: ITL/NUTS codes (`UKI` London, `UKM` Scotland, `UKD3` Greater Manchester) or names (`London`,
      `North West`, `Yorkshire`, `Wales`, `England`, `Midlands`)
    - `contractTypes` (`goods`, `services`, `works`)
    - `thresholdLevels` (`above`, `below`)
    - `smeSuitableOnly`, `vcseSuitableOnly`
    - `minValue` / `maxValue` (GBP, excl. VAT)
    - `onlyOpen` (the deadline is still in the future)
    - `competitorNames` (awards)
- **Three modes:** `monitor`, `backfill` and `awards`.
- **GDPR by default.** Contact-person names, personal e-mail addresses (`first.last@…`) and phone numbers are removed,
  from contact fields and from titles and descriptions. Only organisation names and functional contacts (e.g.
  `procurement@council.gov.uk`, "Commercial Team") are kept.

### What you get that a plain Find a Tender scraper doesn't

| | Typical Find a Tender scrapers | **This Actor** |
|---|---|---|
| Official OCDS API, no HTML scraping | most | ✅ |
| One row per procurement (tender updates merged) | rarely | ✅ newest version, older ids in `relatedNoticeIds` |
| Relevance scoring with reasons | ❌ or extra charge | ✅ `relevanceScore` + `matchReasons`, included |
| Procurement Act 2023 fields: threshold level, SME and VCSE suitability, legal basis | partly | ✅ as filters and output fields |
| UK region filter by name or ITL/NUTS code | partly | ✅ delivery location, else buyer address |
| "Only new since last run" | some | ✅ persistent state per `stateKey` |
| Personal contact data (names, e-mails, phones) | often included | **removed** |
| Charged only for notices that match your profile | rarely | ✅ filtered and below-`minScore` notices are free |

What we measured (Find a Tender, 5 Oct 2026):

- **7 days** (29 Sep to 5 Oct): 2,387 releases, 80 older tender versions merged, 2,307 notices, of them **483 tender
  notices**. The IT example profile below kept **33** of them at `minScore` 40.
- **31 days** (5 Sep to 5 Oct): 9,800 releases, 9,255 notices, of them **1,749 tender notices**.

So a focused profile turns roughly 70 new UK tender notices a day into a short daily list.

### How to get daily UK tender alerts

1. Click **Start** with the prefilled IT-services example. It's a quick market scan (`backfill`) that returns up to
   25 scored UK tenders, so you see real output before you change anything.
2. Replace the CPV codes and keywords with your own. Optionally add UK regions, contract types, threshold levels, SME/VCSE suitability and a value range.
3. Pick a mode: `backfill` for a market scan of the last days, `monitor` for daily alerts that return only notices
   you haven't seen yet, or `awards` for competitor tracking.
4. Look at the results in the **Output** tab (views "Matches" and "Awards") or download them as JSON, CSV or Excel.
5. For daily alerts, switch `mode` to `monitor`, save the input as a **task** and add a **schedule** (for example
   every weekday at 07:00).
6. Send new matches on with Apify **integrations** (Slack, Google Drive, Make, Zapier, n8n or a webhook), or call the Actor from your own code through the **Apify API**.

### Modes

| `mode` | What you get | Typical use |
|---|---|---|
| `monitor` (default) | Notices published or updated in the last `lookbackDays` (default 1) plus today (UK time) that **earlier runs with the same `stateKey` have not returned**. A first run that finds nothing looks back 7 days once (see below). | Schedule it daily (e.g. 07:30 Europe/London) and connect it to e-mail, Slack, a CRM or a spreadsheet. |
| `backfill` | Every matching notice between `dateFrom` and `dateTo` (max 31 days; without dates: the last 7 days). Leaves the monitor state untouched. | Market scan, pipeline building, testing a profile. |
| `awards` | Award notices only, with `winnerNames`, `awardedValue` and `awardedCurrency`. | Competitor and price intelligence, incumbent research before a re-tender. |

#### The Console form vs. API defaults

- **The Console form opens with a short trial:** `mode` = backfill (the last 7 days), an IT profile (CPV 72/48 plus
  `software`, `cloud`, `cyber security` and `data platform`) and `maxResults` = 25. One click shows the 25 best current
  matches for at most USD 0.20. This prefill is also what Apify's daily automated Store test runs. It keeps no state,
  so it returns rows on any day, weekends and bank holidays included.
- **Before you schedule it**, switch `mode` to **monitor** and raise `maxResults`, or set it to 0 for no limit.
- **API calls, tasks and schedules that leave a field out get the defaults:** `mode` = monitor, `lookbackDays` = 1,
  `maxResults` = 200. The prefill values only fill the form.
- **First monitor run on a quiet day:** a monitor run with no saved state (the first run for a `stateKey`, or one with
  `resetState`) that finds nothing new looks back `firstRunBackfillDays` (default 7) once. Everything it returns is
  remembered, so the next run is a normal monitor run. Set it to 0 to turn this off.
- **Quiet days and outages:** a monitor run with nothing new still ends *Succeeded*, with 0 rows and a status message.
  Nothing is charged. If some days fail to download or the run runs out of time, it delivers the finished days and
  says so: the status message starts with **PARTIAL DATA**, the log has a `PARTIAL SOURCE` line with the missing days,
  and `RUN_SUMMARY` lists `sourceStatus`, `missingDays` and `warnings`. You only pay for rows that are delivered. If
  Find a Tender is unreachable, the run ends *Failed* with a clear message. Nothing is charged and the monitor state is
  left unchanged. The API is given up after 210 s in short monitor runs; backfills get 60 s plus 25 s per day of the
  window (about 14 minutes for 31 days), so a run never gets stuck.

### Input examples

#### 1. IT services, 7-day scan (the form's trial input)

```json
{
  "mode": "backfill",
  "cpvCodes": ["72", "48"],
  "keywords": ["software", "cloud", "cyber security", "data platform"],
  "maxResults": 25
}
```

#### 2. Daily bid alert: IT and managed services in the North of England

```json
{
  "mode": "monitor",
  "profileName": "it-services-north",
  "cpvCodes": ["72", "48"],
  "keywords": ["software", "cloud", "cyber security", "data platform", "managed service"],
  "excludeKeywords": ["catering"],
  "regions": ["North West", "Yorkshire", "North East"],
  "minValue": 25000,
  "minScore": 50,
  "lookbackDays": 1
}
```

#### 3. Cleaning SME in London and the South East, below-threshold contracts only

```json
{
  "mode": "backfill",
  "profileName": "cleaning-sme-london",
  "cpvCodes": ["909"],
  "keywords": ["cleaning", "janitorial", "window cleaning"],
  "regions": ["London", "South East"],
  "thresholdLevels": ["below"],
  "smeSuitableOnly": true,
  "maxResults": 100,
  "sortBy": "deadline"
}
```

#### 4. Competitor tracking (awards)

```json
{
  "mode": "awards",
  "profileName": "competitor-watch",
  "cpvCodes": ["72"],
  "competitorNames": ["Capita", "Computacenter", "Softcat"],
  "maxResults": 0
}
```

#### 5. Unscored feed: every new tender in Scotland this week

```json
{
  "mode": "backfill",
  "regions": ["Scotland"],
  "maxResults": 0,
  "includeDescription": false
}
```

Ready-to-run versions of these inputs are in the `examples/` folder of the source code.

#### Input reference (short)

| Field | Meaning |
|---|---|
| `cpvCodes` | CPV prefixes (2–8 digits). `72` = IT services, `48` = software, `45` = construction works, `909` = cleaning, `7971` = security services, `85` = health and social work, `80` = education and training. |
| `keywords` / `excludeKeywords` | English terms. Case is ignored; 4+ letter terms also match inside longer words, shorter ones only as whole words. `"exact phrase"` in double quotes. |
| `regions` | Region names (`London`, `North West`, `Yorkshire`, `Midlands`, `Wales`, `Scotland`, `Northern Ireland`, `England`) or ITL/NUTS codes (`UKI`, `UKD3`, `TLI3`). Delivery location first, else buyer address. |
| `contractTypes` | `goods`, `services`, `works`. |
| `thresholdLevels` | `above`, `below` (the procurement thresholds). Notices that don't say are kept. |
| `smeSuitableOnly` / `vcseSuitableOnly` | Only tenders the buyer marked as suitable for SMEs / for voluntary, community and social enterprises. |
| `minValue` / `maxValue` | Estimated value in GBP excl. VAT (awards: awarded value). Notices without a value are kept and say so. |
| `minScore` | 0–100. Only notices at or above it are returned and charged. 40 is a good default; 60 is strict. |
| `onlyOpen` | Drop tenders whose deadline has passed (default `true`). |
| `competitorNames` | Awards only: keep notices won by a supplier whose name contains one of these strings. |
| `noticeTypes` | `tender` (default), `planning`, `award`, `cancellation`, `modification`, `other`. |
| `maxResults` | Hard cap on rows (and therefore on cost) per run, best matches first. Default 200 (the Console form prefills 25). 0 = no limit. |
| `sortBy` | `relevance`, `deadline`, `publicationDate` or `value`. |
| `stateKey` / `resetState` | Separate monitor memory per profile, or start fresh. |
| `firstRunBackfillDays` | Monitor only: how far a first run with no saved state looks back if its normal window is empty. Default 7, 0 = off. |

Without `cpvCodes`, `keywords` or `competitorNames`, the Actor returns an **unscored feed** (all notices that pass the
filters), charged at the lower `notice` price.

### Sample output

One row from the trial input on 5 Oct 2026 (five real rows are in `sample-output.json`):

```json
{
  "noticeId": "091992-2026",
  "ocid": "ocds-h6vhtk-077cc1",
  "noticeType": "tender",
  "isUpdate": false,
  "title": "ICT Data Platform Consultancy Services",
  "description": "Ayrshire College is seeking to appoint a suitably qualified and experienced technical specialist partner to support the development of a modern, secure and scalable enterprise data platform capability for Ayrshire College.",
  "buyerName": "Ayrshire College",
  "buyerCity": "Kilmarnock",
  "buyerPostalCode": "KA1 3HY",
  "buyerRegionCode": "UKM93",
  "buyerRegion": "Scotland",
  "buyerEmail": "procurement@ayrshire.ac.uk",
  "buyerContactPoint": "Procurement Manager",
  "deliveryRegions": ["UKM93"],
  "cpvMain": "72220000",
  "cpvCodes": ["72220000"],
  "contractNature": "services",
  "procedureType": "Open procedure",
  "legalBasis": "PCR 2015 (Directive 2014/24/EU)",
  "estimatedValue": 200000.0,
  "currency": "GBP",
  "submissionDeadline": "2026-11-11T12:00:00+00:00",
  "daysUntilDeadline": 37,
  "isOpen": true,
  "publicationDate": "2026-09-29",
  "lotCount": 1,
  "suitableForSme": false,
  "suitableForVcse": false,
  "noticeUrl": "https://www.find-tender.service.gov.uk/Notice/091992-2026",
  "relevanceScore": 75,
  "matchReasons": [
    "Main CPV 72220000 matches profile CPV 72",
    "Keyword 'data platform' in title",
    "Tender deadline in 37 day(s)"
  ],
  "profileName": "default",
  "retrievedAt": "2026-10-05T22:43:47+01:00"
}
```

(Shortened: every row has all fields, `null` where the notice doesn't say.) Award rows also carry `winnerNames`,
`awardedValue` and `awardedCurrency`. Every run stores a `RUN_SUMMARY` record in the key-value store: releases and
pages per day, failed days (`missingDays`, `sourceStatus`), merge statistics, filter counts, matches, rows pushed and
bytes downloaded.

#### Output fields

| Group | Fields |
|---|---|
| Identity | `noticeId`, `ocid`, `noticeType`, `noticeCode` (e.g. `UK4`), `noticeLabel`, `isUpdate`, `tenderStatus`, `relatedNoticeIds` |
| Content | `title`, `description` (shortened), `cpvMain`, `cpvCodes`, `contractNature`, `procedureType`, `legalBasis` (Procurement Act 2023 or PCR 2015 …), `aboveThreshold`, `lotCount`, `suitableForSme`, `suitableForVcse` |
| Buyer and place | `buyerName`, `buyerType`, `buyerCity`, `buyerPostalCode`, `buyerRegionCode`, `buyerRegion`, `buyerWebsite`, `buyerEmail` and `buyerContactPoint` (functional only), `deliveryRegions` |
| Money and dates | `estimatedValue` (excl. VAT), `estimatedValueGross`, `currency`, `publicationDate`, `submissionDeadline`, `enquiryDeadline`, `participationDeadline`, `daysUntilDeadline`, `isOpen`, `contractStart`, `contractEnd` |
| Links | `noticeUrl`, `documentsUrl` |
| Matching | `relevanceScore`, `matchReasons`, `profileName`, `retrievedAt` |
| Awards | `winnerNames`, `awardedValue`, `awardedCurrency` |

### Pricing (pay per event)

| Event | Price |
|---|---|
| Actor start | USD 0.00005 |
| `scored-match`: a notice that matched your profile at or above `minScore` | **USD 0.008** |
| `notice`: a row in an unscored feed (no CPV, keyword or competitor profile) | **USD 0.003** |

Filtered notices, notices below `minScore`, older merged versions and notices already delivered by an earlier monitor
run are **never charged**. Platform compute is included. You can cap cost with `maxResults` and with the run's
*maximum total charge*; the Actor stops cleanly when the limit is reached.

**Local test runs on 5 Oct 2026** against the live API (inputs in `examples/`; your numbers depend on the day and profile):

| Run | Rows returned | Your cost |
|---|---|---|
| Form trial: 7-day IT scan, `maxResults` 25 | 25 (33 matched) | **USD 0.20** |
| Daily `monitor`, IT in the North of England, first run (looked back 7 days) | 17 | **USD 0.14** |
| Same monitor, next run | 0 new | USD 0.00005 |
| 7-day `awards`, three IT suppliers as competitors | 3 | **USD 0.024** |
| 7-day unscored feed, Scotland | 114 | 114 × 0.003 = **USD 0.34** |

A daily monitor for a focused profile typically costs **a few US dollars per month**. You pay only for notices that matched.

### Use with AI agents (MCP)

The input schema is written so that an LLM can call the Actor without guessing: plain-English descriptions, enums,
defaults and examples. Add the Actor to the [Apify MCP server](https://mcp.apify.com) in Claude Desktop, Cursor,
VS Code or any other MCP client:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com?tools=actors,lindenwerk/uk-tender-matcher"
    }
  }
}
```

Then ask, for example: *"Find open UK tenders for cyber security services in Wales and the South West from the last 14
days that are suitable for SMEs, and list the five closest deadlines."* The agent maps this to:

- `mode: backfill`, `dateFrom`: 14 days ago
- `cpvCodes: ["72"]`, `keywords: ["cyber security", "penetration testing", "security operations"]`
- `regions: ["Wales", "South West"]`, `smeSuitableOnly: true`
- `sortBy: deadline`

It can then quote `matchReasons` to explain each result.

You can also call the Actor through the Apify API
(`POST https://api.apify.com/v2/acts/lindenwerk~uk-tender-matcher/run-sync-get-dataset-items`) or the Python/JS
clients. It connects to Make, Zapier, n8n, Google Sheets or Slack through Apify integrations.

### Data source, licence and attribution

The Actor processes **public procurement notices** that UK contracting authorities are legally required to publish.

- **Find a Tender** (UK central digital platform), Cabinet Office, through the OCDS release package API
  (`https://www.find-tender.service.gov.uk/api/1.0/ocdsReleasePackages`).
- **Licence:** notice data is published under the **Open Government Licence v3.0**
  (https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/). **Attribution: "Contains public sector
  information licensed under the Open Government Licence v3.0."** Publication policy:
  https://www.gov.uk/government/publications/open-contracting. The data was reshaped and scored by this Actor.
- This is an independent tool, not an official service of the Cabinet Office or any UK government department. Always
  check the original notice (`noticeUrl`) and the procurement documents before you bid. The official notice prevails.

### GDPR and responsible use

UK notices often name a buyer contact officer with a direct e-mail address and phone number. This Actor **drops
personal data by default**:

- It never outputs phone numbers or contact-person names.
- It keeps a buyer e-mail only if it is clearly a role mailbox (`procurement@`, `tenders@`, `commercial.team@` …);
  anything that could be personal (`first.last@`, initials) is dropped.
- It removes person-like e-mail addresses, phone numbers and `Mr/Mrs/Contact: <name>` mentions from titles and
  descriptions.
- Supplier contact details from award notices are never output, only the supplier name as published.

Organisation names (buyers and suppliers) are kept as published. A small share of award suppliers can be sole traders
trading under their own name. Use the output to find and bid for tenders and to understand your market, not for
contact harvesting or unsolicited marketing.

### Limits

- **Coverage.** Find a Tender has all above-threshold UK notices, and since 24 Feb 2025 also below-threshold notices for
  new procurements, *except below-threshold contracts in Scotland* (those stay on Public Contracts Scotland).
  Below-threshold notices of procurements that started before 24 Feb 2025 are on Contracts Finder, which v0.1 does not read.
- Not every notice states a value, region, SME flag or threshold level. Filters keep notices that don't say (except a
  region filter, which needs a delivery or buyer region).
- One run covers at most 31 days. For longer histories, run several backfills.
- A very busy day is read up to 6,000 releases (normal days have 300–900); if that ever cuts a day short, the run says so.
- If Find a Tender rate-limits heavily, the run waits as instructed; if it can't finish within its time budget, it
  delivers the complete days and marks the run PARTIAL DATA with the missing days.

### FAQ

**How is `relevanceScore` calculated?**

- CPV evidence (main CPV counts more than additional CPVs) and keyword evidence (title hits count double, rare terms
  weigh more) are combined as `100 × (1 − (1 − cpv) × (1 − keywords))`.
- In a profile with both CPV codes and keywords, a CPV-only match scores 50, one title keyword alone about 46, and CPV
  plus a title keyword about 73.
- `matchReasons` lists every contribution.

**Which deadline is `submissionDeadline`?** The tender submission deadline as published, as an ISO timestamp with the
UK offset. For two-stage procedures, `participationDeadline` holds the request-to-participate deadline, and
`enquiryDeadline` the clarification deadline.

**What happens when a buyer extends a deadline?** The update arrives as a new release. In monitor mode it isn't
re-sent (and not re-charged) if you already received the procurement; in backfill mode you get the newest version
with the old notice id in `relatedNoticeIds`.

**Can I monitor several profiles?** Yes. Create one saved task per profile, each with its own `stateKey`.

**What does a run with no matches cost?** Only the Actor start event (USD 0.00005).

**Does it cover the EU or other countries?** This Actor is UK only. For Germany and any EU country via TED, see our
[German & EU Tenders Monitor](https://apify.com/lindenwerk/german-eu-tender-matcher); for France, the
[France Tenders Monitor](https://apify.com/lindenwerk/france-tender-matcher).

### Examples

- [IT and software tenders in the UK - last 7 days](https://apify.com/lindenwerk/uk-tender-matcher/examples/uk-it-software-tenders): a published example task with a ready-made input. Open it, adjust the input and run it in your own Apify account.

### Other Lindenwerk Data Actors

- [German & EU Tenders: Public Procurement Monitor](https://apify.com/lindenwerk/german-eu-tender-matcher): Ausschreibungen from oeffentlichevergabe.de and TED, deduplicated and scored to your CPV codes and keywords.
- [France Tenders Monitor: BOAMP, TED & Marchés Publics](https://apify.com/lindenwerk/france-tender-matcher): marchés publics from BOAMP and TED in one deduplicated, scored list.
- [SAM.gov Government Contract Opportunities Monitor](https://apify.com/lindenwerk/sam-gov-contract-matcher): US federal bids and RFPs from SAM.gov, scored to your NAICS and set-asides.
- [Website Technology & Tech Stack Detector: BuiltWith Alternative](https://apify.com/lindenwerk/tech-stack-detector): CMS, shop system, analytics, consent manager and email provider of any website, in bulk.
- [SEO Audit Crawler: Broken Links, llms.txt & AI Bot Check](https://apify.com/lindenwerk/seo-audit-crawler): on-page SEO, broken links, llms.txt and AI crawler check for whole websites.
- [PDF to Markdown & RAG Chunks: Document Parser (DOCX/PPTX/XLSX)](https://apify.com/lindenwerk/pdf-to-markdown-rag): PDF, Word, PowerPoint and Excel to clean Markdown with inline tables and page-cited RAG chunks; it reads the procurement documents behind each notice's `documentsUrl`.

***

*Made by Lindenwerk Data. Contains public sector information licensed under the Open Government Licence v3.0.*

# Changelog

This Actor's version history is a separate document: https://apify.com/lindenwerk/uk-tender-matcher/changelog.md

# Actor input Schema

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

monitor = only notices not returned by a previous run with the same stateKey (schedule it daily; this is the default when the field is omitted, e.g. in API calls). backfill = every notice published or updated in dateFrom..dateTo (max 31 days; without dates: the last 7 days). awards = contract award and contract details notices only (who won what, at what price) in dateFrom..dateTo. The form opens with backfill so a first try shows the last 7 days right away; switch to monitor before you schedule it.

## `profileName` (type: `string`):

Free label for this search profile, copied to every output row. Also the default stateKey in monitor mode, so different profiles keep separate "already seen" lists. Example: "it-services-north".

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

CPV classification codes or prefixes. A prefix matches every code that starts with it: "72" = IT services, "48" = software, "45" = construction works, "909" = cleaning, "7971" = security services, "85" = health and social work, "80" = education and training. A main-CPV match gives the strongest score. Check digits like "-8" are ignored.

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

Words matched in the title (full weight) and in the description (half weight). Case-insensitive. Keywords with 4+ letters also match inside longer words ("cleaning" matches "cleanings"); shorter ones (ICT, CCTV) only as whole words. Wrap a keyword in double quotes to force whole-word matching. Rare keywords in the current batch weigh a little more than common ones.

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

Notices whose title or description contain any of these words are dropped (never charged). Same matching rules as keywords. Example: \["catering", "grounds maintenance"].

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

Keep only notices whose delivery location (else buyer address) is in one of these UK regions. Accepts ITL/NUTS codes ("UKI" London, "UKM" Scotland, "UKL" Wales, "UKN" Northern Ireland, "UKD3" Greater Manchester) or names ("London", "North West", "Yorkshire", "Midlands", "England"). Notices to be delivered across the whole UK match every region.

## `contractTypes` (type: `array`):

Keep only these contract natures. Empty = all.

## `thresholdLevels` (type: `array`):

Keep only notices above or below the procurement thresholds. Empty = all. SMEs often want \["below"] (smaller contracts, simpler procedures). Notices that do not say are kept.

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

Keep only tenders the buyer marked as suitable for small and medium-sized enterprises.

## `vcseSuitableOnly` (type: `boolean`):

Keep only tenders the buyer marked as suitable for voluntary, community and social enterprises (charities, social enterprises).

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

Drop notices whose published estimated value excl. VAT (awards mode: awarded value) is below this amount. Notices without a published value are kept and say so in matchReasons.

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

Drop notices whose published estimated value (awards mode: awarded value) is above this amount.

## `onlyOpen` (type: `boolean`):

Drop tenders whose submission deadline (else request-to-participate deadline) has already passed. Tenders without a published deadline are kept.

## `minScore` (type: `integer`):

Only notices with relevanceScore >= minScore are returned (and charged as scored-match). Guide for a CPV + keyword profile: CPV match only = 50, one title keyword only = 46, CPV + one title keyword = 73, CPV + two keywords = 85+. Ignored for an unscored feed.

## `competitorNames` (type: `array`):

Only return award notices won by an organisation whose name contains one of these strings (case-insensitive), e.g. \["Capita", "Serco"]. Use with mode = awards.

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

Monitor mode: how many days back to look on each run (plus today, UK time). 1 = yesterday and today. Larger values are safe: already delivered notices are skipped and not charged.

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

First day (YYYY-MM-DD, UK time) for backfill and awards mode. Default: dateTo minus 6 days. Ranges longer than 31 days are capped to the last 31 days.

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

Last day (YYYY-MM-DD), inclusive. Default: today.

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

Which notice types to return. Default: tender (monitor/backfill); awards mode forces award. Tender updates (changed deadlines, new documents) are merged into one row per procurement.

## `maxResults` (type: `integer`):

Maximum number of rows to return (and charge) in this run, best matches first. 0 = no limit. Default 200; the form prefills 25 for a cheap first try, so raise it (or set 0) for full results. The run also stops when your maximum total charge for the run is reached.

## `sortBy` (type: `string`):

Order of the output rows.

## `includeDescription` (type: `boolean`):

Include a shortened notice description in each row (personal contact details are removed).

## `descriptionMaxLength` (type: `integer`):

Maximum number of characters of the description per row.

## `includeFunctionalContacts` (type: `boolean`):

Include the buyer's functional contact (e.g. procurement@council.gov.uk, "Commercial Team"). Personal names, personal e-mail addresses (first.last@...) and phone numbers are never output.

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

Name of the "already seen" list kept in the named key-value store "uk-tender-matcher-state". Default: the profile name. Use a different key per profile or schedule.

## `resetState` (type: `boolean`):

Forget all notices seen by earlier runs with this stateKey before running (monitor mode).

## `firstRunBackfillDays` (type: `integer`):

Monitor mode: if a run without saved state (the very first run for this stateKey, or after Reset monitor state) finds nothing new in its lookback window, for example because it starts on a weekend or bank holiday, it looks back this many days once instead, so you see the current open tenders. Later runs are normal monitor runs. 0 = off.

## Actor input object example

```json
{
  "mode": "backfill",
  "cpvCodes": [
    "72",
    "48"
  ],
  "keywords": [
    "software",
    "cloud",
    "cyber security",
    "data platform"
  ],
  "smeSuitableOnly": false,
  "vcseSuitableOnly": false,
  "onlyOpen": true,
  "minScore": 40,
  "lookbackDays": 1,
  "maxResults": 25,
  "sortBy": "relevance",
  "includeDescription": true,
  "descriptionMaxLength": 500,
  "includeFunctionalContacts": true,
  "resetState": false,
  "firstRunBackfillDays": 7
}
```

# Actor output Schema

## `dataset` (type: `string`):

Dataset with one flat row per notice

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

Fetch, merge and filter statistics and runtime

# 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 = {
    "mode": "backfill",
    "cpvCodes": [
        "72",
        "48"
    ],
    "keywords": [
        "software",
        "cloud",
        "cyber security",
        "data platform"
    ],
    "maxResults": 25
};

// Run the Actor and wait for it to finish
const run = await client.actor("lindenwerk/uk-tender-matcher").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 = {
    "mode": "backfill",
    "cpvCodes": [
        "72",
        "48",
    ],
    "keywords": [
        "software",
        "cloud",
        "cyber security",
        "data platform",
    ],
    "maxResults": 25,
}

# Run the Actor and wait for it to finish
run = client.actor("lindenwerk/uk-tender-matcher").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 '{
  "mode": "backfill",
  "cpvCodes": [
    "72",
    "48"
  ],
  "keywords": [
    "software",
    "cloud",
    "cyber security",
    "data platform"
  ],
  "maxResults": 25
}' |
apify call lindenwerk/uk-tender-matcher --silent --output-dataset

```

## MCP server setup

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

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/4Rk8kaUYeeUPc5V1l/builds/nmlYgQe6p8WDRLFrg/openapi.json
