# EU & UK Tenders Scraper: TED, Find a Tender, Contracts Finder (`dafei-data/eu-uk-tender-scraper`) Actor

Tender scraper with bid alerts: public procurement tenders from EU TED, UK Find a Tender and Contracts Finder in one de-duplicated feed via official open APIs. Filter by keyword, CPV code, country, date. Only new tenders each run, free Excel report, no API key.

- **URL**: https://apify.com/dafei-data/eu-uk-tender-scraper.md
- **Developed by:** [dafei-data](https://apify.com/dafei-data) (community)
- **Categories:** Lead generation, Business
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.50 / 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.

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

## EU & UK Tenders Scraper: TED, Find a Tender, Contracts Finder

Get **public tenders** and **public procurement** opportunities from **EU TED** (Tenders Electronic Daily) and the UK's **Find a Tender** and **Contracts Finder** in one clean, de-duplicated dataset. The Actor reads the official open APIs (no HTML scraping, no API key). Filter by keyword, **CPV code** (prefix match), country and publication date. **Incremental mode** returns **only new tenders**, so a daily Schedule plus email or Slack gives you **bid alerts**. Every run also saves a free, **formatted Excel report** (`REPORT.xlsx`) with clickable notice links and number-formatted values, ready to forward to your bid team.

**Who it is for:** bid and sales teams selling to the public sector, consultancies tracking a sector, and anyone who wants one feed instead of checking three procurement portals.

**Keywords:** tender, tenders, tender scraper, TED tenders, Tenders Electronic Daily, public procurement, government tenders, government contracts, TED API, EU tenders, UK tenders, Find a Tender, Contracts Finder, CPV codes, bid alerts, tender alerts, RFP, Excel tender report.

> This is an independent tool. TED, Find a Tender and Contracts Finder are named only to identify the public data sources; this Actor is not affiliated with or endorsed by the EU Publications Office or the UK government.

### Formatted Excel report (free)

With `excelReport` on (default), each run writes `REPORT.xlsx` to the run's default key-value store: styled and frozen header row, auto-filters, a clickable **Open notice** link per row, the estimated value as a real number column (`#,##0.00`) and deadline / published columns as real Excel dates, so you can sort by deadline immediately. It contains the same rows as the dataset (up to 20,000), costs nothing extra and does not change the dataset. Download it from the run's **Storage → Key-value store** tab or via `https://api.apify.com/v2/key-value-stores/<storeId>/records/REPORT.xlsx`, or attach it in an email integration.

### Try it now: copy-paste input

Paste this into the **Input → JSON** tab and click **Start** (same file: `examples/readme-de-it-10.json`):

```json
{
  "cpvCodes": [
    "72000000",
    "48000000"
  ],
  "sources": [
    "ted"
  ],
  "countries": [
    "DEU"
  ],
  "maxItems": 10,
  "incremental": false
}
```

#### Sample output

![Sample output table rendered from a real run](https://api.apify.com/v2/key-value-stores/rZAPAMH2laMhiHfpr/records/tender-sample-output.png)

*Real Apify cloud run on 2026-10-27 (UTC) with exactly this input: 10 German IT tenders from TED (columns shown: noticeId, title, buyer, country, mainCpv, deadline, publishedAt; the dataset also has url, amount, currency, cpvCodes, license and attribution).*

### Quick start: ready-to-run examples

All three were run locally on 2026-10-27 with `maxItems: 5`; every returned notice carried a matching CPV code. Leave `dateFrom`/`dateTo` empty to search the last 7 days.

**1. German IT tenders** – CPV 72000000 *IT services* and 48000000 *Software packages*:

```json
{ "cpvCodes": ["72000000", "48000000"], "sources": ["ted"], "countries": ["DEU"], "maxItems": 20, "incremental": true, "stateStoreName": "watch-de-it" }
```

**2. UK construction tenders** – CPV 45000000 *Construction work* (main CPV only):

```json
{ "cpvCodes": ["45000000"], "cpvMainOnly": true, "sources": ["fts", "cf"], "countries": ["GBR"], "maxItems": 20, "incremental": true, "stateStoreName": "watch-uk-construction" }
```

**3. Medical equipment in France, Italy and Spain** – CPV 33100000 *Medical equipments*:

```json
{ "cpvCodes": ["33100000"], "sources": ["ted"], "countries": ["FRA", "ITA", "ESP"], "maxItems": 20, "incremental": true, "stateStoreName": "watch-eu-medical" }
```

Look up codes in the official CPV list: https://simap.ted.europa.eu/cpv

### Data sources

| Source | `sources` value | Official endpoint | Docs | Licence |
|---|---|---|---|---|
| EU TED (Tenders Electronic Daily) | `ted` | `POST https://api.ted.europa.eu/v3/notices/search` (anonymous) | https://docs.ted.europa.eu/api/latest/search.html | © European Union, reuse per https://ted.europa.eu/en/legal-notice |
| UK Find a Tender | `fts` | `GET https://www.find-tender.service.gov.uk/api/1.0/ocdsReleasePackages?stages=tender` (no key, OCDS) | https://www.find-tender.service.gov.uk/Developer/Documentation | Open Government Licence v3.0 |
| UK Contracts Finder | `cf` | `GET https://www.contractsfinder.service.gov.uk/Published/Notices/OCDS/Search?stages=tender` (no key, OCDS) | https://www.contractsfinder.service.gov.uk/apidocumentation | Open Government Licence v3.0 |

#### Attribution (OGL v3.0)

UK data: *Contains public sector information licensed under the Open Government Licence v3.0* (https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/). Sources: Find a Tender Service and Contracts Finder, © Crown copyright. EU data: *Source: TED – Tenders Electronic Daily, © European Union*.
Every output item carries its own `license` and `attribution` fields, so the attribution travels with the data. Keep them when you republish.

### What it does

- Queries the three official APIs, filters by keyword, country and publication date, and normalises every notice into one schema.
- **maxItems split**: `maxItems` is divided evenly between the active sources. Any quota a source cannot fill is given to the others.
- **Incremental mode**: seen notices are remembered in a *named* Key-Value Store, so scheduled runs output (and charge for) only new notices. Pair with an Apify Schedule + email/Slack/webhook integration for alerts.
- **Cross-source de-duplication** (the same tender often appears in both Find a Tender and Contracts Finder). A notice is dropped as a duplicate if:
  1. its OCDS `ocid` was already output (also catches multiple releases/updates of one procedure); **or**
  2. its normalised title + normalised buyer + deadline date (YYYY-MM-DD) match an earlier notice. Normalisation means lower-case, accents removed, and every non-alphanumeric character turned into a single space. This fallback is used only when title, buyer **and** deadline are all present.
     The first occurrence wins, in source order `ted` → `fts` → `cf` → `sam`. Keys are also stored for incremental mode, so a tender seen yesterday in one source will not reappear today from another.

#### Experimental: US SAM.gov (off by default)

Set `enableSam: true`, add `"sam"` to `sources` and supply **your own** SAM.gov API key (`samApiKey`) to also query `GET https://api.sam.gov/opportunities/v2/search` (docs: https://open.gsa.gov/api/get-opportunities-public-api/). This is **experimental**: it has only been tested against the documented sample response, never live.

### Input

| Field | Type | Description |
|---|---|---|
| `keywords` | string\[] | OR-combined. TED: full-text; UK sources: matched in title/buyer. Empty = all |
| `cpvCodes` | string\[] | CPV codes, OR-combined, prefix match (see below). Combined with `keywords` using AND. Empty = all |
| `cpvMainOnly` | boolean | Match only the main CPV code of the notice (default `false` = main + additional codes) |
| `sources` | string\[] | Default `["ted","fts","cf"]`. `sam` is experimental |
| `countries` | string\[] | ISO 3166-1 alpha-3, e.g. `DEU`, `FRA`, `USA` |
| `dateFrom` / `dateTo` | `YYYY-MM-DD` | Publication date range (default: last 7 days) |
| `maxItems` | integer | Max **new** results per run (default 100) |
| `incremental` | boolean | Only output unseen notices (default `true`) |
| `stateStoreName` | string | Named KV store for seen IDs (default `tender-seen-ids`) |
| `enableSam` | boolean | **Experimental**, default `false` |
| `samApiKey` | secret string | Experimental; your own key, used only if `enableSam` |

#### CPV codes (`cpvCodes`)

- Accepted forms: `72000000`, `72000000-8` (check digit ignored), a prefix such as `7231` or `7231*`. At least 2 digits.
- **Prefix match**: trailing zeros of an 8-digit code are hierarchy padding, so `72000000` matches every code that starts with `72` (72000000, 72212000, …) and `33100000` matches `331…`. To match only one sub-category, give a longer code, e.g. `72212000` → `72212…`.
- **TED – server-side**: the Actor adds the official TED expert-search field to the query: `classification-cpv IN (72* 48*)` (main + additional CPV), or `main-classification-proc IN (72*)` when `cpvMainOnly` is on ([TED search fields](https://docs.ted.europa.eu/ODS/latest/reuse/field-list.html), [expert query examples](https://ted.europa.eu/en/help/search-browse)).
- **Find a Tender / Contracts Finder – local filter**: their OCDS search endpoints have no CPV parameter (Find a Tender only accepts `stages, limit, cursor, updatedFrom, updatedTo`; Contracts Finder ignores unknown parameters). The Actor reads all tender notices in the date range and keeps those whose CPV codes match: `tender.classification` (main CPV), plus `tender.items[].classification/additionalClassifications` and `tender.lots[].classification`. Broad date ranges therefore take longer for UK sources.
- SAM.gov has no CPV codes, so SAM notices are dropped when `cpvCodes` is set.
- Each result includes `mainCpv` and `cpvCodes`, so you can check the match.

Example:

```json
{ "keywords": ["software", "cloud"], "sources": ["ted"], "countries": ["DEU", "FRA"],
  "dateFrom": "2026-10-01", "dateTo": "2026-10-20", "maxItems": 100, "incremental": true }
```

### Output fields

| Field | Meaning |
|---|---|
| `title` | Notice title (English preferred when available) |
| `buyer` | Contracting authority / agency |
| `country` | Buyer country, ISO alpha-3 (UK sources: `GBR`) |
| `amount` | Estimated lot value (SAM, experimental: award amount); else `null` |
| `currency` | e.g. `EUR`, `USD`; `null` if no amount |
| `deadline` | Tender submission deadline |
| `url` | Link to the official notice page |
| `source` | `ted`, `fts`, `cf` (or `sam` when experimental SAM is on) |
| `noticeId` | Stable ID, prefixed with source (`ted:681459-2026`, `fts:100684-2026`) |
| `ocid` | OCDS contracting-process ID (UK sources), else `null` |
| `publishedAt` | Publication date |
| `mainCpv` | Main CPV code of the notice (8 digits), else `null` |
| `cpvCodes` | All CPV codes of the notice (main + additional), de-duplicated |
| `license` | Licence of the source data |
| `attribution` | Required attribution statement (e.g. OGL v3.0) |

Example items (real output, 2026-10-26 run, input `{"keywords":["software","cloud"],"countries":["DEU","FRA","GBR"]}`). Each source has its own licence and attribution. (This sample predates the `mainCpv`/`cpvCodes` fields, which current runs also output.)

```json
[
  {"title":"Germany – Software implementation services – Dienstleistung zur Weiterentwicklung der Software TopEase","buyer":"Landwirtschaftliche Rentenbank","country":"DEU","amount":null,"currency":null,"deadline":null,"url":"https://ted.europa.eu/en/notice/-/detail/741107-2026","source":"ted","noticeId":"ted:741107-2026","ocid":null,"publishedAt":"2026-10-26","license":"EU Publications Office reuse policy (Commission Decision 2011/833/EU), https://ted.europa.eu/en/legal-notice","attribution":"Source: TED – Tenders Electronic Daily, © European Union, https://ted.europa.eu"},
  {"title":"C002218 Slit-lamp Simulator and all associated parts and software","buyer":"The Common Services Agency (more commonly known as Public Services Delivery Scotland) (PSD Scotland)","country":"GBR","amount":null,"currency":null,"deadline":"2026-11-20T13:00:00Z","url":"https://www.find-tender.service.gov.uk/Notice/099063-2026","source":"fts","noticeId":"fts:099063-2026","ocid":"ocds-h6vhtk-078bf8","publishedAt":"2026-10-20T14:29:22+01:00","license":"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. Source: Find a Tender Service, © Crown copyright, https://www.find-tender.service.gov.uk"},
  {"title":"Procurement of Handheld 3D SLAM scanner with a backpack mount and software","buyer":"Chemonics International, Inc.","country":"GBR","amount":1,"currency":"GBP","deadline":"2026-11-05T15:00:00Z","url":"https://www.contractsfinder.service.gov.uk/Notice/68ff1794-e6cd-4d95-8b3d-cb20dd7eba2d","source":"cf","noticeId":"cf:68ff1794-e6cd-4d95-8b3d-cb20dd7eba2d-918585","ocid":"ocds-b5fd17-57210174-8eaf-47ae-9ecc-ea509532da3b","publishedAt":"2026-10-21T07:56:09+01:00","license":"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. Source: Contracts Finder, © Crown copyright, https://www.contractsfinder.service.gov.uk"}
]
```

Note: `amount` is passed through exactly as the buyer published it. Some UK buyers enter placeholder values such as `1`.

### How to set up daily bid alerts

Combine three things: **incremental mode** (only new tenders), a **Schedule** (runs the Actor automatically) and a **notification** (email, Slack or webhook).

1. **Save a task.** Open the Actor, enter one of the examples above, keep `incremental: true` and give the watch list its own `stateStoreName` (for example `watch-de-it`). Leave `dateFrom`/`dateTo` empty so every run looks at the last 7 days. Click *Save as a new task*.
   - The seen notice IDs are stored in the named Key-Value Store, so tenders already sent are skipped and **not charged**. The 7-day window overlaps between daily runs, so nothing published late is missed.
   - Use a different `stateStoreName` for each watch list. Changing it (or deleting the store) starts from zero again.
2. **Schedule it.** In Apify Console go to *Schedules* → *Create new*, set a cron expression such as `0 7 * * *` (every day at 07:00) and your time zone, and add the saved task as the action. Docs: https://docs.apify.com/platform/schedules
3. **Get an email or Slack message.** On the task's *Integrations* tab choose one of:
   - *Send results email via Gmail*: set the recipient, subject and body, optionally attach the dataset (CSV/JSON/Excel). It fires after successful runs. Docs: https://docs.apify.com/platform/integrations/gmail
   - *Slack*: post a message to a channel after each run. Docs: https://docs.apify.com/platform/integrations/slack
   - *HTTP webhook* on the event `ACTOR.RUN.SUCCEEDED` to call your own system (CRM, Zapier, Make, n8n). It receives the run ID; fetch the new tenders from `https://api.apify.com/v2/actor-runs/{runId}/dataset/items`. Docs: https://docs.apify.com/platform/integrations/webhooks and https://docs.apify.com/platform/integrations/webhooks/events
4. **Optional: be told when something breaks.** Apify *Monitoring* can e-mail you when a run fails or the dataset is empty. Docs: https://docs.apify.com/platform/monitoring

Note: with incremental mode on, a day with no new tenders gives an empty dataset. The Gmail integration may still send an email for that successful run.

### FAQ

**Is this a TED scraper or an API?** It calls the official TED Search API (no HTML scraping), so it works like a hosted TED API with filters, de-duplication and alerts.
**Which UK tenders are covered?** Open opportunities (`stage=tender`) from Find a Tender (above-threshold) and Contracts Finder (below-threshold). The same tender listed on both is returned once.
**Do I need an API key?** No, not for TED or the UK sources. US SAM.gov is optional, experimental and needs your own key.
**How do keywords work?** For TED they are a full-text search. For the UK sources they are matched against the title and buyer.

**How do I filter by CPV code?** Use `cpvCodes` (not `keywords`). TED filters on its server; Find a Tender and Contracts Finder are filtered locally on the notice's CPV codes. Turn on `cpvMainOnly` to ignore additional CPV codes.

### Pricing

Pay per event: **$0.0035 per new tender result ($3.50 per 1,000 results)**. The run stops automatically when your max cost per run is reached. Already-seen notices in incremental mode are free.

### Compliance

- Uses only **official public APIs**; no HTML scraping, no login bypass.
- **No personal data** is collected. Contact names, e-mails and phone numbers in the source data are never read into the output: OCDS `parties[].contactPoint` (UK) and SAM.gov `pointOfContact`. Only whitelisted fields are mapped, and unit tests check this.
- (Experimental SAM.gov) SAM.gov requires each user to use **their own** API key under the SAM.gov / api.data.gov terms. The Actor never registers accounts or obtains keys.
- TED data: © European Union, reuse under the EU Publications Office legal notice. UK data: Open Government Licence v3.0, © Crown copyright (see Attribution above). SAM.gov data is US government public information.
- You are responsible for complying with each source's terms and rate limits; the Actor retries politely on HTTP 429.

### Development

```bash
npm install
npm test          # unit tests (mapping, filters, incremental dedupe, quota split, retry)
docker build -f .actor/Dockerfile -t eu-tenders-actor .
apify run -p      # local run, no account needed; input in storage/key_value_stores/default/INPUT.json
```

### Free plan limit & upgrade

- **Apify free plan:** up to **100 results per run** (the run finishes normally and the status message says when the limit was applied).
- **Paid Apify plan:** the full `maxItems` (up to 10,000 per run), same pay-per-result price.
- Upgrade your Apify plan: https://apify.com/pricing
- Run this Actor on Apify Store: https://apify.com/dafei-data/eu-uk-tender-scraper

# Actor input Schema

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

Keywords (OR-combined). TED: full-text search. UK sources: matched against title and buyer only. For CPV codes use the 'CPV codes' field below.

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

CPV codes (OR-combined, prefix match). 72000000 matches all 72xxxxxx codes; use a longer prefix such as 7221 or 72212000 to narrow. Check digit (-8) is ignored. TED: filtered server-side (classification-cpv / main-classification-proc). Find a Tender / Contracts Finder: filtered locally on the notice CPV codes. Combined with keywords using AND.

## `cpvMainOnly` (type: `boolean`):

Match only the notice's main CPV code. Off = main and additional CPV codes.

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

Data sources: 'ted' (EU TED), 'fts' (UK Find a Tender), 'cf' (UK Contracts Finder). 'sam' (US SAM.gov) is EXPERIMENTAL and only used when 'Enable SAM.gov (experimental)' is on. maxItems is split evenly across active sources.

## `countries` (type: `array`):

ISO 3166-1 alpha-3 codes, e.g. DEU, FRA, GBR. Empty = all. UK sources use GBR.

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

Default: 7 days ago

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

Default: today

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

Free Apify plan: capped at 100 results per run (paid plans: up to 10,000). Max NEW results to output across all sources

## `incremental` (type: `boolean`):

Remember seen noticeIds in a named Key-Value Store and output only unseen notices.

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

Named KV store used for incremental mode. Use different names for different watch lists.

## `enableSam` (type: `boolean`):

EXPERIMENTAL: also query US SAM.gov Opportunities API with your own API key. Off by default; not covered by the launch SLA.

## `samApiKey` (type: `string`):

EXPERIMENTAL. Your own SAM.gov API key; used only when enableSam is on.

## `excelReport` (type: `boolean`):

Also save a ready-to-share Excel file (REPORT.xlsx) to the run's default key-value store: styled header, frozen first row, filters, clickable links and number-formatted amount and date columns. Free: it does not add any charge and does not change the dataset.

## Actor input object example

```json
{
  "cpvMainOnly": false,
  "sources": [
    "ted"
  ],
  "maxItems": 10,
  "incremental": true,
  "stateStoreName": "tender-seen-ids",
  "enableSam": false,
  "excelReport": true
}
```

# Actor output Schema

## `tenders` (type: `string`):

No description

## `overview` (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": [
        "ted"
    ],
    "maxItems": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("dafei-data/eu-uk-tender-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": ["ted"],
    "maxItems": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("dafei-data/eu-uk-tender-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": [
    "ted"
  ],
  "maxItems": 10
}' |
apify call dafei-data/eu-uk-tender-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dafei-data/eu-uk-tender-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/aVd8PdYrSliTkqQ83/builds/x9hl75S9Qm1SUIAxr/openapi.json
