# TED Tenders: EU Public Procurement Search & Change Alerts (`opendata-desk/eu-tender-change-monitor`) Actor

One call returns EU public tenders from TED (civil government contracts) by keywords, countries, sectors or CPV codes, up to 90 days back. With a watch name, later runs return only new tenders, deadline changes, corrigenda and cancellations. $0.003 per notice, $0.02 per change alert, $0.005 start.

- **URL**: https://apify.com/opendata-desk/eu-tender-change-monitor.md
- **Developed by:** [TH Kim](https://apify.com/opendata-desk) (community)
- **Categories:** Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 new 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

## TED Tenders: EU Public Procurement Search & Change Alerts

**One call returns EU public tenders from [TED](https://ted.europa.eu)** (Tenders Electronic Daily, civil government
contracts only) that match your keywords, buyer countries, sectors or CPV codes, published up to 90 days back and up to
10,000 records per run. Give the search a watch name and **later runs return only what changed**: new tenders,
corrigenda, moved submission deadlines and cancellations, with the old and the new value side by side.
**Price: $0.003 per notice, $0.02 per change alert**, plus a $0.005 start at the default 512 MB.

Most TED scrapers give you raw notices. When a buyer publishes a corrigendum that moves the deadline by two weeks, it
arrives as just another notice, and you have to spot it yourself. This Actor links every change notice to the tender you
already know and tells you what changed.

### Quick start

```json
{ "sectors": ["IT services"], "countries": ["Germany"], "daysBack": 7, "maxResults": 50 }
```

This returns up to 50 German IT-services tenders (CPV division 72) published in the last 7 days. CPV codes work too:
`{ "cpvCodes": ["72"], "countries": ["DE"] }` is the same search.

### What you get

- **New tenders** that match your filters.
- **Deadline changes**: `submission_deadline` old → new, in ISO 8601 with the buyer's time zone and in UTC.
- **Corrigenda**: title, CPV, value or other tracked changes, with TED's change reason code (for example `cor-buy`,
  buyer correction).
