# EU TED Tender Monitor – CPV & Keyword Alerts (`opendatalabs/eu-ted-tender-monitor`) Actor

Monitor EU public tenders from the official TED Search API (Publications Office of the EU). Filter by CPV code, keyword, country and notice type; get only new tenders on a schedule. Contact persons, e-mails, phones and street addresses are excluded by design.

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

## Pricing

from $3.00 / 1,000 tender delivereds

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 TED Tender Monitor – CPV & Keyword Alerts

Get alerts for new **EU public tenders** published on **TED (Tenders Electronic Daily)**. Filter by **CPV code**, **keyword**, **buyer country** and **notice type**, and run on a schedule to receive **only new tenders** since the last run. Data comes exclusively from the **official TED Search API** (`POST https://api.ted.europa.eu/v3/notices/search`) – no scraping, no login, no API key.

### What you get per tender

| Field | Example |
|---|---|
| publicationNumber | `756453-2026` |
| publicationDate | `2026-11-03` |
| title (English when available) | `Sweden – Information systems … – Omvärldsanalys IT` |
| buyerName / buyerCountry | `Liseberg AB` / `SWE` (organisation only) |
| mainCpv | `72222000` (notice-level main CPV) |
| cpvCodes | `["72222000"]` (all codes incl. additional/lot) |
| estimatedValue / currency | `13000000` / `SEK` |
| deadline | `2026-12-12` (earliest tender-receipt deadline) |
| procedureType / noticeType | `open` / `cn-standard` |
| tedUrl, pdfUrl, xmlUrl | links on ted.europa.eu |

**Privacy by design:** contact-person names, e-mails, phone/fax numbers and street addresses are never requested from TED and never output. The list is recorded in `RUN_SUMMARY.omittedPersonalDataFields`.

### How CPV matching works (read this)

CPV codes are hierarchical: `72000000` covers every IT-services code below it (`72222000`, `72212180`, …).

- **`cpvMatch: "main"` (default, precise):** a tender is returned only if its **notice-level main CPV** (eForms BT-262, TED field `main-classification-proc`) equals one of your codes or sits under it. Tenders where your code appears only as an *additional* or *lot-level* code are dropped for free and do not count towards `maxItems`.
- **`cpvMatch: "any"` (broad):** TED's raw match. Any CPV anywhere on the notice counts, including additional and lot codes. Example: an Irish wound-dressings framework (main CPV 33141110) lists one IT code 72212180 among 18 additional codes, so it appears under `72000000` in `any` mode but **not** in `main` mode.

**Limitation:** `main` mode can miss multi-lot tenders whose overall main CPV is in another division even though one lot is exactly what you sell (e.g. a hospital tender with main CPV 33000000 and a software lot). Use `any` mode, or add a keyword, if you need those. Older notices without a procedure-level main CPV fall back to the first CPV TED lists.

### How to run

1. Pick CPV codes (e.g. `72000000` IT services, `45000000` construction, `79400000` consulting), and/or keywords, and/or countries.
2. Keep `noticeTypes = ["cn-standard"]` for open calls for tender.
3. Set `maxItems` (default **10**, so a first run is cheap).
4. For alerts, set `mode = "new"` and schedule the Actor.

#### Sample input

```json
{
  "cpvCodes": ["72000000", "48000000"],
  "keywords": ["cloud", "data platform"],
  "countries": ["DE", "AT", "NL"],
  "noticeTypes": ["cn-standard"],
  "publishedSince": "2026-10-29",
  "mode": "all",
  "maxItems": 200
}
```

#### Ready-made weekly CPV alert (schedule)

Create a Saved Task with this input and attach an Apify Schedule with cron `0 7 * * 1` (Mondays 07:00):

```json
{
  "cpvCodes": ["72000000"],
  "countries": ["DE", "FR"],
  "noticeTypes": ["cn-standard"],
  "mode": "new",
  "maxItems": 500,
  "stateStoreName": "eu-ted-tender-monitor-state"
}
```

In `new` mode the Actor remembers delivered publication numbers in the named key-value store (one key per filter set), re-scans from 2 days before the previous run to catch late indexing, and outputs only unseen tenders. Hook a webhook/Make/Zapier/Slack integration to the dataset to get the alert delivered.

### Empty input

Running with **no filters** succeeds as a tightly capped **demo**: one free `guidance` row explaining how to set filters, plus at most **10** standard contract notices from the last 3 days. A no-filter TED query matches tens of thousands of notices per week (≈18,800 between 1–5 Nov 2026), so the cap keeps empty-input QA runs fast and costing cents.

### Pricing

Pay-per-event:

| Event | Price |
|---|---|
| Actor start | $0.005 |
| `tender` – each notice delivered in mode `all` | $0.003 |
| `tender-new` – each new notice delivered in mode `new` | $0.005 |

