# EU Tender Monitor - TED Alerts (`lowlanddata/eu-tender-monitor`) Actor

Get alerted the moment a new EU public tender matching your business is published on TED. Filter by CPV codes, keywords in any EU language, countries and NUTS regions; alerts only on never-seen tenders via the official TED API. Clean output, no buyer contact persons.

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

## Pricing

from $1.60 / 1,000 baseline tenders

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## EU Tender Monitor — TED Alerts

Watches TED (Tenders Electronic Daily, the EU's official procurement journal — every
above-threshold public tender in the Union) for notices matching your buyer profile:
CPV codes, keywords in any EU language, countries, NUTS regions, value floor. It
alerts the moment a matching tender is published, and it remembers what it has
already told you — so an alert always means something changed, never a re-scrape of
yesterday's list.

It reads the official TED Search API — the access route the EU itself provides for
reuse — and the output is GDPR-clean by construction: buyer organizations, CPV
codes, deadlines and values, but no contact persons, no emails, no phone numbers.
Safe to store, share with your bid team, or import into a CRM.

### Quick start (30 seconds, no account or API keys needed)

1. Run the actor with the prefilled input (IT-services tenders, EU-wide, last 3
   days) — it finishes in a few seconds and shows a baseline.
2. Replace the demo profile with your own: your CPV codes, your keywords, your
   countries or NUTS regions.
3. Add a **daily schedule** (Actor page → Schedule). From then on, runs are quiet
   until a tender you care about is actually published.

### Who uses this

- **A software agency in Antwerp** monitors `cpvCodes: ["72000000"]`,
  `countries: ["BE", "NL"]` on a daily schedule. When a hospital publishes a
  systems-integration tender, the alert lands with the `deadline` and the
  `ted.europa.eu` link the same day — not when a sales rep stumbles on it in week
  three of a five-week window.
- **A bid manager at a construction group** watches `nutsCodes: ["BE2"]` (Flanders)
  plus `cpvCodes: ["45000000"]` with `minValueEur: 1000000` — only the region she
  can actually serve, only projects worth mobilizing for.
- **A consultancy tracking its competitors** monitors `noticeTypes: ["award"]` on
  its CPV family: `winnerNames` and `totalValueEur` show who wins what, at what
  price, across every EU member state.
- **A market-entry analyst** runs `keywords: ["laboratorium", "laboratory"]`
  EU-wide for a quarter of demand data — TED translates every title into all 24 EU
  languages, so keywords match tenders from Portugal to Finland.