- **Cancellations**: notices whose change reason is `cancel` or `cancel-intent`.
- **Other updates** of a tender you watch, such as its award notice.
- Title in the original language **and in English** (TED's translation), buyer name and country, CPV codes, estimated
  value and currency, TED link.

The full field list, with a title and an example for every field, is in the dataset schema (Output tab). The run's
output links point to the full dataset items (every field) and to the run summary.

A run summary is saved as `RUN-SUMMARY` in the run's default key-value store: the TED query used, the resolved filters
(`filters.countries`, `filters.cpvPrefixes`, `filters.sectors`), sector words that matched nothing
(`filters.unmatchedSectors`), match, fetch and output counts, and defence notices excluded. On a failed run, `error`
says why.

The run's status message sums up the result in one line (for example `3 changes since the last run: 2 new, 1
deadline changed.`). Apify's MCP server passes it to AI agents with the run.

### Input

In the order of the input form:

- **keywords**: full-text keywords (TED FT search); a notice matches if it contains any of them, for example `cloud`
  or `school catering`.
- **countries**: buyer countries as names (`Germany`, `Deutschland`, `Czech Republic`, `The Netherlands`) or ISO 3166
  codes (`DE`, `DEU`). EU/EEA countries, Switzerland, the UK and EU candidate countries. Empty = all countries. An
  unknown country stops the run before anything is searched.
- **sectors**: plain-language sectors, mapped to CPV codes and combined (OR) with `cpvCodes`: IT services, software,
  computer hardware, telecommunications, construction, architecture and engineering, cleaning, waste management,
  environmental services, consulting, legal services, accounting and auditing, marketing and advertising, translation,
  recruitment and staffing, printing, events, healthcare, medical equipment and pharmaceuticals, education and
  training, research and development, transport, postal and courier, energy, food and catering, furniture, office
  supplies, financial services and insurance, real estate, repair and maintenance, landscaping, clothing. Case and
  accents do not matter, and a trailing "services" or "works" is ignored ("Cleaning services" = cleaning). Words that
  match no sector are logged and listed in `RUN-SUMMARY` under `filters.unmatchedSectors`. If none matches and no CPV
  code is given, the run stops before anything is searched instead of searching all sectors. The input form suggests
  `IT services` as an example.
- **cpvCodes**: CPV codes for exact control, prefix match: `72` or `72000000` covers all of division 72 (IT services),
  `72212000` covers 72212\*. The check digit (`-8`) is ignored. Combined (OR) with `sectors`. **Default: none; with no
  sectors either, all sectors are searched.**
- **noticeTypes**: eForms notice types. Default: contract notices and PINs used as a call for competition.
- **daysBack**: publication window for a one-off search and for the first run of a watch, 0 to 90 days (default 3).
- **maxResults**: records output per run, 1 to 10,000 (default 50). Caps the cost.
- **stateKey**: the watch name (see below). Empty = one-off search.

A stopped run (unknown country, invalid CPV code, no matching sector) outputs nothing and charges no notices; the Apify
platform's start event still applies.

### Change alerts: how to watch

1. Pick your filters.
2. **Give the watch a name** in `stateKey` (for example `it-services-de-at`). The first run is a baseline and outputs
   the current matches as `new`. Every later run outputs only new and changed tenders.
3. Schedule it (daily is a good fit: TED publishes every working day).

Several independent watches: use a different `stateKey` for each. If you change the filters of an existing watch, it
starts a new baseline.

Leave `stateKey` empty for a one-off snapshot: every run returns all matches as `new` and nothing is remembered.

Watches created before 2026-09-27 keep working. Until then `cpvCodes` defaulted to `["72"]`. A saved watch that relied
on that default and still sends no CPV codes keeps `["72"]` (`RUN-SUMMARY` shows `filters.legacyCpvDefault: true`). To
widen it to all sectors, start a new watch name.

#### Example watch input

```json
{
    "keywords": [],
    "countries": ["Germany", "AT"],
    "cpvCodes": ["48000000"],
    "sectors": ["IT services"],
    "noticeTypes": ["cn-standard", "cn-social", "pin-cfc-standard", "pin-cfc-social"],
    "daysBack": 7,
    "maxResults": 200,
    "stateKey": "it-services-de-at"
}
```

#### Example output record (deadline moved by a buyer corrigendum)

```json
{
    "change_type": "deadline_changed",
    "changed_fields": [
        { "field": "submission_deadline", "old": "2026-10-01T12:00:00+02:00", "new": "2026-10-13T12:00:00+02:00" }
    ],
    "previous_notice_id": "598442-2026",
    "previous_known": true,
    "notice_id": "646410-2026",
    "procedure_id": "cdeb8789-e330-4031-a8e6-5e354dd91d3a",
    "change_reason_code": "cor-buy",
    "notice_type": "cn-standard",
    "submission_deadline": "2026-10-13T12:00:00+02:00",
    "submission_deadline_utc": "2026-10-13T10:00:00Z",
    "ted_url": "https://ted.europa.eu/en/notice/-/detail/646410-2026"
}
```

### How changes are linked

Every eForms notice carries a **procedure identifier** (BT-04) that the original contract notice and all its change notices share. The Actor remembers, per watch, the last version of each procedure it has seen and compares the tracked fields (deadline, value, currency, titles, CPV codes, buyer, notice type) when a newer notice of the same procedure appears. If a notice has no procedure identifier, it falls back to the notice UUID that the change notice points to (BT-758).

Limits: only changes published **after** your baseline are reported with old values. A change notice for a tender published before your watch started is still reported (as `corrigendum`, `deadline_changed` is not possible without the old value, or `cancelled`) with `previous_known: false`. Legacy pre-eForms notices (before November 2023) often lack a procedure identifier. A run reads at most 10,000 matching notices; narrow the filters or shorten `daysBack` if the log says the limit was hit.

### Civil procurement only

This Actor excludes defence and security procurement, in the query and again in code. A notice is dropped if its legal basis is Directive 2009/81/EC, if its notice type is a defence type (`cn-desg`, `can-desg`), or if any of its CPV codes is in division 35 (security, fire-fighting, police and defence equipment) or in a small list of defence-specific codes (50600000–50640000, 73410000, 73420000, 75220000 group). This is deliberately broad: some civil security or fire-fighting tenders in CPV division 35 are also excluded. No sector maps to these codes.

### Privacy

The Actor requests and outputs **no contact persons, e-mail addresses or phone numbers**. It outputs the buying organisation's name and country only.

### Pricing (pay per event)

| Event | Price |
|---|---|
| Actor start (each run) | $0.005 per GB of run memory: once at the default 512 MB |
| Notice (each `new` record) | $0.003 |
| Change alert (each `deadline_changed`, `corrigendum`, `cancelled` or `other_update` record) | $0.02 |

A daily watch that sees 20 new tenders and 3 changes costs about $0.125 per day ($0.005 + 20 × $0.003 + 3 × $0.02). Use `maxResults` to cap the output of a run.

### Data source and reliability

- Data comes from the official, anonymous [TED Search API v3](https://docs.ted.europa.eu/api/latest/search.html) of the EU Publications Office. No website scraping, no login.
- Scroll (iteration) mode gives a consistent snapshot of all matches. Requests back off and retry on HTTP 429 and 5xx.
- Each run re-reads the last two publication days, so a notice indexed late is not missed.
- TED data: © European Union. Reuse is governed by the [TED legal notice](https://ted.europa.eu/en/legal-notice).

### FAQ

**Why did my second run return nothing?** Nothing matching your filters changed or was published since the previous run. That is the point of a watch.

**Why did my run stop with "None of the sectors matched"?** None of your `sectors` words is in the list above and no CPV code was given. Searching all sectors instead would return (and charge) far more than you asked for, so the run stops. Check the spelling or use CPV codes.

**Where did the default CPV code 72 go?** Since 2026-09-27 an empty `cpvCodes` means all sectors. The input form now suggests the sector `IT services` (CPV 72), and older watches keep `72` (see above).

**Can I monitor a single tender?** Use keywords or a narrow CPV code; a per-procedure input is not in v1.

**Which notice types are there?** `cn-standard` (contract notice), `cn-social` (social and other specific services), `pin-cfc-*` (prior information notice used as call for competition), `qu-sy` (qualification system), `can-standard` (award), `can-modif` (contract modification), `veat` (voluntary ex-ante transparency), and more in the input form.

This Actor was built with AI assistance.

# Actor input Schema

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

Optional full-text keywords (TED FT search). A notice matches if it contains any of them, e.g. "cloud" or "school catering".

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

Buyer countries as names ("Germany", "Deutschland") or ISO 3166 codes ("DE", "DEU"). EU/EEA countries, Switzerland, the UK and EU candidate countries. Leave empty for all countries. An unknown country stops the run before anything is searched.

## `sectors` (type: `array`):

Plain-language sectors, mapped to CPV codes and combined (OR) with cpvCodes: IT services, software, computer hardware, telecommunications, construction, architecture and engineering, cleaning, waste management, environmental services, consulting, legal services, accounting and auditing, marketing and advertising, translation, recruitment and staffing, printing, events, healthcare, medical equipment and pharmaceuticals, education and training, research and development, transport, postal and courier, energy, food and catering, furniture, office supplies, financial services and insurance, real estate, repair and maintenance, landscaping, clothing. Words that match no sector are listed in RUN-SUMMARY; if none matches and no CPV code is given, the run stops before anything is searched.

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

CPV codes for exact control, optional. Prefix matching: "72" or "72000000" matches all of division 72 (IT services); "72212000" matches 72212\*. The check digit ("-8") is ignored. Combined (OR) with sectors. Leave both empty for all sectors.

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

eForms notice types to include. Change notices keep the notice type of the notice they change, so corrigenda of included types are always tracked. Defence notice types (cn-desg, can-desg) are never included.

## `daysBack` (type: `integer`):

Publication-date window in days before today, for a one-off search and for the first (baseline) run of a watch. Later runs of a watch continue from the last seen publication date (with a 2-day overlap).

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

Maximum number of records output per run (caps cost). On the baseline run, extra matches are remembered but not output. On change runs, changes over the cap are carried to the next run.

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

Set a name to monitor changes: the Actor remembers what it has seen under this name and, from the second run on, outputs only new and changed tenders (corrigenda, deadline changes, cancellations). Use one name per independent watch; changing the filters under the same name starts a new baseline. Leave empty for a one-off snapshot (every run outputs all matches as new and nothing is remembered).

## Actor input object example

```json
{
  "keywords": [],
  "countries": [],
  "sectors": [
    "IT services"
  ],
  "cpvCodes": [],
  "noticeTypes": [
    "cn-standard",
    "cn-social",
    "pin-cfc-standard",
    "pin-cfc-social"
  ],
  "daysBack": 3,
  "maxResults": 50,
  "stateKey": ""
}
```

# Actor output Schema

## `changes` (type: `string`):

All output records with every field: new tenders and, for a watch, corrigenda, deadline changes and cancellations with old and new values.

## `summary` (type: `string`):

The TED query used, resolved filters (countries, CPV prefixes, sectors), sector words that matched nothing, match and output counts. On a failed run it holds the error.

# 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 = {
    "sectors": [
        "IT services"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("opendata-desk/eu-tender-change-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 = { "sectors": ["IT services"] }

# Run the Actor and wait for it to finish
run = client.actor("opendata-desk/eu-tender-change-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 '{
  "sectors": [
    "IT services"
  ]
}' |
apify call opendata-desk/eu-tender-change-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,opendata-desk/eu-tender-change-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/tzLn0ghzjRRmsTyZa/builds/mn43iiXaWPmXsPJKZ/openapi.json
