# EU Tenders Scraper (TED) — Public Contracts by CPV & Country (`northbell/eu-tenders-ted-search`) Actor

EU public tenders from TED (Tenders Electronic Daily): buyer, title, CPV codes, estimated value, deadline, country and procedure. Filter by country, CPV code, keywords or a TED query, and flag the notices that are new since your last run. Official TED API, no login.

- **URL**: https://apify.com/northbell/eu-tenders-ted-search.md
- **Developed by:** [Northbell](https://apify.com/northbell) (community)
- **Categories:** Business, Lead generation
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.60 / 1,000 notices

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/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 Tenders Scraper (TED) — Public Contracts by CPV & Country

Search the EU's **Tenders Electronic Daily (TED)** by buyer country, CPV code, keyword or your own TED query and get one clean row per notice: **buyer, title, CPV codes, estimated value and currency, submission deadline, country, place of performance and procedure type**, with a link to the notice on TED. **$2.00 per 1,000 notices**, no login, no API key.

Run it again, or put it on a daily schedule, and every row also tells you **whether the notice is new since the last run** (`isNew`). Set *What to return* to *Only new* and you pay only for the notices you have not seen before. TED publishes thousands of notices a day, so the useful question is what appeared since yesterday; this keeps the list of what you have already seen.

### What you get

| Field | What it is |
|---|---|
| `publicationNumber`, `url` | the TED notice number (for example `689141-2026`) and the link to its page on TED |
| `noticeKind`, `noticeType` | what kind of notice it is: `open-call` (a contract notice you can still bid on), `award` (the result), `planning`, `contract-modification`, `consultation`…, and TED's own notice type code (`cn-standard`, `can-standard`, `pin-only`…) |
| `publishedOn` | the date TED published it |
| `deadline`, `deadlineType` | the submission deadline with time and time zone as the notice states it (the date only when the notice gives no time). `tenders` = deadline for receipt of tenders, `requests-to-participate` = deadline to ask to take part (restricted and negotiated procedures). The earliest one if the lots differ (a lot date earlier than the notice's publication date is a typing slip by the buyer and is skipped when another lot has a valid one) |
| `isNew`, `previousRunAt` | `true` / `false` whether you had not seen this notice in your previous run of the same search, and when that run was (`null` on the first run) |
| `buyerName`, `buyerNames`, `buyerCity` | the contracting authority (a public body); `buyerNames` lists every buyer when several buy together |
| `buyerCountry`, `buyerCountryIso3` | the buyer's country as ISO 3166 two- and three-letter codes (`DE` / `DEU`) |
| `title`, `titleLanguage` | the title as the buyer wrote it, and its language as a three-letter code (`deu`, `fra`, `eng`…) |
| `contractNature` | `services`, `supplies` or `works` |
| `cpvCodes`, `mainCpv`, `mainCpvDescription` | all CPV codes of the notice (each once), the main one, and the main one's name in English |
| `estimatedValue`, `currency`, `estimatedValueSource` | the estimated value the buyer states for the whole procurement, its currency, and where it comes from: `notice` (stated for the whole procedure) or `lots` (sum of the lots' values) |
| `awardedValue`, `awardedCurrency` | on award results: the total value awarded |
| `placeOfPerformance`, `nutsCodes` | where the work is done (`Hannover (DE)`) and the NUTS region codes |
| `procedureType`, `procedureCode` | for example `Open procedure` / `open`, `Restricted procedure` / `restricted`, `Negotiated procedure with a call for competition` / `neg-w-call` |
| `observedAt` | when the row was collected |

### Run it again and again

- **On a schedule**: save your input as a task and add a **Schedule**: the same search every morning. Send each run to Google Sheets, Slack or email with an integration on the task.
- **Watch for changes**: Set **What to return** to *Only notices that are new since the last run*.
- **In bulk**: Combine **Buyer countries**, **CPV codes** and **Keywords** in one run.

### Use it for

- **Bid monitoring**: a daily schedule with your countries and CPV codes, *Open calls* and *Only new*, sent to Sheets, Slack or your CRM
- **Lead lists for suppliers**: the public bodies that buy what you sell: buyer, city, country, CPV, value and the notice link
- **Market research**: *Award results* by CPV code and country show who buys, how often and for how much
- **Pipeline planning**: *Planning notices* announce purchases before the call is published

### Input

```json
{
  "countries": ["DE", "FR"],
  "cpvCodes": ["72000000"],
  "keywords": ["cloud"],
  "noticeKind": "open-calls",
  "publishedWithinDays": 7,
  "maxNotices": 200,
  "output": "new"
}
```

- All filters are combined with AND; the entries inside one list are combined with OR.
- **countries**: the buyer's country, ISO codes with two or three letters. This is a filter on the buyer, not a ranking hint.
- **cpvCodes**: a code covers its whole branch: `72000000` (or `72`) matches every `72…` code.
- **keywords**: each line is searched as a phrase in the notice title (which includes the name of the main CPV code) and in the description of the procedure.
- **query**: optional, in [TED's expert-search syntax](https://ted.europa.eu/en/search/expert-search), for anything the fields above cannot say, for example `contract-nature-main-proc=works AND estimated-value-proc>5000000`.
- Results are always sorted newest first, and **maxNotices** (default 200, up to 5,000) cuts the list from the old end. With *Only new* it is the number of new notices to return: the Actor reads down the list, past the notices you have already seen (up to 5,000 notices deep), until it has found that many. Notices below the cut-off are not marked as seen, so they come back as new in your next run.

"Is it new" is remembered per search — the same countries, CPV codes, keywords, notice kind and query — in a named key-value store in your Apify account, so the second run of the same search already shows `isNew`. Changing *Published within* or *Maximum notices* keeps the same history.

### Pricing

**$2.00 per 1,000 notices**, plus $0.01 per run. Error rows (a search TED rejects, TED not answering) are free. With *Only new*, notices you have already seen are skipped and not charged. The $0.01 run-start fee is the one charge every run pays, even one that returns nothing.

### Notes

- Source: the public search API of TED, the EU Publications Office's procurement journal. Anonymous access, nothing from behind a login. TED's own legal notice applies to reuse of the data.
- Buyers are public bodies. **No contact persons, email addresses or phone numbers are returned**; the Actor never asks TED for them.
- Titles stay in the buyer's language (`titleLanguage`); in a read of the 5,000 newest notices on 9 Oct 2026 only 204 had an English title. `mainCpvDescription` is in English, which tells you what the notice is about.
- `deadline` is filled on open calls (2,488 of 2,501 in that read; the rest are mostly qualification-system and prior-information notices that name no deadline). Award and modification notices have none, and planning notices almost never (1 of the 2,499 other notices in that read). A few buyers type a deadline that lies before the notice's own publication date on every lot (12 of 2,272 tender deadlines in that read); the Actor passes on what the buyer entered.
- `estimatedValue` is what the buyer states, and about half of the open calls state none (1,324 of 2,501 in that read), so it is empty there. Some buyers enter a placeholder: a value of 1 or less in the buyer's currency made up 87 of the 2,242 values in that read, so check small values before you add them up. `estimatedValueSource` `lots` is the sum of the lots that state a value and can understate the total. `awardedValue` is filled on award results only (1,811 of 2,011 in that read), and some of those are 0 too.
- `isNew` and `previousRunAt` are `null` on the first run of a search, because there is nothing to compare with yet.
- `procedureType` is empty on contract-modification and consultation notices (they have no procedure type in TED), on most planning notices, and on a few award and open-call notices (24 of 4,512 in that read). `placeOfPerformance` and `nutsCodes` are empty when the notice names no place.
- A notice a buyer corrects appears once, in its latest version.
- TED publishes thousands of notices a day (3,705 latest-version notices on 8 Oct 2026), so a wide search with the default 200 gives you the newest 200. Narrow it with country, CPV code, keywords or notice kind rather than raising the limit. Reading 5,000 notices took 37 seconds in a live run on 9 Oct 2026.
- A notice that carries no buyer, no title and no CPV code at all (1 of 5,000 in that read) is skipped and not charged.
- If TED stops a search early because it took too long, the Actor keeps the notices it received and adds a free error row saying the list may be incomplete; narrow the search and run it again.
- If TED is rate limiting, the Actor waits and retries twice. If it still gets no answer it adds a free error row, and the run is marked failed when nothing at all could be read.

If the winner of an award is a UK company, see [UK Companies House Scraper](https://apify.com/northbell/companies-house-search) for its register entry. For country-level economic data, see [World Bank Data Scraper](https://apify.com/northbell/world-bank-data-scraper).

### For AI agents

`{"countries": ["DE"], "noticeKind": "open-calls", "publishedWithinDays": 3, "maxNotices": 5}` returns up to 5 rows, newest first, each with buyer, title, CPV codes, deadline and value. Rows have `type`: `notice` or `error`. Fields that do not apply to a notice are `null` (or an empty list), never missing. Schedule the same input daily with `"output": "new"` to get only what was published since the previous run.

# Actor input Schema

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

Only notices whose buyer is in one of these countries. One ISO code per line: DE, FR, ES, PL, IT, NL … (two or three letters; EL for Greece and UK for the United Kingdom also work). Leave empty for every country.

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

Only notices for these CPV codes (the EU's Common Procurement Vocabulary), one per line, e.g. 72000000 for IT services or 45000000 for construction work. A code covers its whole branch: 72000000 (or just 72) matches every 72… code. Leave empty for all.

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

Words or phrases to find in the notice title (which includes the name of its main CPV code) or in the description of the procedure. One per line; a notice that matches any line is returned. Keep them short, e.g. "cloud hosting" or "road maintenance". Leave empty to skip.

## `noticeKind` (type: `string`):

Open calls are the notices you can still bid on. "All notices" also returns award results, planning notices and contract modifications.

## `publishedWithinDays` (type: `integer`):

Only notices published in the last N days. Run it on a daily schedule with 7 and the setting below on "Only new" for a cheap feed of fresh tenders.

## `maxNotices` (type: `integer`):

How many notices to return, newest first. You pay per notice returned. With "Only new" the Actor reads past the notices you have already seen until it has found this many new ones. Narrow the search with the filters above rather than raising this.

## `output` (type: `string`):

With "Only new", notices you already got in an earlier run of the same search are skipped and not charged. The first run returns everything, because there is nothing to compare with yet.

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

Advanced: a query in TED's expert-search syntax, combined with all the filters above using AND, e.g. contract-nature-main-proc=works AND estimated-value-proc>5000000. Syntax and field names: https://ted.europa.eu/en/search/expert-search. Results are always sorted newest first.

## Actor input object example

```json
{
  "countries": [
    "DE",
    "FR"
  ],
  "cpvCodes": [
    "72000000"
  ],
  "noticeKind": "open-calls",
  "publishedWithinDays": 7,
  "maxNotices": 200,
  "output": "all"
}
```

# Actor output Schema

## `notices` (type: `string`):

Buyer, title, CPV codes, value, deadline, country and procedure for each notice.

# 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 = {
    "countries": [
        "DE",
        "FR"
    ],
    "cpvCodes": [
        "72000000"
    ],
    "noticeKind": "open-calls"
};

// Run the Actor and wait for it to finish
const run = await client.actor("northbell/eu-tenders-ted-search").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 = {
    "countries": [
        "DE",
        "FR",
    ],
    "cpvCodes": ["72000000"],
    "noticeKind": "open-calls",
}

# Run the Actor and wait for it to finish
run = client.actor("northbell/eu-tenders-ted-search").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 '{
  "countries": [
    "DE",
    "FR"
  ],
  "cpvCodes": [
    "72000000"
  ],
  "noticeKind": "open-calls"
}' |
apify call northbell/eu-tenders-ted-search --silent --output-dataset

```

## MCP server setup

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

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/C6GdCMsCDKyeWCpp7/builds/gtLdsvb9bK4p3hE2d/openapi.json