- **A grants-and-tenders desk** pairs this with its US sibling
  ([US RFP Monitor](https://apify.com/lowlanddata/us-rfp-monitor)) for one
  transatlantic pipeline with identical alert semantics.

### What you get

One JSON item per **event**: a baseline snapshot per tender on the first run, then
only transitions — new matching tenders, and (opt-in) corrigenda or change notices
to tenders you have already seen. A new-tender alert looks like:

```json
{
  "publicationNumber": "495369-2026",
  "title": "Poland – Software package and information systems – Dostawa i wdrożenie systemu informatycznego",
  "buyerName": "Szpital Wojewódzki w Katowicach",
  "buyerCountry": "POL",
  "cpvCodes": ["48000000", "72263000"],
  "nutsCodes": ["PL22A"],
  "noticeCategory": "contract-notice",
  "sourceNoticeType": "cn-standard",
  "description": "Przedmiotem zamówienia jest dostawa i wdrożenie systemu informatycznego wraz z usługami utrzymania.",
  "sourceLanguage": "pol",
  "publicationDate": "2026-07-17",
  "deadline": "2026-08-20",
  "estimatedValueEur": 1030063,
  "url": "https://ted.europa.eu/en/notice/-/detail/495369-2026",
  "isBaseline": false,
  "isUpdate": false
}
```

Field honesty notes:

- `title` is TED's official translation (all 24 EU languages exist; you get
  English). Its pattern is "Country – CPV label – buyer's own words", and the
  buyer's own words stay in the source language.
- `description` is source-language only — TED does not translate descriptions.
  `sourceLanguage` tells you which.
- `estimatedValueEur` / `totalValueEur` appear only when the buyer published a
  value — most notices carry none, and absence does not mean small.
- `deadline` is the earliest lot deadline on the notice; always verify on the
  linked notice before planning a bid.
- Award notices carry `winnerNames` and `winnerCountries` — organization names as
  the EU officially publishes them, no contact data.

### How much does EU tender monitoring cost?

Pay per outcome — checking costs nothing; you pay when the monitor tells you
something new:

- **New matching tender (the alert): $0.02** — also for change alerts.
- Baseline item (first run only): $0.002.
- Actor start: $0.005. Everything included - there are no separate platform-usage
  charges, and higher Apify plans get automatic unit-price discounts.

Cost feel: a focused profile (one CPV family, two countries) sees a handful of new
tenders per working day → roughly **$2–8/month** for complete coverage of your
niche, with the one-time baseline (say 100 items) costing about $0.20. A run with
zero new matches costs $0.005 all-in. Dedicated EU
tender-alert services charge €30–200/month for the same coverage.

#### Why an alert costs more than a scraped row

The store's headline ("from $2.00 / 1,000 baseline results") is what a trial costs:
a 100-item baseline ≈ $0.20. Apify displays every event per 1,000, so in the
pricing table the alert event reads "$20.00 / 1,000" next to raw scrapers at $1–3
per 1,000 rows — but that comparison is between different things:

- A **raw TED scraper** sells rows: to monitor with rows you re-download your whole
  query every day (~3,500 EU notices/working day) and diff them yourself.
- **This monitor** sells the transition: it remembers state in your account and
  charges $0.02 exactly when a tender you have not seen matches your profile.
  Nobody buys 1,000 alerts a month.

### Not technical? Let your AI assistant set it up

Paste this prompt together with a description of what you sell and where:

```text
Set up the Apify actor lowlanddata/eu-tender-monitor to watch EU public tenders
for me. Build the input JSON from my business description:
- cpvCodes: the EU CPV classification codes for my products/services (8 digits,
  e.g. 72000000 for IT services; a prefix like "72" also works)
- keywords: 3-8 words in any EU language a tender title would contain (they match
  TED's official titles in all 24 languages)
- countries: ISO codes of countries I can serve (e.g. ["BE", "NL"]), or
  nutsCodes for regions (e.g. ["BE2"] for Flanders)
- noticeTypes: ["contract-notice"] to bid, add "award" to track winners
- minValueEur only if I gave a minimum deal size
Leave alertOn, emitBaseline and maxItems at their defaults. Run it once and show
me the baseline as a table (title, buyer, country, deadline, value, url). Then
create a daily schedule for it and set up a notification so I get an email when
a run produces items.
```

***

### Coverage — measured, not promised

- **Source:** the official TED Search API, keyless. TED carries every
  above-EU-threshold tender from all member states plus EEA and enlargement
  countries — ~3,500–4,100 notices per working day (measured July 2026), zero on
  weekends (the OJ S publishes working days only).
- **Below-threshold national tenders are out of scope by design** — they live on
  national portals (BOAMP, TenderNed, PLACSP, …), not TED. This actor is the EU
  layer; it does not pretend otherwise.
- **Keyword matching:** titles in all 24 EU languages (server-side, official TED
  index). Descriptions only in their source language, and only with
  `alsoMatchDescriptions: true`. CPV codes are the reliable, language-free filter —
  prefer them.
- A profile broader than ~10,000 notices per run is truncated honestly (the run
  status says so and suggests narrowing) — precision over silent gaps.

### How the monitor thinks

- **First run = baseline.** It fetches your profile's matches from the last
  `lookbackDays` (default 7), pushes them flagged `isBaseline: true` at the low
  baseline price, and memorizes them. `emitBaseline: false` seeds silently instead.
- **After that, only transitions.** Each run queries from the previous run (one
  day of overlap; the memory absorbs duplicates) and pushes only tenders it has
  never alerted. Corrigenda and change notices to seen tenders alert as
  `isUpdate: true` when `alertOn: "new-and-updated"`; with `alertOn: "new"` they
  stay silent but are remembered, so switching later never replays history.
- **Editing the profile starts a fresh baseline** — matching-relevant inputs are
  the profile's identity, so a changed profile can never flood you with alerts
  computed against another profile's memory. `maxItems`, `emitBaseline` and
  schedule changes do not restart anything.
- **Memory** lives in a named key-value store (`stateStoreName`, default
  `ted-monitor-state`) in your account. Run several profiles on separate schedules
  against the same store — they never interfere. `resetState: true` wipes the
  current profile's memory.
- Re-running after a crash or overlap re-alerts at most the last few unsaved items
  — state is checkpointed during the run.

### Input reference

| Field                   | Type    | Default               | Notes                                                                                     |
| ----------------------- | ------- | --------------------- | ----------------------------------------------------------------------------------------- |
| `keywords`              | array   | `[]`                  | Any EU language; matched against official titles in all 24 languages. OR within the list. |
| `cpvCodes`              | array   | `["72000000"]`\*      | Hierarchical: `72000000` or the prefix `72` match all IT-services codes.                  |
| `countries`             | array   | `[]`                  | Buyer country, 2- or 3-letter ISO (`BE` or `BEL`).                                        |
| `nutsCodes`             | array   | `[]`                  | Place of performance, prefix match (`BE2` = Flanders).                                    |
| `noticeTypes`           | array   | `["contract-notice"]` | Also: `prior-information`, `award`.                                                       |
| `alsoMatchDescriptions` | boolean | `false`               | Descriptions are source-language only — see coverage notes.                               |
| `minValueEur`           | integer | —                     | Tenders without a published value always pass.                                            |
| `lookbackDays`          | integer | `7`                   | Baseline reach on the first run (max 30).                                                 |
| `alertOn`               | string  | `"new"`               | `"new-and-updated"` adds corrigendum/change alerts.                                       |
| `emitBaseline`          | boolean | `true`                | `false` = silent seeding.                                                                 |
| `resetState`            | boolean | `false`               | One-shot: wipe this profile's memory.                                                     |
| `maxItems`              | integer | `500`                 | Overflow alerts on the next run, never lost.                                              |
| `stateStoreName`        | string  | `"ted-monitor-state"` | Only change to fully isolate monitor instances.                                           |

\* At least one of `keywords`, `cpvCodes`, `countries`, `nutsCodes` is required —
an unfiltered profile would be all of TED.

### Scheduling and API use

Run it daily (TED publishes working days only — weekend runs simply find nothing
and cost $0.005). Pair the schedule with a Monitoring alert on "dataset items

> 0" to get an email exactly when a tender matched.

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/lowlanddata~eu-tender-monitor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"cpvCodes": ["72000000"], "countries": ["BE"], "lookbackDays": 7}'
```

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('lowlanddata/eu-tender-monitor').call({
  cpvCodes: ['72000000'],
  countries: ['BE', 'NL'],
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items); // only new tenders since the last run
```

