# EU TED Tenders Scraper — Public Procurement & RFP Data (`foxlabs/ted-tenders`) Actor

Fetch EU public procurement notices from the official TED (Tenders Electronic Daily) API: tenders, contract awards (CAN), RFPs & bid data. Filter by country, CPV sector, date, value & notice type. Clean JSON for B2G lead gen, tender alerts, CRM, dashboards & LLM pipelines.

- **URL**: https://apify.com/foxlabs/ted-tenders.md
- **Developed by:** [Berkan Kaplan](https://apify.com/foxlabs) (community)
- **Categories:** Business, Lead generation
- **Features:** Limited permissions
- **Stats:** 68 total users, 23 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 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 Public Tenders — TED 📄

**foXLabs procurement series:** [World Bank](https://apify.com/foxlabs/worldbank-tenders) · [IADB](https://apify.com/foxlabs/iadb-tenders) · [Ukraine Prozorro](https://apify.com/foxlabs/ukraine-prozorro-tenders) · [UK Contracts Finder](https://apify.com/foxlabs/uk-contracts-finder-supplier-data) · [US federal awards](https://apify.com/foxlabs/usaspending-contract-award-data) · [India](https://apify.com/foxlabs/india-government-tenders)

🎉 Turn TED into clean, structured EU public-tender data — no login, no API key, one row per notice, with the buyer, title, value, CPV code, deadline and country.
Built for suppliers, bid/proposal teams and public-procurement market intelligence.

### 🔍 What is the EU Public Tenders — and when should you use it?

Give this actor **keywords**, **buyer countries**, **CPV sectors**, notice types and a **date range**, and it returns the matching notices from Tenders Electronic Daily (TED), the EU public-procurement journal (official open data) — as clean rows you can filter, export or feed to an AI agent. Every run queries the official TED search API live, so the data is as fresh as the journal itself.

**Use it when you need:** EU tender opportunities by sector or keyword; the notices that mention a public buyer; or a daily pipeline of new notices.

**Use something else when:** you need company firmographics — TED is procurement notices, not a company registry.

### 🤖 Use with AI agents

**Already on the Apify MCP server?** Ask for this Actor by name: `foxlabs/ted-tenders`.

**Your agent can pay for its own runs.** This Actor is pay-per-event with agentic payments, so an agent can discover it, run it and settle the bill over **x402 (USDC on Base)** or **Skyfire** — no Apify account or API token of its own. Billing is the same either way: per notice delivered.

Otherwise paste this into Claude, ChatGPT, Cursor or any MCP-enabled assistant:

```
I want to pull EU public tender notices using the Apify Actor `foxlabs/ted-tenders`.
Input: `keywords` (words searched in the notice text, comma = alternatives), `countries` (ISO3 buyer countries, e.g. DEU),
`cpvCodes` (CPV divisions), `datePreset` (last_24_hours, last_7_days, last_30_days, last_90_days, ytd) and `maxResults`.
Start with: {"keywords":"software","countries":["DEU"],"datePreset":"last_30_days","maxResults":50}
Ask me what to look for, run the Actor, then summarise the notices as a table.
```

The machine-readable API, MCP config and OpenAPI definition live at `apify.com/foxlabs/ted-tenders.md`.

### 📋 Overview

Everything you need to turn Tenders Electronic Daily (TED), the EU public-procurement journal (official open data) into clean, structured data — in one actor, with no login, cookies or API key.

**Why teams pick this actor:**

- ✅ **Keyword search on the official feed** — words are matched in the full notice text, combined with country, sector, notice-type and date filters.
- 🧹 **No empty-promise columns** — only fields TED actually fills.
- 🔗 **Stable identifiers** — every row carries TED's publication number and links, ready to join across runs.
- 💰 **Pay only for results** — per notice delivered; a search that finds nothing costs only the run start.
- 🤖 **Agent-ready** — MCP + x402 agentic payments.

### ✨ Features

- 🔎 **Keywords** — full-text search across the notice (`software`, `cloud services`); commas separate alternatives.
- 🌍 **Filters** — buyer country, CPV sector, notice type, date presets or a custom range (`20260501`, `2026-05-01` or `01.05.2026`).
- 🧠 **Expert mode** — paste a native TED expert query when you need more (`total-value>=1000000 AND classification-cpv=72000000`).
- 🌐 **Language** — titles, descriptions and buyer names in the language you pick, falling back to English.
- 🧹 **Clean schema** — camelCase rows, ready for CSV/Excel/JSON.

### 🎬 Quick Start

```bash
curl -X POST "https://api.apify.com/v2/acts/foxlabs~ted-tenders/runs?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"keywords":"software","countries":["DEU"],"datePreset":"last_30_days","maxResults":50}'
```

### 🚀 Getting Started (3 steps)

1. **Choose your filters** — keywords, buyer countries, CPV sectors and a date range.
2. **Set the cap** — `maxResults` limits the rows (0 = no limit).
3. **Run and export** — get a clean dataset as JSON, CSV or Excel.

### 📥 Input

```json
{"keywords":"software","countries":["DEU"],"datePreset":"last_30_days","maxResults":50}
```

| Field | Type | Description |
|---|---|---|
| `datePreset` | string | `last_24_hours`, `last_7_days` (default), `last_30_days`, `last_90_days`, `ytd` or `custom`. |
| `publicationDateFrom` / `publicationDateTo` | string | Custom range, used when `datePreset` is `custom`: `20260501`, `2026-05-01` or `01.05.2026` (day first). |
| `keywords` | string | Words to find in the notice text; commas separate alternatives, each entry is matched as an exact phrase. |
| `countries` | array | Buyer countries as ISO3 codes (`DEU`, `FRA`, …). Empty = all. |
| `cpvCodes` | array | CPV main divisions (exact match). Empty = all sectors. |
| `noticeTypes` | array | Notice types to keep. Empty = all. |
| `language` | string | Language for titles, descriptions and buyer names (default `ENG`). |
| `maxResults` | integer | Most rows in one run (default 500, the form suggests 100; 0 = no limit). |
| `query` | string | Power users: a native TED expert query; it replaces the filters above. Plain words here are searched like `keywords`. Two-letter countries (`DE`), CPV codes with a check digit (`72000000-5`) and the old TED syntax for country, CPV, date and full text (`CY=[DE] AND PC=[72000000]`) are rewritten automatically. Results come newest first unless the query has its own `SORT BY`. |
| `pageSize` | integer | Notices per API call (max 250). |

### 📤 Output

One row per notice, saved to the dataset. Every row carries `scrapedAt` and links to the notice on TED.

```json
{
  "publicationNumber": "650347-2026",
  "publicationDate": "2026-09-22",
  "title": "Germany – Health services – Videosprechstunde",
  "noticeType": "cn-standard",
  "procedureType": "open",
  "buyerName": "AOK Bayern - Die Gesundheitskasse",
  "buyerCountry": "DEU",
  "buyerCity": "München",
  "buyerEmail": "vergabestelle1@by.aok.de",
  "totalValue": 2600500,
  "totalValueCurrency": "EUR",
  "cpvCodes": ["85100000"],
  "deadlineDate": "2026-10-05",
  "detailUrl": "https://ted.europa.eu/en/notice/-/detail/650347-2026",
  "scrapedAt": "2026-09-29T19:35:39.878Z"
}
```

| Field | Description |
|---|---|
| `publicationNumber` | TED publication number |
| `publicationDate` | Publication date |
| `title` | Notice title |
| `noticeType` | Notice type |
| `noticeSubtype` | Notice subtype |
| `procedureType` | Procedure type |
| `buyerName` | Contracting buyer |
| `buyerCountry` | Buyer country (ISO3) |
| `buyerCity` | Buyer city |
| `buyerEmail` | Buyer email, when published |
| `buyerPhone` | Buyer phone, when published |
| `buyerUrl` | Buyer website |
| `totalValue` | Estimated or awarded value |
| `totalValueCurrency` | Currency of the value |
| `cpvCodes` | CPV codes |
| `deadlineDate` | Tender deadline |
| `description` | Notice description |
| `placeOfPerformanceCountry` | Place of performance country |
| `placeOfPerformanceCity` | Place of performance city |
| `detailUrl` | Notice page on TED |
| `xmlUrl` | Notice XML on TED |

### 💼 Use cases

**1. Bid pipeline** — find open EU tenders in your field.
*Input:* keywords and/or CPV sectors, `last_24_hours`. *Output:* new notices with value and deadline. *Use:* a daily bid feed.

**2. Buyer intelligence** — follow a public buyer.
*Input:* the buyer's name as a keyword (matched in the full notice text, so notices that mention the buyer appear too). *Output:* those notices. *Use:* anticipate demand.

**3. Market sizing** — size public spend in a category.
*Input:* CPV sectors and a longer date range. *Output:* notices with values. *Use:* estimate market size.

### 🔗 Integration

**JavaScript / Node.js**

```javascript
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: 'YOUR_TOKEN' });
const run = await client.actor('foxlabs/ted-tenders').call({"keywords":"software","countries":["DEU"],"datePreset":"last_30_days","maxResults":50});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items[0]);
```

**Python**

```python
from apify_client import ApifyClient
client = ApifyClient('YOUR_TOKEN')
run = client.actor('foxlabs/ted-tenders').call(run_input={"keywords":"software","countries":["DEU"],"datePreset":"last_30_days","maxResults":50})
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)
```

Written for `apify-client` 3 for Python, where `call()` returns a run object; with version 1 or 2 it returns a dict — write `run['defaultDatasetId']` there.

**Automation (n8n / Zapier / Make):** schedule a daily run with `datePreset: last_24_hours` → handle the JSON dataset → push new notices to a sheet, CRM or chat.

### 📊 Pricing

Pay-per-event: **per notice delivered** to the dataset, plus Apify's small run-start event. A search that matches nothing costs only the run start. [View current pricing.](https://apify.com/foxlabs/ted-tenders)

### ❓ FAQ

**Do I need an account, login or API key?** No. This reads Tenders Electronic Daily (TED), the EU public-procurement journal (official open data).

**What do I search by?** Keywords, buyer countries, CPV sectors, notice types and dates — or a native TED expert query.

**How current is the data?** Every run queries TED live, so results are as fresh as the journal.

**What does each row represent?** One TED notice: the contracting buyer, title, value, CPV codes, deadline and country.

**Can I export to CSV / Excel / JSON?** Yes — directly from the Apify dataset.

### 🐛 Troubleshooting

- **Fewer rows than expected** — raise `maxResults`, widen the date range or remove a filter.
- **No rows for a keyword** — each comma-separated entry is an exact phrase; try a shorter word or the term in the notice's language.
- **The run ended with "No notices delivered — TED rejected the query"** (no rows, status SUCCEEDED, the details in the key-value store record `QUERY_ERROR`) — the advanced field needs TED expert syntax (`field operator value`); put plain words in `keywords` instead. Country, CPV, date and full-text fields in the old syntax (`CY=[DE]`, `PC=[…]`, `PD=[…]`, `FT=[…]`) are translated for you; other old fields such as `TD=[3]` end the run the same way, with a message naming them — use the notice-type filter instead. A run ends FAILED only when something went wrong on our side (network, TED unavailable after the retries).

### ⚖️ Is it legal to scrape this data?

This actor reads public procurement notices published on TED as open data. Results can still contain personal data (e.g. a person’s name); personal data is protected by the GDPR and similar laws, so only process it with a legitimate basis. See Apify’s blog post on the legality of web scraping.

### 🤝 Support & contact

- 🌐 **Website:** [data.foxlabs.com.tr](https://data.foxlabs.com.tr)
- 📧 **Email:** info@foxlabs.com.tr
- 🐛 **Issues:** open a ticket in the Actor’s **Issues** tab
- 🧰 **More clean B2B data actors:** [Fox Labs on Apify](https://apify.com/foxlabs)

### Changelog

#### 0.6.14 — 2026-10-10 — a query TED cannot run no longer fails the run

- **Behaviour change — run status.** A query TED rejects as typed (HTTP 400 syntax error, an old-syntax field such as `TD=[3]` that cannot be translated, a date that is not a date) used to end the run as FAILED. It now ends SUCCEEDED with no rows: the status message says what TED rejected and how to fix it, and the key-value store record `QUERY_ERROR` holds the message and the query. Such a run is the user's to fix, not a broken Actor — in the 30 days to 2026-10-10, 13 of the 117 runs by users ended FAILED, and every failure we could inspect (our own test runs) was a rejected query. A failure on our side — network, TED 429 or 5xx after the retries — still ends FAILED.
- Nothing is billed for such a run beyond the Actor start event; no output field or price changed.

#### 0.6.13 — 2026-10-08 — README: the Python example works with the current Apify client

- The README's Python example read the results with `run['defaultDatasetId']`. With the current Apify client for Python (version 3) `call()` returns a run object, not a dict, and that line raised `TypeError: 'Run' object is not subscriptable` — after the run itself had finished. The example now reads `run.default_dataset_id`; with client version 1 or 2, keep `run['defaultDatasetId']`. Checked with apify-client 3.2.1, 2.5.1 and 1.12.2.
- No code, output field or pricing change.

#### 0.6.12 — 2026-10-07 — a run that Apify starts again does not write a notice twice

- **A run that the platform starts again no longer repeats what it has written.** Apify can move a run to another server while it is going; the Actor then starts again from the first page with the same dataset. Until now it wrote every page again: in a test with `maxResults` 1500 that was started again after 400 rows, the dataset ended with 1,900 rows, 171 notices twice. The Actor now first reads which notices its own dataset already holds; they count towards `maxResults` and are not written or charged again.
- No output field changed, no pricing change.

#### 0.6.11 — 2026-10-02 — `SORT BY … ASC` no longer fails the run

- TED rejects the `ASC` keyword (`SORT BY publication-date ASC` → HTTP 400 "extraneous input 'ASC'"); it sorts ascending without a keyword. `ASC` / `ASCENDING` are now dropped and `DESCENDING` becomes `DESC` before the query is sent.

#### 0.6.10 — 2026-10-02 — every run returns the newest notices first

- **Result order changed: newest notices first.** TED returns notices oldest first, so a form run used to start at the oldest day of the chosen window — `maxResults: 20` on "last 7 days" gave the week's 20 oldest notices. Every run now asks TED for `SORT BY publication-date DESC`, as the TED website lists them, so a capped run keeps the most recent notices. An expert query with its own `SORT BY` keeps its order. Output fields and price are unchanged.

#### 0.6.9 — 2026-10-02 — expert queries without a date return the newest notices first

- **An expert query with no publication date returned the oldest notices first** — TED's archive starts in October 2016, and those old-format notices carry no buyer e-mail, phone or deadline (measured: `CY=[DE] AND PC=[72000000]` returned 20 notices from 2016-10-01…04). Such a query now asks TED for the newest notices first, as the TED website lists them. Queries that set a publication date or their own `SORT BY`, and the form filters, keep their order.

#### 0.6.8 — 2026-10-02 — common expert-query mistakes no longer fail the run

- **Three mistakes in the advanced field used to fail the run with HTTP 400** (measured on the TED API): a two-letter country (`buyer-country=DE`), a CPV code written with its check digit, as CPV codes are officially printed (`classification-cpv=72000000-5`), and the old TED expert syntax (`CY=[DE] AND PC=[72000000]`). They are now rewritten before the query is sent — `DE` → `DEU` (also `EL` → `GRC`, `UK` → `GBR`), `72000000-5` → `72000000`, and old `CY`, `PC`, `PD` (dates and `<>` ranges) and `FT` fields into the current syntax. The log lists every rewrite.
- An old-syntax field that cannot be translated (for example `TD=[3]`) now stops the run with a message naming that field, instead of TED's generic syntax error.
- No change to output fields, filters or price.

#### 0.6.7 — 2026-09-29 — keyword search; plain words no longer fail the run

- **New `keywords` field.** Words are searched in the full notice text (TED full-text search); commas separate alternatives.
- **Plain words in the advanced field used to fail the run.** TED answers anything that is not expert syntax with an error, so a query like `software` ended as a failed run. Text with no field operator is now searched as keywords, together with the filters; a real expert-syntax error now says how to fix it.
- **Custom dates in more formats:** `20260501`, `2026-05-01` and `01.05.2026` are accepted (the form used to take only `20260501`).
- **README corrected against the real input.** Every example used input keys this Actor does not have (`queries`, `maxResultsPerQuery`, with a literal `undefined` value), listed phantom fields (`maxConcurrency`, `includeRaw`) and described company-registry features ("name or registry-ID lookup", "legal form, formation date"). Examples, input and output now match the Actor. No price change.

#### 0.6 — 2026-09-07

- **Enabled AI-agent payments (x402) + rebuilt the README to the full standard** (What-is / when, AI-agents + x402 agentic payments + MCP, Overview, Features, Use cases, Integration, FAQ, Troubleshooting, Support & contact).

#### 0.0

- Initial release: EU procurement notices from Tenders Electronic Daily (TED), the EU public-procurement journal (official open data).

# Changelog

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

# Actor input Schema

## `datePreset` (type: `string`):

Quick date filter. Pick 'Custom' to use the From/To fields below.

## `publicationDateFrom` (type: `string`):

e.g. 20260501, 2026-05-01 or 01.05.2026 (day first). Only used when Date range = Custom.

## `publicationDateTo` (type: `string`):

e.g. 20260531, 2026-05-31 or 31.05.2026 (day first). Only used when Date range = Custom. Leave empty for 'today'.

## `keywords` (type: `string`):

Words to find in the notice text, e.g. software or cloud services. Separate alternatives with commas (software, cybersecurity = either); each entry is matched as an exact phrase. Leave empty to get every notice matching the other filters.

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

Filter by country of the contracting authority. Leave empty for all EU.

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

Filter by sector using CPV (Common Procurement Vocabulary) main divisions. Multi-select OK. Leave empty for all sectors. Note: exact CPV match — selecting '72000000 — IT services' returns notices tagged with that exact code (sub-codes like 72100000 not included unless you also select them via Advanced query).

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

Filter by notice type. Leave empty for all types.

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

Language for localized fields (title, description, buyer name). Falls back to English if a notice has no translation.

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

Hard cap on dataset rows. Helps control cost. Set to 0 for unlimited.

## `query` (type: `string`):

POWER USERS ONLY. Native TED DSL. Examples: 'publication-date>=20260101 AND buyer-country=DEU', 'total-value>=1000000 AND classification-cpv=72000000'. An expert query OVERRIDES all filters above. Plain words with no field operator are searched like Keywords, together with the filters. Two-letter countries (DE), CPV check digits (72000000-5) and the old TED syntax for country, CPV, date and full text (CY=\[DE], PC=\[…], PD=\[…], FT=\[…]) are rewritten automatically. Results come newest first unless the query has its own SORT BY. Leave empty for normal use.

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

Notices fetched per API call. TED max is 250. Default 100 is usually fine.

## Actor input object example

```json
{
  "datePreset": "last_7_days",
  "countries": [
    "DEU"
  ],
  "cpvCodes": [],
  "noticeTypes": [],
  "language": "ENG",
  "maxResults": 100,
  "query": "",
  "pageSize": 100
}
```

# Actor output Schema

## `dataset` (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 = {
    "datePreset": "last_7_days",
    "countries": [
        "DEU"
    ],
    "language": "ENG",
    "maxResults": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("foxlabs/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 = {
    "datePreset": "last_7_days",
    "countries": ["DEU"],
    "language": "ENG",
    "maxResults": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("foxlabs/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 '{
  "datePreset": "last_7_days",
  "countries": [
    "DEU"
  ],
  "language": "ENG",
  "maxResults": 100
}' |
apify call foxlabs/ted-tenders --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,foxlabs/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/WrjbjIv6VqibkwD3r/builds/5ndYsyuqEmxsUQlgy/openapi.json
