Nexus Tools / Data guides

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.

Updated: November 2026. API behaviour below was tested live on 4 November 2026.

The short version

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

GoalQuery
Country of the buyer (three-letter ISO code)buyer-country=ITA or buyer-country IN (ITA FRA)
CPV prefixclassification-cpv=72* or classification-cpv IN (72* 45*)
Notice typenotice-type=cn-standard, notice-type IN (can-standard can-social)
Publication datepublication-date>=20261020 or publication-date>=today(-7)
Full textFT~("cybersecurity")
Buyer name wordbuyer-name~"comune"
Valuetotal-value>=1000000
Sort, newest firstappend 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

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:

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:

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.

NoticeTypeBuyerTitleMain CPV labelValueWinners
766723-2026noticeIT1116 MI 01/01 - AFFIDAMENTO DEL SERVIZIO ATTINENTE ALL'ATTIVIT…Construction supervision services795,867 EUR-
766718-2026noticeESEl objeto del contrato proyectado consiste en el suministro, in…Artificial lawn306,043 EUR-
766726-2026noticeFRPrestations d'hôtesses d'accueil pour les besoins de l'EPCI de…Reception services400,000 EUR-
766777-2026noticeESServicios para la redacción del proyecto de construcción adapta…Railway engineering services1,729,830 EUR-
766781-2026noticeDELieferung eines Feuerwehrfahrzeugs HLF10 nach DIN 14530-26 für…Firefighting vehicles450,000 EUR-
766970-2026noticeITRif. IO - PA SERVICE ESTRATTORIAnalysis apparatus510,000 EUR-
767025-2026noticeITSERVIZIO DI RECAPITO DEI DOCUMENTI TRIBUTARI DELLA REGIONE TOSC…Postal services related to letters9,438,041 EUR-
767161-2026noticeESServicio de revisión y mantenimiento preventivo y correctivo de…Lift-maintenance services298,598 EUR-
767206-2026noticeFRAccord cadre à bon de commande - Réalisation de diagnostics de…Pollution investigation services400,000 EUR-
767495-2026noticeFRABONNEMENT À DES PÉRIODIQUES ET À DES SOURCES D’INFORMATION SPÉ…Subscription services1,371,204 EUR-
765701-2026noticeDEMaßnahme B52HE151500001 - Neubau eines norbayerischen Hochleist…Architectural, construction, engi…301,378 EUR-
765720-2026noticePL„Zakup autobusów o napędzie elektrycznym wraz z budową niezbędn…Public-service buses58,500,000 PLN-
766016-2026noticePLCałodobowe świadczenie usług medycznych - KRP IMedical practice and related serv…4,673,815 PLN-
763827-2026awardFRFourniture de gaz, location et maintenance d’installations asso…Liquid nitrogen521,920 EURMESSER France SAS; LNI Swissgas France
763848-2026awardDERahmenvereinbarung Codeanalyse Open Source Software (CAOS) Los 1IT services: consulting, software…2,000,000 EURmgm security partners GmbH
763850-2026awardDERasterelektronenmikroskopLaboratory, optical and precision…190,000 EURJEOL Germany GmbH
763855-2026awardDESG Zeven - KuBiZ Zeven - BodenbelagsarbeitenConstruction work240,770 EURGavrilov Fußbodentechnik GmbH & Co. KG
763865-2026awardPLWykonanie zastawek oraz zastawek z przepustem w Leśnictwach: Bu…Works for complete or part constr…1,368,000 PLNDREW - KOS SPÓŁKA Z OGRANICZONĄ ODPOWIE…
764035-2026awardESServicio de Transporte no Sanitario desde el ámbito territorial…Taxi services169,692 EURTUAXI COOP V
764046-2026awardPLBudowa nowych przystanków osobowych na linii kolejowej nr 70 i…Construction work24,250,943 PLNPRIMOST POŁUDNIE Sp. z o.o.; BUDIMEX KO…
764172-2026awardESServicio de traducción e interpretación en los órganos judicial…Interpretation services48,300 EUROfilingua 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.

Links