EU public tenders as JSON or CSV: the TED Search API, a Python example, and a ready-made dataset
How to pull contract notices and award notices from TED (Tenders Electronic Daily) yourself, which limits and field quirks to expect, and when a normalized feed is worth paying for.
The short version
- TED publishes EU public procurement notices. The official Search API v3 is free and needs no API key: one
POSTendpoint, a query language, JSON in and out. - Doing it yourself is easy for a one-off export. The work is in the details: sorting, pagination beyond 15,000 results, multilingual objects, repeated values and mixed types. This page lists the ones we hit.
- If you want one flat schema, CPV labels, awards and a schedule without maintaining a script, we run an Apify Actor for that. Real samples of its output are at the bottom, free to download.
1. The official TED Search API
Documentation: docs.ted.europa.eu/api/latest/search.html. The docs describe the API as aimed at data reusers and not requiring authentication; that matches what we saw (anonymous requests return 200). There is also a Swagger UI at api.ted.europa.eu/swagger-ui and an OpenAPI file at api.ted.europa.eu/api-v3.yaml.
Endpoint: POST https://api.ted.europa.eu/v3/notices/search. The body is JSON with a query (the "expert query" language), a list of fields to return, and paging parameters.
curl -s -X POST https://api.ted.europa.eu/v3/notices/search \
-H "Content-Type: application/json" \
-d '{
"query": "buyer-country=ITA AND classification-cpv=72* AND notice-type=cn-standard AND publication-date>=today(-14) SORT BY publication-date DESC",
"fields": ["publication-number", "notice-type", "buyer-name", "buyer-country",
"classification-cpv", "publication-date", "estimated-value-proc", "estimated-value-cur-proc"],
"limit": 2,
"paginationMode": "ITERATION"
}'
Real response from that request, trimmed to one notice (the 39 in totalNoticeCount is what matched on 4 November 2026):
{
"notices": [
{
"publication-number": "766353-2026",
"notice-type": "cn-standard",
"buyer-name": { "ita": ["Società per Azioni Esercizi Aeroportuali S.E.A"] },
"buyer-country": ["ITA"],
"classification-cpv": ["72262000", "72262000"],
"publication-date": "2026-11-04+01:00",
"estimated-value-proc": "3156000.00",
"estimated-value-cur-proc": "EUR",
"links": { "...": "per-language HTML / PDF / XML URLs, returned even though not requested" }
}
],
"totalNoticeCount": 39,
"iterationNextToken": "AAABoaiWMVYAGJkw...",
"timedOut": false
}
Query syntax that worked
| Goal | Query |
|---|---|
| Country of the buyer (three-letter ISO code) | buyer-country=ITA or buyer-country IN (ITA FRA) |
| CPV prefix | classification-cpv=72* or classification-cpv IN (72* 45*) |
| Notice type | notice-type=cn-standard, notice-type IN (can-standard can-social) |
| Publication date | publication-date>=20261020 or publication-date>=today(-7) |
| Full text | FT~("cybersecurity") |
| Buyer name word | buyer-name~"comune" |
| Value | total-value>=1000000 |
| Sort, newest first | append SORT BY publication-date DESC to the query |
Combine with AND / OR. A syntax error or unknown field returns HTTP 400 with a type such as QUERY_UNKNOWN_FIELD or QUERY_SYNTAX_ERROR and the column of the problem. Adding "checkQuerySyntax": true to the body validates a query without running it (documented in the OpenAPI file).
Sorting: it goes in the query, not in the body
This one cost us time. The request body has no sort parameters, and unknown body properties are silently ignored. We sent "sortField": "publication-date", "sortOrder": "DESC" and also ASC, with an invalid field name, and with an invalid order: the results were identical every time, oldest first (by publication number). The documented way is the SORT BY field DESC clause at the end of the expert query. With it, the same request returned notices from the current day first. Without it, "give me the last 50 tenders from the last 14 days" returns the oldest 50 of those 14 days.
Limits and pagination
limitis capped at 250. Asking for 300 returns400 SEARCH_EXCEEDS_MAX_LIMIT. The OpenAPI file adds a second cap of 10,000 fields per page (length offieldstimeslimit).- Page mode has a 15,000-result window. The default mode is
PAGE_NUMBERandpage x limitmust stay at or below 15,000, otherwise400 SEARCH_WINDOW_TOO_WIDE(for example page 61 with limit 250). - Use
"paginationMode": "ITERATION"for bigger exports. Pass the returnediterationNextTokenback asiterationNextTokenuntil no more notices come back. We iterated 75 pages (18,750 notices) in one run, past the page-mode window. Per the docs this mode is consistent (a point-in-time snapshot), while page mode can show duplicates or gaps if a new Official Journal issue is released between your requests. totalNoticeCounttells you how many notices the query matches, so you can size an export before running it. The response also has atimedOutflag; check it.- We found no documented rate limit and no rate-limit headers. We did see occasional
502responses from the CDN in front of the API, so retry with backoff.
Field quirks worth knowing
Fields use eForms business-term names (title-proc, estimated-value-lot...). Asking for a field that does not exist returns 400 with the full list of supported field names. The shapes are not uniform:
- Dates carry a timezone suffix:
"2026-10-20+02:00". Cut to 10 characters for a calendar date. - Text is multilingual objects keyed by three-letter language code:
title-procis{"ita": "..."},title-lotis{"ita": ["..."]}(lists),buyer-nameis{"ita": ["..."]}. There is no translation: the text is in the buyer's language. - Same kind of field, different types: in one live query,
estimated-value-procwas a string ("324000.00"),estimated-value-lota list of strings andtotal-valuean integer. - Repeated values:
classification-cpvrepeats a code once per lot. One notice returned 14 entries for 7 distinct codes. De-duplicate while keeping order. - HTML entities in text: 7 of 100 Italian contract notices we queried had titles containing entities such as
’instead of an apostrophe. Runhtml.unescapeif you display titles. - Country codes are three-letter (
ITA) and returned as lists. linkscomes back even if you did not request it, with one URL per language for HTML and PDF plus an XML URL.- Value fields depend on the form: contract notices usually fill
estimated-value-proc; award notices carry result values and winners in separate fields. Older (pre-eForms) notices have fewer structured fields.
2. DIY: notices to CSV in Python
This script (tested against the live API on 4 November 2026, it returned 39 notices) pages through the results, retries on transient errors, flattens the multilingual fields and writes a CSV. It needs pip install requests.
import csv
import html
import time
import requests
URL = "https://api.ted.europa.eu/v3/notices/search"
FIELDS = ["publication-number", "notice-type", "title-proc", "buyer-name", "buyer-country",
"classification-cpv", "publication-date", "estimated-value-proc", "estimated-value-cur-proc",
"deadline-receipt-tender-date-lot"]
QUERY = "buyer-country=ITA AND classification-cpv=72* AND notice-type=cn-standard AND publication-date>=today(-14) SORT BY publication-date DESC"
def first_text(multi, langs=("eng", "ita")):
"""Titles and names come as {"ita": "...", "eng": "..."} (sometimes with a list value)."""
if not isinstance(multi, dict):
return multi
for lang in (*langs, *multi):
if lang in multi:
v = multi[lang]
return html.unescape(v[0] if isinstance(v, list) else v) # some titles contain ’ etc.
def search(query, limit=250):
body = {"query": query, "fields": FIELDS, "limit": limit, "paginationMode": "ITERATION"}
while True:
for attempt in range(5):
r = requests.post(URL, json=body, timeout=60)
if r.status_code in (429, 500, 502, 503, 504):
time.sleep(2 ** attempt)
continue
r.raise_for_status()
break
else:
raise RuntimeError("TED API unavailable")
data = r.json()
yield from data["notices"]
token = data.get("iterationNextToken")
if not token or not data["notices"]:
return
body["iterationNextToken"] = token
rows = []
for n in search(QUERY):
rows.append({
"id": n["publication-number"],
"type": n.get("notice-type"),
"published": (n.get("publication-date") or "")[:10], # "2026-10-20+02:00" -> "2026-10-20"
"title": first_text(n.get("title-proc")),
"buyer": first_text(n.get("buyer-name")),
"cpv": ";".join(dict.fromkeys(n.get("classification-cpv") or [])), # de-duplicate, order kept
"value": n.get("estimated-value-proc"),
"currency": n.get("estimated-value-cur-proc"),
"url": f"https://ted.europa.eu/en/notice/-/detail/{n['publication-number']}",
})
with open("ted_tenders.csv", "w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=rows[0].keys())
w.writeheader()
w.writerows(rows)
print(len(rows), "notices")
3. When the Actor saves time
For a one-off look, the script above is enough and free. The Apify Actor TED EU Public Tenders (by Nexus Scrapers) wraps the same official API. It pays off when you would otherwise maintain that code yourself:
- One normalized schema. Every item has the same keys: a buyer object, ISO 3166 country codes, money as
{amount, currency}, dates asYYYY-MM-DD, a deadline with time and offset, unified notice types and procedure types. Missing values arenull, never missing keys. The same schema is used by our UK Actor, so EU and UK tenders can go into one table. - CPV codes with official labels, deduplicated, in any of the 24 EU languages (
cpvLabelLanguage), plus the main CPV and its division name. The labels come from the Publications Office's CPV 2008 vocabulary. - Award view. For award notices there is a dataset view with winners (name, country), awarded value and award date.
- Filters as plain inputs (keywords, countries, CPV prefixes, notice types, date range, deadline, value, buyer name) instead of hand-written expert queries, with an
expertQueryescape hatch. - Scheduling and integrations through Apify: run daily, then send the dataset to Slack, e-mail, Google Sheets or a webhook, or call it through the Apify API or MCP. Export as JSON, CSV or Excel.
- Retries and pagination are handled, up to 15,000 results per run.
Pricing is pay per result: $0.002 per result at the time of writing; the Actor page shows the current price.
What it does not do: it does not translate notices, does not fetch tender documents (it links to the notice), and does not expose per-lot detail. If you only need a single export, use the script above.
4. Real sample: 21 normalized records
These 21 records were produced on 4 November 2026 by running the Actor's own normalizer code on live API responses, locally and without the Apify platform: 13 contract notices (buyers in IT, DE, FR, ES or PL, estimated value at least 100,000, published 4 November 2026) and 8 award notices that name winners (same countries plus NL, published 4 November 2026). They are the first matches of those filters, at most three per country, not hand-picked for quality.
Download: sample.json (full records, UTF-8) and sample.csv (flattened: lists joined with | , opens in Excel). Free to use under the licence in section 5.
| Notice | Type | Buyer | Title | Main CPV label | Value | Winners |
|---|---|---|---|---|---|---|
| 766723-2026 | notice | IT | 1116 MI 01/01 - AFFIDAMENTO DEL SERVIZIO ATTINENTE ALL'ATTIVIT… | Construction supervision services | 795,867 EUR | - |
| 766718-2026 | notice | ES | El objeto del contrato proyectado consiste en el suministro, in… | Artificial lawn | 306,043 EUR | - |
| 766726-2026 | notice | FR | Prestations d'hôtesses d'accueil pour les besoins de l'EPCI de… | Reception services | 400,000 EUR | - |
| 766777-2026 | notice | ES | Servicios para la redacción del proyecto de construcción adapta… | Railway engineering services | 1,729,830 EUR | - |
| 766781-2026 | notice | DE | Lieferung eines Feuerwehrfahrzeugs HLF10 nach DIN 14530-26 für… | Firefighting vehicles | 450,000 EUR | - |
| 766970-2026 | notice | IT | Rif. IO - PA SERVICE ESTRATTORI | Analysis apparatus | 510,000 EUR | - |
| 767025-2026 | notice | IT | SERVIZIO DI RECAPITO DEI DOCUMENTI TRIBUTARI DELLA REGIONE TOSC… | Postal services related to letters | 9,438,041 EUR | - |
| 767161-2026 | notice | ES | Servicio de revisión y mantenimiento preventivo y correctivo de… | Lift-maintenance services | 298,598 EUR | - |
| 767206-2026 | notice | FR | Accord cadre à bon de commande - Réalisation de diagnostics de… | Pollution investigation services | 400,000 EUR | - |
| 767495-2026 | notice | FR | ABONNEMENT À DES PÉRIODIQUES ET À DES SOURCES D’INFORMATION SPÉ… | Subscription services | 1,371,204 EUR | - |
| 765701-2026 | notice | DE | Maßnahme B52HE151500001 - Neubau eines norbayerischen Hochleist… | Architectural, construction, engi… | 301,378 EUR | - |
| 765720-2026 | notice | PL | „Zakup autobusów o napędzie elektrycznym wraz z budową niezbędn… | Public-service buses | 58,500,000 PLN | - |
| 766016-2026 | notice | PL | Całodobowe świadczenie usług medycznych - KRP I | Medical practice and related serv… | 4,673,815 PLN | - |
| 763827-2026 | award | FR | Fourniture de gaz, location et maintenance d’installations asso… | Liquid nitrogen | 521,920 EUR | MESSER France SAS; LNI Swissgas France |
| 763848-2026 | award | DE | Rahmenvereinbarung Codeanalyse Open Source Software (CAOS) Los 1 | IT services: consulting, software… | 2,000,000 EUR | mgm security partners GmbH |
| 763850-2026 | award | DE | Rasterelektronenmikroskop | Laboratory, optical and precision… | 190,000 EUR | JEOL Germany GmbH |
| 763855-2026 | award | DE | SG Zeven - KuBiZ Zeven - Bodenbelagsarbeiten | Construction work | 240,770 EUR | Gavrilov Fußbodentechnik GmbH & Co. KG |
| 763865-2026 | award | PL | Wykonanie zastawek oraz zastawek z przepustem w Leśnictwach: Bu… | Works for complete or part constr… | 1,368,000 PLN | DREW - KOS SPÓŁKA Z OGRANICZONĄ ODPOWIE… |
| 764035-2026 | award | ES | Servicio de Transporte no Sanitario desde el ámbito territorial… | Taxi services | 169,692 EUR | TUAXI COOP V |
| 764046-2026 | award | PL | Budowa nowych przystanków osobowych na linii kolejowej nr 70 i… | Construction work | 24,250,943 PLN | PRIMOST POŁUDNIE Sp. z o.o.; BUDIMEX KO… |
| 764172-2026 | award | ES | Servicio de traducción e interpretación en los órganos judicial… | Interpretation services | 48,300 EUR | Ofilingua S.L. |
One award record in full (description shortened):
{
"schemaVersion": "1.0",
"source": "TED",
"sourceId": "763827-2026",
"url": "https://ted.europa.eu/en/notice/-/detail/763827-2026",
"documentUrls": {
"html": "https://ted.europa.eu/fr/notice/763827-2026/html",
"pdf": "https://ted.europa.eu/fr/notice/763827-2026/pdf",
"xml": "https://ted.europa.eu/en/notice/763827-2026/xml"
},
"noticeType": "contract-award",
"noticeTypeCode": "can-standard",
"procedureType": "open",
"contractNature": "supplies",
"title": "Fourniture de gaz, location et maintenance d’installations associées au profit de l’Unité Nationale de Police Judiciaire de la Gendarmerie Nationale (UNPJ-GN) à Cergy-Pontoise",
"description": "Fourniture de gaz, location et maintenance d’installations associées au profit de l’Unité Nationale de Police Judiciaire de la Gendarmerie Nationale (UNPJ-GN)…",
"language": "fr",
"buyer": {
"name": "COMSOPGN",
"country": "FR",
"city": "Le Blanc",
"nutsCode": "FRB03",
"buyerType": "central-government"
},
"cpvCodes": [
{
"code": "24111800",
"label": "Liquid nitrogen"
},
{
"code": "24111000",
"label": "Hydrogen, argon, rare gases, nitrogen and oxygen"
},
{
"code": "24111600",
"label": "Hydrogen"
},
{
"code": "31120000",
"label": "Generators"
}
],
"mainCpv": "24111800",
"mainCpvDivision": "Chemical products",
"estimatedValue": null,
"lots": 3,
"publicationDate": "2026-11-04",
"submissionDeadline": null,
"awardInfo": {
"winners": [
{
"name": "MESSER France SAS",
"country": "FR"
},
{
"name": "LNI Swissgas France",
"country": "FR"
}
],
"awardedValue": {
"amount": 521920,
"currency": "EUR"
},
"awardDate": "2026-08-18"
},
"placeOfPerformance": {
"country": "FR",
"city": null,
"nutsCode": null
},
"scrapedAt": "2026-11-04T22:02:44+00:00"
}
5. Data licence and attribution
The TED legal notice says the European Commission's reuse policy is implemented by the Commission Decision of 12 December 2011 on the reuse of Commission documents (2011/833/EU), and that, unless otherwise noted, procurement notices published in the Supplement to the Official Journal of the European Union can be freely reused for commercial or non-commercial purposes. In practice: credit the source (for example "Source: TED, Publications Office of the European Union") and indicate if you changed the data. The editorial content of the TED websites is CC BY 4.0. Notices can contain names of natural persons (sole traders, contacts), so handle them in line with GDPR. CPV labels come from the CPV 2008 vocabulary of the Publications Office (Regulation (EC) No 213/2008). This is a summary, not legal advice; read the legal notice for the exact terms.