What is the TED API v3?
TED (Tenders Electronic Daily) is the EU's official journal for public procurement, where every above-threshold tender notice from all 27 member states is published. Below-threshold tenders live on national portals, covered in how to find EU tenders. The current TED API is version 3, and its Search API exposes those notices as JSON through a single endpoint:
POST https://api.ted.europa.eu/v3/notices/search
You can call it without any authentication, so it is open to every reuser. The complete request and response reference lives in the Swagger documentation.
Download the OpenAPI specification
The Swagger page is generated from an OpenAPI 3.1 file you can download directly and feed into a client generator or Postman:
https://api.ted.europa.eu/api-v3.yaml
It covers every v3 endpoint, including search, notice submission, validation and rendering.
Do you need an API key?
Not to search. The Search API is fully public. A key is only needed for the other endpoints, such as submitting, validating or rendering your own notices. If you do want one, it takes three steps:
- Visit the TED Developer Portal.
- Log in with your EU Login credentials.
- Generate your key under Manage API Keys, and copy it right away because it can't be shown again.
Search tenders with Python
Every request carries three things: a query, the fields you want back, and a limit. The query itself uses TED's expert search syntax, written as field operator value and combined with AND, OR, and NOT. These are the fields you will reach for most often:
| Field | Example | Meaning |
|---|---|---|
classification-cpv |
classification-cpv=38000000 |
CPV code (sector) |
buyer-country |
buyer-country=DEU |
Buyer country, ISO 3-letter |
publication-date |
publication-date>=20260420 |
Published on or after a date |
estimated-value-proc |
estimated-value-proc>=50000 |
Estimated value of the procurement |
Rather than memorize the syntax, build your query visually in the TED expert search and copy the resulting query string into your code. If you are unsure which CPV code fits your sector, look it up with the CPV tool.
The example below finds laboratory-equipment tenders (CPV 38000000) published since the start of the year, with an estimated value between 50,000 and 500,000 EUR, from buyers in Germany. Because a single page holds at most 250 notices, it pages through results with iteration mode, pulling five pages of 100 for up to 500 notices in one run:
import requests
import time
URL = "https://api.ted.europa.eu/v3/notices/search"
query = " AND ".join([
"classification-cpv=38000000", # Lab equipment
"buyer-country=DEU", # Germany
"publication-date>=20260101",
"estimated-value-proc>=50000",
"estimated-value-proc<=500000",
])
payload = {
"query": query,
"fields": [
"publication-number",
"notice-title",
"classification-cpv",
"estimated-value-proc",
"estimated-value-cur-proc",
],
"limit": 100,
"scope": "ACTIVE",
"paginationMode": "ITERATION",
}
notices = []
for _ in range(5): # Fetch five pages.
data = requests.post(URL, json=payload).json()
notices.extend(data["notices"])
# Use iteration token to paginate results efficiently.
token = data.get("iterationNextToken")
if not token or len(data["notices"]) < payload["limit"]:
break
payload["iterationNextToken"] = token
time.sleep(1)
for notice in notices:
number = notice["publication-number"]
title = notice["notice-title"]["eng"]
cpv = ", ".join(notice["classification-cpv"])
value = notice.get("estimated-value-proc", "n/a")
currency = notice.get("estimated-value-cur-proc", "")
print(f"{number} | {title} | {value} {currency} | {cpv}")
A couple of response details are worth knowing. notice-title is keyed by language code, so notice-title["eng"] returns the English title, while classification-cpv arrives as a list of codes. In iteration mode every page hands you an iterationNextToken that you pass back on the next request, and the loop stops as soon as a page comes back smaller than your limit. From here you can swap DEU for any country code, or 38000000 for the sector you bid on.
Filter by CPV code and keywords
Use IN (...) to match several CPV codes at once. Newer topics such as blockchain, AI or cybersecurity have no CPV code of their own, so buyers file them under broad IT codes like 72000000 (IT services) or 48000000 (software packages). Combine those with a full-text search on FT to narrow them down:
query = (
"FT~(blockchain) "
"AND classification-cpv IN (72000000 48000000) "
"AND publication-date>=20260101"
)
Not sure which code fits your sector? Search all 9,000+ codes with the CPV tool.
Bulk download historical notices
For backfills, paging through the Search API is slow. TED publishes every Official Journal issue as a daily package of XML notices, available without signing in:
https://ted.europa.eu/packages/daily/{yyyynnnnn}
https://ted.europa.eu/packages/monthly/{yyyy-nn}
The daily pattern takes the OJ S number: the year followed by the five-digit issue number, so 202600001 is the first issue of 2026. The monthly pattern takes the year and month, such as 2026-01. Both return a .tar.gz archive of eForms XML, and TED's release calendar maps dates to issue numbers. Parsing eForms XML is a project of its own, because notices use nested UBL structures and many fields only exist in some notice types.
Limits and good practice
The Search API is generous, but a handful of limits shape how you use it:
-
Page size. A request returns at most 250 notices, so anything larger needs
paginationMode: "ITERATION"and the token loop shown above. -
Query size. A single query can hold up to 2,000 clauses. When you target many CPV codes, group them with
IN (...)rather than chaining a long list ofORterms. - Rate limits. Calls are throttled, so space out requests in scheduled jobs and use bulk packages for backfills.
-
Field shapes. Many fields are multilingual or come back as lists, which is why the example reads them defensively with
.get()and a language key. - Be a good citizen. Cache what you fetch and avoid pulling the same notices twice. TED refreshes throughout the day, so an hourly or daily sync is usually plenty.
That is the entire loop: build a query, POST it, read the JSON, and paginate. From there you can store notices in a database, push alerts to your team, or feed them straight into your own product.
Top comments (0)