DEV Community

Cover image for How to pull open EU public tenders from TED into Python and pandas
Norbert Sobala
Norbert Sobala

Posted on

How to pull open EU public tenders from TED into Python and pandas

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.

TED cybersecurity tenders in a table

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. keywordMode switches between any and all. excludeKeywords drops noise like window 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 PL9 for Masovia.
  • cpvCodes: CPV prefixes. 72 is IT services, 45 construction, 33 medical 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.

Input schema of the EU Tenders Scraper

Option 1: no code

  1. Open the actor page and click Try for free. You need an Apify account; the free plan is enough for a test.
  2. In Keywords, type your search. I used cybersecurity.
  3. Under notice stages keep Tenders. Switch on Only open tenders.
  4. Set Published within to a month and Max results to 40.
  5. Click Start. My run finished in under 5 seconds.
  6. 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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)