```python
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("lowlanddata/eu-tender-monitor").call(run_input={
    "cpvCodes": ["72000000"],
    "countries": ["BE", "NL"],
})
items = client.dataset(run["defaultDatasetId"]).list_items().items
```

Typical daily runs finish in well under a minute, comfortably inside the 300s
sync-call wall; a 30-day EU-wide baseline on a broad CPV family is the one case
that can take a few minutes — run it async once.

### Use it with AI agents (MCP)

Claude, Cursor and other MCP-capable agents can run this monitor as a tool through
Apify's hosted MCP server: the agent builds the profile from a plain-language
brief, runs it, and reads the alerts back — no glue code.

Claude Code:

```bash
claude mcp add apify --transport http "https://mcp.apify.com?actors=lowlanddata/eu-tender-monitor"
```

Cursor or Claude Desktop (add a custom connector / MCP server with this URL):

```text
https://mcp.apify.com?actors=lowlanddata/eu-tender-monitor
```

Sign in with your Apify account when prompted — runs are billed to it. Setup
details per client: [Apify MCP docs](https://docs.apify.com/platform/integrations/mcp),
or start from this actor's own MCP page:
[apify.com/lowlanddata/eu-tender-monitor/api/mcp](https://apify.com/lowlanddata/eu-tender-monitor/api/mcp).

Prompts that work once connected:

- "Watch EU tenders for cloud migration services in Belgium and the Netherlands
  and show me today's matches."
- "What IT tenders over €500k were published in Flanders this week?"
- "Who won public software tenders in Spain this month, and at what value?"

### Is it legal to monitor EU tenders?

Yes — unambiguously. TED is the EU's official publication channel and its notices
"can be freely reused, for commercial or non-commercial purposes" under the
European Commission's reuse decision (2011/833/EU); this actor reads the official
TED Search API that exists exactly for this. No login walls, no anti-bot bypass —
if TED is unreachable, the run reports it rather than fighting it. And the output
is safe by construction: no personal data is requested from the API in the first
place (see the compliance section below).

Data source: Tenders Electronic Daily (TED), © European Union,
[ted.europa.eu](https://ted.europa.eu) — reused under Commission Decision
2011/833/EU.

### Compliance: safe to store, safe to CRM-import

TED notices do contain personal data — buyer contact persons with direct emails
and phone numbers, and beneficial-ownership names. This actor's guarantee is
structural, not a filter: the TED API only returns fields that are explicitly
requested, and the request whitelist simply never includes the person, contact,
email, phone or beneficial-owner field families — the data never even crosses the
wire, and an automated test proves that a response carrying such fields cannot
reach the output. What remains is organization-level: buyer names, winner
organization names, codes, dates, values. One honest limit: `description` and
`title` contain text the buyer wrote — on rare occasions a buyer writes a person's
name into free text. We cannot rewrite official notices; everything structured is
guaranteed clean.

### FAQ

**Why did my second run return nothing?** Because nothing new matched — that is
the product working. The first run is the baseline; afterwards you only pay for
tenders you have not seen. Check the run status line: it says how many notices
were checked.

**Does the output contain buyer contact persons or emails?** No — by design. The
API request itself never asks for those fields. You get the buying organization's
name and the official notice link; contact details are on the notice page if you
need them for a bid.

**Which countries does this cover?** All EU member states plus EEA and several
enlargement/neighbourhood countries — anything published on TED. Filter with
`countries` (buyer's country) or `nutsCodes` (where the work is performed).

**Can I monitor several profiles?** Yes — one schedule per profile, same actor,
same state store. Each profile keeps its own memory; they never interfere.

**Do I need to write keywords in every language?** No. TED translates titles into
all 24 EU languages and the search covers all of them — "software" matches a
Polish notice. Descriptions are the exception (source language only), which is why
CPV codes are the recommended primary filter.

**What about tenders below the EU thresholds?** They are published on national
portals, not TED, and are out of this actor's scope by design.

**What is a corrigendum alert?** Buyers amend notices — deadline moves, changed
lots. With `alertOn: "new-and-updated"` you get those as `isUpdate: true` items;
with the default you never hear about them (but they are remembered).

### Troubleshooting

- **"The profile matches all of TED (~3,500 notices/day)"** — add at least one of
  `keywords`, `cpvCodes`, `countries` or `nutsCodes`.
- **"TED rejected the search query"** — usually a malformed filter value; check
  CPV codes are digits and country codes are real ISO codes.
- **"Monitor state record … is not readable"** — the state was written by a newer
  version; run once with `resetState: true` to start a fresh baseline.
- **"Coverage truncated by the per-run request budget"** — the profile matched
  more than ~10,000 notices in one window; narrow it or lower `lookbackDays`.

Something else off? Open an issue on the actor's Issues tab — reports are read and
answered.

# Actor input Schema

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

Alert on tenders whose title contains ANY of these words - in any EU language. TED translates every title into all 24 official languages and the search covers all of them, so "software", "logiciel" or "Straßenbau" all work. Example: \["software", "cloud"]. Combine with CPV codes for precision; at least one of keywords, cpvCodes, countries or nutsCodes is required.

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

EU procurement classification codes (CPV), the most reliable filter - language-free. Hierarchical: "72000000" (IT services) matches every sub-code like 72266000, and a short prefix such as "72" works too. Example: \["72000000", "48000000"]. Find codes at simap.ted.europa.eu (CPV).

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

Only alert on tenders from buyers in these countries. Two- or three-letter ISO codes, e.g. \["BE", "NL"] or \["BEL", "NLD"]. Empty = all of TED (EU/EEA and beyond).

## `nutsCodes` (type: `array`):

Only alert on tenders performed in these EU NUTS regions, hierarchical prefix match: "BE2" covers all of Flanders, "FR10" the Paris region. Tenders whose place of performance is a whole country match via that country's code in countries. Example: \["BE2", "NL3"].

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

Which notice stages to monitor. Default: contract notices (open competitions you can still bid on). Add prior-information notices to see demand earlier, or award notices to track who wins.

## `alsoMatchDescriptions` (type: `boolean`):

Additionally match your keywords against tender descriptions. Honest limitation: unlike titles, descriptions exist only in the notice's source language, so a Polish tender's description only matches Polish keywords. Titles are always matched in all 24 languages.

## `minValueEur` (type: `integer`):

Only alert on tenders with an estimated or total value of at least this many euros. Tenders that publish no value (most of them) always pass this filter - TED makes values optional, and silently dropping them would hide real opportunities.

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

How many days of past notices the FIRST run of a profile loads as its baseline. Later runs automatically query from the previous run with one day of overlap - re-alerting a seen notice is impossible. TED publishes on working days only, so weekend runs find nothing new.

## `alertOn` (type: `string`):

"new" alerts once per tender. "new-and-updated" also alerts when a tender you have already seen is changed by a later corrigendum or change notice (marked isUpdate: true).

## `emitBaseline` (type: `boolean`):

On the very first run of a profile, push the currently matching tenders (flagged isBaseline: true, charged at the lower baseline price) so you start with today's full picture. Turn off to seed silently and only ever receive new alerts.

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

Discard this profile's memory of already-seen tenders and start over with a fresh baseline run. One-shot switch: turn it back off afterwards.

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

Upper bound on items pushed per run (baseline or alerts). Matches beyond the cap are not lost: they alert on the next run.

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

Named key-value store holding the monitor's seen-tender memory. Only change this to run fully isolated monitor instances.

## Actor input object example

```json
{
  "keywords": [],
  "cpvCodes": [
    "72000000"
  ],
  "noticeTypes": [
    "contract-notice"
  ],
  "alsoMatchDescriptions": false,
  "lookbackDays": 3,
  "alertOn": "new",
  "emitBaseline": true,
  "resetState": false,
  "maxItems": 10,
  "stateStoreName": "ted-monitor-state"
}
```

# Actor output Schema

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

EU procurement notices matching your profile, one JSON object per notice. On the first run: the current baseline; afterwards: only new (or changed) matches.

# 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 = {
    "keywords": [],
    "cpvCodes": [
        "72000000"
    ],
    "noticeTypes": [
        "contract-notice"
    ],
    "lookbackDays": 3,
    "maxItems": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("lowlanddata/eu-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 = {
    "keywords": [],
    "cpvCodes": ["72000000"],
    "noticeTypes": ["contract-notice"],
    "lookbackDays": 3,
    "maxItems": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("lowlanddata/eu-tender-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "keywords": [],
  "cpvCodes": [
    "72000000"
  ],
  "noticeTypes": [
    "contract-notice"
  ],
  "lookbackDays": 3,
  "maxItems": 10
}' |
apify call lowlanddata/eu-tender-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=lowlanddata/eu-tender-monitor",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/acts/SieZKOjEctULJ9dvi/builds/2TLrJoJBkTmoUn8eW/openapi.json
