Public tenders in Europe are published in one place: TED (Tenders Electronic Daily), the EU's official procurement journal. The data is open. The problem is volume and format. Notices arrive in 24 languages, values come in PLN, CZK or SEK, and the TED search UI is fine for browsing but slow if you want a daily shortlist in a spreadsheet.
This guide shows how I pull open tenders from TED into a table with the EU Tenders Scraper (TED) on Apify, first with no code, then with Python and pandas.
For the examples I ran one real search this morning: keyword cybersecurity, open tenders only, published in the last month, capped at 40 results. It returned 18 notices in 4.4 seconds. All numbers below come from that run.
What you get
Each row is one TED notice. The output has around 55 fields. These are the ones I use most:
| Field | What it holds | Example from my run |
|---|---|---|
noticeId |
TED notice number | 755875-2026 |
title |
Title in the original language | Development, Implementation, Maintenance and Operation of financial IT Systems |
buyerName, buyerCountry, buyerCity
|
Contracting authority | European Commission, DG BUDG, BE, Brussels |
cpvMain, cpvLabel
|
Main CPV code and its label |
72000000, IT services |
contractType, procedureType
|
Services / supplies / works, open / restricted etc. |
services, open
|
estimatedValueEur |
Estimated value converted to EUR | 700000000 |
deadline, daysToDeadline
|
Submission deadline and days left | 2026-11-30, 32 |
lotsCount, lots
|
Number of lots with title and value per lot | 2 |
suitableForSme, euFunded
|
Flags set by the buyer | True |
documentsUrl, submissionUrl
|
Tender documents and e-submission links | ec.europa.eu links |
isCorrection, previousDeadline, deadlineChanged
|
Correction tracking | True |
status |
open / closed / awarded | open |
Award notices add winners (with country and company IDs), award value, number of bids and lowest/highest bid. I left awards out of this run to keep it short.
A few things from the 18 notices that are hard to see in the TED UI:
- They came from 9 countries. Belgium had 7, Italy 3, Poland 2, and Finland, Albania, Czechia, Greece, Romania and France one each.
- They were written in 8 languages. Five were in Dutch, so a keyword search in English alone still catches non-English notices here, but the README suggests adding the word in the buyer's language too.
- 11 of 18 had a published value. The median was €459,036. The largest was a €700M European Commission framework for financial IT systems, split into 2 lots.
- Deadlines ranged from 1 to 47 days away, median 22. Three notices had a deadline that moved since the first version, and the actor flags that with
deadlineChanged. - All 18 had both a documents link and an e-submission link.
Main input options
You don't need all 30-odd settings. These cover most searches:
-
keywords: full-text search in title, description and lots.
keywordModeswitches between any and all.excludeKeywordsdrops noise likewindow cleaning. -
noticeStages:
competition(open tenders), awards, or planning notices. Planning gives you a heads-up months before the tender. -
countries and placeOfPerformance: buyer country, or a NUTS code like
PL9for Masovia. -
cpvCodes: CPV prefixes.
72is IT services,45construction,33medical equipment. This is language-independent, so it is more reliable than keywords for broad sectors. - minValue / maxValue: filter by estimated value. Notices without a value are excluded when you set these.
- onlyOpen and deadlineWithinDays: only tenders you can still bid on, or only those closing within N days.
- publishedWithin or publishedFrom / publishedTo: the date window.
- includeBuyerHistory: for each tender, the buyer's past awards in the same sector, likely incumbent, top winners and average number of bids. This is charged separately.
- aiEnrichment with companyProfile: translation into your language, a short summary, key requirements and a 0 to 100 fit score. Also charged separately.
- onlyNew with monitorId: for scheduled runs, return only notices you haven't received before.
- maxResults: the cap. I used 40.
Option 1: no code
- Open the actor page and click Try for free. You need an Apify account; the free plan is enough for a test.
- In Keywords, type your search. I used
cybersecurity. - Under notice stages keep Tenders. Switch on Only open tenders.
- Set Published within to a month and Max results to 40.
- Click Start. My run finished in under 5 seconds.
- Open the Output tab. Export to CSV, Excel or JSON, or send it straight to Google Sheets.
If the table is too wide, the export dialog lets you pick columns. I usually keep title, buyer, country, value, deadline and the two links.
Option 2: Python with apify-client and pandas
Install the two packages:
pip install apify-client pandas
Then run the actor and load the results:
import os
import pandas as pd
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run_input = {
"keywords": ["cybersecurity"],
"noticeStages": ["competition"],
"publishedWithin": "month",
"onlyOpen": True,
"maxResults": 40,
}
run = client.actor("flarewow/eu-tenders-ted-scraper").call(run_input=run_input)
items = client.dataset(run["defaultDatasetId"]).list_items(clean=True).items
df = pd.DataFrame(items)
cols = ["noticeId", "buyerName", "buyerCountry", "cpvLabel",
"estimatedValueEur", "daysToDeadline", "url"]
shortlist = (
df[cols]
.sort_values("daysToDeadline")
.reset_index(drop=True)
)
print(shortlist.head(10).to_string())
print(df["buyerCountry"].value_counts())
print("Median value (EUR):", df["estimatedValueEur"].median())
shortlist.to_csv("ted_cyber_open.csv", index=False)
Keep the token in an environment variable, not in the script. On my 18 rows value_counts() printed BE 7, IT 3, PL 2 and six countries with 1, and the median came out at 459036.0. pandas skips the 7 empty values when it computes the median.
One small thing: estimatedValueEur is already converted with ECB reference rates, so you can sort Polish and Czech notices next to euro ones without doing the maths yourself. Use currency and estimatedValue if you need the original figure.
If you bid on single lots, set "splitLots": True. Each lot becomes its own row with its own value. Each row then counts as one notice for billing.
Scheduling and integrations
For a daily alert, set onlyNew to true and give the search a monitorId such as cyber-eu. Then create a schedule in Apify Console (Schedules, daily at 7:00, for example). Each run returns only notices that weren't delivered before, and you aren't charged twice for the same one.
From there you can push results to Google Sheets, Slack, Make, Zapier, n8n or any webhook. I send the CSV to a sheet and let a filter view do the rest.
Pricing
The actor uses pay-per-event pricing. At the time of writing:
| Event | Free plan | Business |
|---|---|---|
| Notice | $6.00 per 1,000 | $4.50 per 1,000 |
| Buyer intelligence | $0.020 per tender | $0.015 per tender |
| AI analysis | $0.030 per notice | $0.020 per notice |
My 18 notices came to $0.108 at the free-plan rate. Tender to award linking and EUR conversion are free. Check the actor page for current prices before a big job.
Responsible use
TED is official EU open data, published for re-use, and the actor reads it through the official TED API. That said, notices can contain the name, email or phone of a contact person at the buyer. Treat those as personal data under GDPR. Use them to respond to the tender, don't build marketing lists from them, and leave them out of anything you publish. In the screenshot above I left the contact fields out.
This tutorial was written with AI assistance and checked by the author. All numbers come from a real run of the scraper.


Top comments (0)