# TED Tenders: Romania & EU notices by CPV (official API) (`clearsource/ted-tenders`) Actor

Search EU public tenders from TED (Tenders Electronic Daily) by CPV code, buyer country, notice type and keywords. Clean JSON with provenance on every record, an incremental mode for daily alerts, and a daily self-check. Data from the official TED Search API.

- **URL**: https://apify.com/clearsource/ted-tenders.md
- **Developed by:** [PPFTEC S.R.L](https://apify.com/clearsource) (community)
- **Categories:** Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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 EU tenders

> **Status: not published.** 0.1.5, 2026-10-04. The source is *allow-with-conditions* in `docs/compliance/sources.md` and was approved by Petre on 2026-10-04 (DECISIONS D1). Operator: PPFTEC S.R.L. (https://ppftec.com). First live health check passed on 2026-10-04 (`docs/qa/ted-tenders-live-2026-10-04.md`); no Apify run yet (D2b). Prices below are hypotheses (D4).

Search public tenders published in **TED (Tenders Electronic Daily)**, the EU's official procurement journal, through the official, anonymous **TED Search API v3**. Each run returns clean, flat notice records (title in your language, buyer, country, CPV codes, estimated value, deadlines, links) with full provenance on every record.

**For:** bid and sales teams watching tenders in their sector, procurement consultancies and agencies, data teams, and AI agents that need EU tender data as JSON.

### Quick start

RO IT tenders from the last 7 days:

```json input
{
  "countries": ["ROU"],
  "cpvCodes": ["72000000"],
  "publishedFrom": "7 days",
  "language": "ron",
  "maxResults": 200
}
```

**Daily Romanian tenders above the EU threshold.** TED publishes the Romanian notices that are above the EU thresholds (below-threshold Romanian procedures are only in SEAP). Run this once a day with an Apify schedule (e.g. 07:00 Europe/Bucharest); each run returns only the notices published since the previous one, marked `is_new: true`:

```json input
{
  "countries": ["ROU"],
  "noticeTypes": ["cn-standard", "cn-social"],
  "publishedFrom": "3 days",
  "incremental": true,
  "stateKey": "daily-ted-ro",
  "language": "ron",
  "maxResults": 500
}
```

Construction (CPV 45000000) in Germany and Austria, only notices that are new since the last run (schedule it daily):

```json input
{
  "countries": ["DEU", "AUT"],
  "cpvCodes": ["45000000"],
  "noticeTypes": ["cn-standard"],
  "incremental": true,
  "stateKey": "construction-de-at"
}
```

Expert query (TED syntax, overrides every filter above, dates included; cannot be combined with `incremental`):

```json input
{
  "expertQuery": "buyer-country IN (FRA) AND FT ~ (cybersecurity) AND publication-date >= 20260901 SORT BY publication-date DESC",
  "maxResults": 100
}
```

Validate an input without calling TED (no request, nothing emitted, nothing charged; the built query is stored under key `DRY_RUN`):

```json input
{ "keywords": ["cloud", "software"], "countries": ["ROU"], "dryRun": true }
```

### Output example

One record (from a recorded TED response, 2026-10-03). Every record validates against `src/schemas/ted-notice.schema.json` before it is pushed; a record that fails is a bug and fails the run.

```json output
{
  "record_type": "ted-notice",
  "publication_number": "678144-2026",
  "notice_version": 1,
  "notice_type": "cn-standard",
  "procedure_type": "open",
  "contract_nature": "supplies",
  "publication_date": "2026-10-02",
  "source_publication_date": "2026-10-02+02:00",
  "buyer_names": ["COMPANIA NATIONALA DE ADMINISTRARE A INFRASTRUCTURII RUTIERE S.A."],
  "buyer_country": ["ROU"],
  "buyer_city": ["Bucuresti"],
  "cpv_codes": ["34992200"],
  "place_of_performance": ["RO321", "ROU"],
  "title": "Romania – Road signs – ANSAMBLU LUMINOS PENTRU CRESTEREA SIGURANTEI RUTIERE PE SECTOARELE PERICULOASE",
  "title_language": "eng",
  "estimated_value": { "amount": 74811664, "currency": "RON", "level": "procedure" },
  "tender_deadlines": ["2026-11-02+02:00"],
  "notice_url": "https://ted.europa.eu/en/notice/-/detail/678144-2026",
  "xml_url": "https://ted.europa.eu/en/notice/678144-2026/xml",
  "matched_keywords": ["road signs"],
  "provenance": {
    "envelope_version": "1.0.0",
    "source_id": "ted-search-api-v3",
    "source_url": "https://api.ted.europa.eu/v3/notices/search",
    "source_request": {
      "method": "POST",
      "body_sha256": "104903eec02e5f9ff178ada343864de9c8ac062dc2143c9219ac186eb6da918e",
      "page": "1"
    },
    "source_record_url": "https://ted.europa.eu/en/notice/-/detail/678144-2026",
    "retrieved_at": "2026-10-03T23:48:49.847Z",
    "source_version": { "api_version": "v3", "record_version": "1" },
    "producer": { "name": "ted-tenders", "version": "0.1.5", "build": null, "run_id": null, "adapter": "ted-search-v3" },
    "record_hash": "sha256:138d91e5cea81f61d1bb84dcc89308d6297431039ba953a0527b8234314c0a5e",
    "licence": {
      "id": "LicenseRef-EC-Decision-2011-833",
      "url": "https://ted.europa.eu/en/legal-notice",
      "attribution": "Source: TED — Tenders Electronic Daily, © European Union, https://ted.europa.eu"
    },
    "personal_data": "none"
  }
}
```

### Fields

| Field | From TED | Notes |
| --- | --- | --- |
| `publication_number` | `publication-number` | Primary key, e.g. `678144-2026`. |
| `notice_version` | `notice-version` | Integer. |
| `notice_type` | `notice-type` | e.g. `cn-standard` (contract notice), `can-standard` (award). |
| `procedure_type`, `contract_nature` | `procedure-type`, `contract-nature-main-proc` | `null` when absent. |
| `publication_date` | `publication-date` | `YYYY-MM-DD`; the raw value (with offset) is in `source_publication_date`. |
| `buyer_names`, `buyer_city` | `buyer-name`, `buyer-city` | All languages flattened, de-duplicated. Organisations only. |
| `buyer_country` | `buyer-country` | ISO 3166-1 alpha-3. |
| `cpv_codes` | `classification-cpv` | De-duplicated 8-digit codes. |
| `place_of_performance` | `place-of-performance` | NUTS / country codes, de-duplicated. |
| `title`, `title_language` | `notice-title` | Your `language`, else English, else the first available. |
| `estimated_value` | `estimated-value-*` | Procedure value if present (`level: procedure`); else the sum of lot values when all lots share one currency (`lots-sum`); else `null`. |
| `tender_deadlines` | `deadline-receipt-tender-date-lot` | Verbatim source strings. |
| `notice_url`, `xml_url` | `links` | English HTML page (else first language); multilingual XML. |
| `matched_keywords` | — | Your keywords found in any title language (TED's full-text match also covers fields we do not return, so this can be empty). |
| `is_new` | — | `true` in incremental mode. |
| `provenance` | — | Source, request digest, retrieval time, versions, record hash, licence. See `docs/spec/provenance-schema.md`. |

### Pricing (pay per event)

Pay per event. You pay only for notices actually pushed.

| Event | Price | When |
| --- | --- | --- |
| `apify-actor-start` | $0.00005 (Apify default) | per run |
| `notice` | $0.002 ($2 per 1,000) | per notice pushed |

Examples: a daily incremental alert with ~20 new notices ≈ $0.04/day; a 1,000-notice backfill ≈ $2. Nothing is charged for dry runs, health runs, invalid input, TED errors or schema drift. Set a maximum charge per run in Apify; the actor stops cleanly when it is reached.

### Incremental mode and scheduling

With `incremental: true` the actor keeps a small state in your named key-value store `pda-ted-tenders-state` (key derived from your input, or your `stateKey`):

- a **watermark**: the newest publication date of the last run that went through its *whole* result set;
- the publication numbers already delivered inside the current query window.

The next run starts one day before the watermark (overlap), skips notices already delivered and marks the rest `is_new: true`. A run that stops early (your `maxResults`, your max charge, the scan cap or the run time budget) does **not** move the watermark, so the next run re-scans the same window and delivers the older notices it did not reach; nothing is skipped and nothing is charged twice. State is saved after every page.

`incremental` needs the filter fields (`keywords`, `cpvCodes`, `countries`, `noticeTypes`, dates); it is rejected together with `expertQuery`, because an expert query is sent verbatim and cannot be narrowed by date. Do not run two incremental runs with the same state key at the same time (the last one to save wins).

Schedule a daily run and connect Apify integrations (e-mail, Slack, webhook) to get alerts.

### Limits

- **Rate:** one request at a time, at least 1 s apart, and **at most 500 notices per rolling 6 minutes** (our reading of TED's fair-use limits: 700 requests/min and 600 views or downloads per 6 min per IP). That is about 80 notices per minute: a 1,000-notice backfill takes about 12 minutes. TED documents no rate-limit headers; we back off on 429/5xx (1, 2, 4, 8 s, `Retry-After` honoured, 5 attempts) and then fail.
- **Paging:** always TED's `ITERATION` mode (no 15,000-notice cap), 100 notices per page.
- **Work per run:** a run scans at most `max(10 × maxResults, 2,000)` notices (and the matching number of pages), and stops after 4 hours (or just before the Apify run timeout). At ~80 notices per minute that is roughly 19,000 notices per run; larger backfills need several incremental runs. These stops end the run with exit code 0 and a `Stopped: …` status.
- **Retry-After:** honoured in full; if TED asks us to wait longer than the remaining run time, the run stops with exit code 1 and says so.
- **CPV codes match their sub-codes:** `cpvCodes: ["72000000"]` also returns notices that only carry child codes such as `72223000` or `72268000` (live check 2026-10-04: 66 of 100 returned notices had no exact `72000000`, none lacked a `72…` code). Use the most specific code you want.
- **Titles:** TED translates titles into all EU languages; the selector offers eng, ron, deu, fra, spa, ita, pol.
- **Excluded for privacy:** contact-person names, e-mails, phones, fax numbers and winner details are never requested from TED.
- **API only:** the actor calls `api.ted.europa.eu` only; never the TED website or `ted.europa.eu/packages/*`.

### Data source and licence

Source: **TED — Tenders Electronic Daily**, Publications Office of the European Union, via the TED Search API v3 (`https://api.ted.europa.eu/v3/notices/search`, anonymous; docs: https://docs.ted.europa.eu/api/latest/index.html).

Attribution (also in every record's `provenance.licence` and in the dataset description): **Source: TED — Tenders Electronic Daily, © European Union, https://ted.europa.eu**

Reuse: TED notices may be reused for commercial or non-commercial purposes under Commission Decision 2011/833/EU (https://ted.europa.eu/en/legal-notice). **Modifications:** this actor selects 18 fields, normalises dates, flattens multilingual names, de-duplicates lists and computes `estimated_value`; the original notice is always linked in `notice_url`. This actor is **not affiliated with or endorsed by** the European Union or the Publications Office. The TED logo is not used.

### Reliability

- **Schema drift fails loudly.** Every TED response is checked against a pinned source shape (`src/source-shape.schema.json`). A missing or retyped field, a non-numeric value, or TED rejecting one of our fields stops the run with exit code 1, the status `SCHEMA DRIFT at ted-search-api-v3: …` and a `DRIFT_REPORT` (expected, actual, sample) in the run's key-value store. Records already pushed stay valid. New unknown keys are only warnings (`DRIFT_REPORT.warnings`).
- **Health checks.** `mode: "health"` (hidden) runs a canary: TED syntax check on our field list, a 10-notice search for Romanian notices of the last 7 days (shape + output schema), and the API doc hash; it pushes a record to the named dataset `pda-health` with p50/p95 latency. Planned schedule: daily, 06:15 UTC.
- **Kill switch.** The maintainer can disable all runs with `PDA_KILL_SWITCH=on` (status `Disabled by maintainer: …`, no request, no charge). It is checked at start and again before every request to TED.
- **Changelog:** see `CHANGELOG.md`.

### Security

- **Network:** the actor calls only `https://api.ted.europa.eu` (fixed URLs; user input only reaches the JSON body). Redirects are never followed: a 3xx response fails the run.
- **Personal data:** contact-person, e-mail, phone, fax and winner fields are never requested. In rare cases a buyer name can be a natural person (e.g. a sole-trader contracting entity) as published by TED.
- **Container:** `apify/actor-node:24` pinned by digest; the process runs as the non-root user `myuser`.
- **Source gate:** TED is approved (D1, 2026-10-04). The gate stays in the code: a source marked `pending` is refused on Apify and in local live runs unless the maintainer sets `PDA_ALLOW_PENDING_SOURCES=1`.
- **Input limits:** at most 20 keywords, 100 CPV codes, 40 countries; `expertQuery` at most 2,000 characters; keywords are plain words (letters, digits, spaces and `. , ' & / -`; no `AND/OR/NOT` or search operators: use `expertQuery`). Dates must fall between 1993-01-01 and today + 2 years.
- **Health mode** (`mode: "health"`) is maintainer-only. It needs `PDA_HEALTH_ENABLED=1`, the secret env var `PDA_HEALTH_TOKEN` (≥ 32 characters, an Apify secret referenced as `@pdaHealthToken` in `.actor/actor.json`) and the same value in the secret input `healthToken`, compared in constant time; if `PDA_MAINTAINER_USER_ID` is set, `APIFY_USER_ID` must match it. Otherwise the run answers "Invalid input" with no request. The token is never logged or stored.
- **Links:** `notice_url` and `xml_url` must point at `ted.europa.eu`; anything else is treated as schema drift.
- **Dependencies (`npm audit`, 2026-10-04): 0 vulnerabilities.** `basic-ftp` is forced to 6.2.1 via `overrides` (GHSA-c475-qrg2-pj4r). `http-cache-semantics` was updated in the lockfile to 4.3.0 (fix for GHSA-ch52-4w7c-c8xp, published 2026-10-04) with a plain `npm audit fix`, so the accepted-risk note of 0.1.1 is closed. Never use `npm audit fix --force` (it would downgrade `apify` to 3.1.13).
- **Not yet addressed (tracked in `docs/security/ted-tenders-review-2026-10-04.md`):** the TED per-IP limit is enforced per run only, so concurrent runs on shared Apify egress IPs could exceed it (TED-SEC-04, needs a live check); concurrent incremental runs with the same state key are not locked (TED-SEC-11, documented above).

### Use with AI assistants (MCP)

This actor can be called by AI assistants through Apify's MCP server (a service run by Apify; this actor is a separate, independent product of PPFTEC S.R.L., not affiliated with the EU or TED). Add it to an MCP client such as Claude, VS Code or Cursor:

```
{
  "mcpServers": {
    "clearsource-ted-tenders": {
      "url": "https://mcp.apify.com?tools=clearsource/ted-tenders"
    }
  }
}
```

On first use your browser asks you to sign in to Apify (OAuth); or send your token as `Authorization: Bearer <APIFY_TOKEN>`. The assistant reads the input schema, runs the actor and then fetches the dataset items. Runs are billed to your Apify account at the pay-per-event prices above.

Try: "Find new construction tenders (CPV 45000000) in Germany and Austria from the last 7 days." Details of the MCP server: https://docs.apify.com/platform/integrations/mcp (checked 2026-10-10). Step-by-step guide: https://clearsource.ppftec.com/guides/ted-tenders-by-cpv

### Guide

Step-by-step guide, including the free manual way: https://clearsource.ppftec.com/guides/ted-tenders-by-cpv

### Terms of use

This actor is offered by PPFTEC S.R.L. under Apify's terms plus our terms of use: https://clearsource.ppftec.com/terms

### Disclaimer

Data is provided "as is", as published by TED at `provenance.retrieved_at`. Check the official notice (`notice_url`) before acting. This is not legal or procurement advice.

### Calling via API

```bash
curl -X POST "https://api.apify.com/v2/acts/<account>~ted-tenders/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"countries":["ROU"],"cpvCodes":["72000000"],"maxResults":50}'
```

`<account>` is pending D2. The actor can also be called by AI agents through Apify's MCP server once it is published.

### Development

```bash
npm ci
npm run lint && npm run typecheck && npm test && npm run build
## Live calls (User-Agent: public-data-api/ted-tenders@<version> (+https://ppftec.com)):
PDA_LIVE=1 npm run test:contract   # max 3 requests to TED; writes test/contract/out/live-*.json
PDA_LIVE=1 node scripts/probe-cpv.mjs 72000000   # 1 request: CPV hierarchy check
PDA_HEALTH_TOKEN=<secret> npm run monitor   # health run of dist/main.js (token from your local env)
## Offline local run with recorded fixtures (ignored on Apify):
mkdir -p storage/key_value_stores/default
echo '{"countries":["ROU"],"maxResults":5}' > storage/key_value_stores/default/INPUT.json
PDA_REPLAY_FIXTURES=$PWD/test/fixtures CRAWLEE_STORAGE_DIR=./storage node dist/main.js
```

Environment variables: see `.env.example` (no secrets are needed).

# Changelog

This Actor's version history is a separate document: https://apify.com/clearsource/ted-tenders/changelog.md

# Actor input Schema

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

Full-text terms searched in all languages (TED FT ~). A notice matches if it contains any keyword.

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

8-digit CPV codes, e.g. 72000000. Check digit optional.

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

e.g. ROU, DEU, FRA.

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

Leave empty for all. Contract notices = cn-\*. (List to be checked against the TED code list, unverified.)

## `publishedFrom` (type: `string`):

Earliest publication date to include: an absolute date (YYYY-MM-DD) or a relative one such as "7 days".

## `publishedTo` (type: `string`):

Latest publication date to include. Leave empty for up to today.

## `scope` (type: `string`):

Which notices TED searches: ACTIVE (still open), LATEST (latest version of each notice) or ALL.

## `language` (type: `string`):

Language used for notice titles in the output, when TED provides it (falls back to any available language).

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

TED expert-search query. When set, it replaces the filters above. Cannot be combined with 'Only new notices since last run'.

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

Return only notices not delivered by previous runs with the same state key. Use with a schedule (e.g. daily).

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

Name for the incremental state. Use a different key for each saved search; defaults to one derived from the filters.

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

Maximum number of notices returned (and charged) in this run.

## `dryRun` (type: `boolean`):

Build and validate the TED query without fetching or charging anything.

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

Internal: maintainer health check. Not for normal use; requires a maintainer token. Allowed values: search, health (validated by the actor).

## `healthToken` (type: `string`):

Reserved for the maintainer's monitor.

## Actor input object example

```json
{
  "keywords": [
    "cloud",
    "software"
  ],
  "cpvCodes": [
    "72000000"
  ],
  "countries": [
    "ROU"
  ],
  "publishedFrom": "7 days",
  "scope": "ALL",
  "language": "eng",
  "incremental": false,
  "maxResults": 1000,
  "dryRun": false,
  "mode": "search"
}
```

# Actor output Schema

## `results` (type: `string`):

One record per TED notice with provenance (source URL, retrieval time, licence).

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

Key-value store records such as DRY_RUN or DRIFT_REPORT when present.

# 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": [
        "cloud",
        "software"
    ],
    "cpvCodes": [
        "72000000"
    ],
    "countries": [
        "ROU"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("clearsource/ted-tenders").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": [
        "cloud",
        "software",
    ],
    "cpvCodes": ["72000000"],
    "countries": ["ROU"],
}

# Run the Actor and wait for it to finish
run = client.actor("clearsource/ted-tenders").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 '{
  "keywords": [
    "cloud",
    "software"
  ],
  "cpvCodes": [
    "72000000"
  ],
  "countries": [
    "ROU"
  ]
}' |
apify call clearsource/ted-tenders --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,clearsource/ted-tenders"
        }
    }
}
```

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/fOkKT4MaMrtQMywvK/builds/1jOrxzdlXmLleVGRY/openapi.json