Filtered-out notices (including CPV mismatches dropped by `main` matching), already-seen notices and guidance rows are free. The Actor stops cleanly at your spending limit.

### Rate limits and reliability

TED's fair-usage policy allows 700 requests/minute. This Actor defaults to **60/min** (max 300), uses ITERATION pagination (`iterationNextToken`, ≤250 per page) for consistent bulk retrieval, and retries timeouts, 429 and 5xx with exponential backoff honouring `Retry-After`. Invalid expert queries fail fast with TED's error message.

### Output files

- Dataset: one row per tender (+ a guidance row on empty input).
- `RUN_SUMMARY` in the default key-value store: query used, TED total match count, requests made, delivered/skipped counts, truncation, charging counts, omitted personal-data fields, source credit.

### Source and credit

Data: **TED – Tenders Electronic Daily, © European Union, Publications Office of the European Union** (https://ted.europa.eu), retrieved via the official TED Search API. Reuse is authorised under the TED legal notice (https://ted.europa.eu/en/legal-notice). **Changes made:** fields are selected and normalised (English title chosen when available, lot values summed, earliest deadline chosen, personal data removed, notices filtered to the requested main CPV by default). This Actor is not affiliated with or endorsed by the Publications Office.

# Actor input Schema

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

Common Procurement Vocabulary codes to match, e.g. 72000000 (IT services) or 45000000 (construction). Check digits like 72000000-5 and short prefixes like 72 are accepted. A code matches itself and everything under it (72000000 covers 72222000).

## `cpvMatch` (type: `string`):

main (recommended) = keep a notice only if its notice-level MAIN CPV code is one of your codes or under it. any = TED's raw match, which also returns notices where your code appears only as an additional or lot-level code (e.g. a wound-dressings tender listing one IT sub-code under 72000000).

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

Full-text phrases matched anywhere in the notice (any language). Several keywords are combined with OR. Letters, digits, spaces and basic punctuation only.

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

Buyer country codes, ISO 3166 alpha-2 (DE) or alpha-3 (DEU). Leave empty for all countries publishing on TED.

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

TED notice types to include. For open tenders you can bid on, use cn-standard (and cn-social). Leave empty for every type.

## `publishedSince` (type: `string`):

Only notices published on or after this date (YYYY-MM-DD). Defaults to 7 days ago (3 days for the empty-input demo).

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

all = every matching notice in the window. new = only notices not delivered by a previous run with the same filters (state kept in a named key-value store). Use new on a schedule.

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

Hard cap on tenders delivered per run, keeping the first run cheap. Default 10; maximum 10,000. Empty-input demo runs are always capped at 10.

## `expertQuery` (type: `string`):

Optional extra clause in TED expert-query syntax, AND-ed with the filters above, e.g. procedure-type IN (open). See docs.ted.europa.eu for field names.

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

Named key-value store that remembers delivered notices for mode new. Each distinct filter set gets its own key, so schedules can share one store.

## `pageSize` (type: `integer`):

Notices requested per TED API call (max 250, the API limit). Larger pages mean fewer requests.

## `requestsPerMinute` (type: `integer`):

Client-side rate limit for the TED Search API. TED fair use allows 700 per minute; this Actor defaults to 60 and never exceeds 300.

## `maxRetries` (type: `integer`):

Total attempts for a TED request on timeouts, 429 and 5xx, with exponential backoff and Retry-After support.

## `requestTimeoutSecs` (type: `integer`):

Per-request timeout for calls to the TED Search API before a retry is attempted.

## Actor input object example

```json
{
  "cpvCodes": [
    "72000000"
  ],
  "cpvMatch": "main",
  "noticeTypes": [
    "cn-standard"
  ],
  "mode": "all",
  "maxItems": 10,
  "stateStoreName": "eu-ted-tender-monitor-state",
  "pageSize": 100,
  "requestsPerMinute": 60,
  "maxRetries": 4,
  "requestTimeoutSecs": 60
}
```

# Actor output Schema

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

One row per delivered TED notice (plus a free guidance row on empty input).

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

TED query used, match count, requests made, delivered/skipped/filtered counts, charging and source credit.

# 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 = {
    "cpvCodes": [
        "72000000"
    ],
    "noticeTypes": [
        "cn-standard"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("opendatalabs/eu-ted-tender-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 = {
    "cpvCodes": ["72000000"],
    "noticeTypes": ["cn-standard"],
}

# Run the Actor and wait for it to finish
run = client.actor("opendatalabs/eu-ted-tender-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 '{
  "cpvCodes": [
    "72000000"
  ],
  "noticeTypes": [
    "cn-standard"
  ]
}' |
apify call opendatalabs/eu-ted-tender-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,opendatalabs/eu-ted-tender-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/v0WqaNOMr26No4ZMx/builds/YWQbf65yBEbeD42FO/openapi.json
