TED — Contratos de poderes adjudicadores españoles en el DOUE
Anuncios de licitación y adjudicación de compradores españoles publicados en Tenders Electronic Daily, el suplemento S del Diario Oficial de la UE. Recoge sobre todo contratos sujetos a regulación armonizada (por encima de los umbrales europeos).
Ficha técnica
- Formato
- JSON (API de búsqueda v3), XML eForms/TED schema (paquetes diarios y mensuales), CSV (subconjunto 2006–2023)
- Registros
- ~515.000 avisos con comprador español en la API (octubre 2026)
- Cobertura
- Paquetes XML: 1993–actualidad; API de búsqueda: octubre 2016–actualidad; CSV: 2006–2023
- Actualización
- Diaria (de lunes a viernes, con cada número del DOUE S)
- Licencia
- Reutilización libre, comercial o no (Decisión 2011/833/UE)
- Entidad
- Oficina de Publicaciones de la Unión Europea
Descripción
TED (Tenders Electronic Daily) es la versión en línea del Suplemento del Diario Oficial de la Unión Europea (DOUE S), que gestiona la Oficina de Publicaciones de la UE. En él se publican obligatoriamente los contratos sujetos a regulación armonizada, es decir, los que superan los umbrales europeos, y los poderes adjudicadores españoles publican también de forma voluntaria algunos de menor importe. La mayoría de los anuncios españoles llegan a TED desde la Plataforma de Contratación del Sector Público o desde las plataformas autonómicas, que figuran como eSenders oficiales. Por eso casi todo lo que aparece aquí está también en PLACSP: TED sirve para cruzar datos y para comparar con otros países, más que como fuente adicional de contratos.
Hay tres vías de acceso. La API de búsqueda (POST https://api.ted.europa.eu/v3/notices/search) es pública y no necesita clave; admite el lenguaje de consulta experta del buscador de TED, y España se filtra con buyer-country=ESP. Devuelve como máximo 250 avisos por petición, y para recorrer más de 15.000 hay que paginar con paginationMode ITERATION. Los paquetes XML (https://ted.europa.eu/packages/daily/ y /packages/monthly/) contienen todos los avisos de un número del DOUE o de un mes, de todos los países, así que el filtrado por España se hace al procesarlos. Por último, data.europa.eu mantiene un subconjunto en CSV con los campos principales de anuncios de licitación y de adjudicación, que solo llega hasta 2023 y no incluye avisos eForms.
Desde el 14 de noviembre de 2022 TED publica avisos eForms (Reglamento de Ejecución (UE) 2019/1780, basado en UBL 2.3), que convivieron un tiempo con los antiguos formularios TED schema (R2.0.9), así que un histórico completo obliga a leer los dos esquemas. En eForms los compradores y adjudicatarios se declaran una vez como organizaciones (ORG-0001...) y los lotes y resultados las referencian por identificador. Los textos llegan en varios idiomas, y la API los devuelve agrupados por código de idioma (spa, mul...).
Estructura de datos
- publication-number — Número de publicación en TED (p. ej. 599896-2026)
- publication-date — Fecha de publicación en el DOUE S
- notice-type — Tipo de aviso eForms (cn-standard, can-standard, pin-only...)
- form-type — Familia del formulario (competition, result, planning...)
- buyer-name — Nombre del poder adjudicador
- buyer-country — País del comprador (ESP para España)
- organisation-identifier-buyer — Identificador del comprador (NIF, DIR3...)
- buyer-city — Municipio del comprador
- notice-title — Título del aviso en cada idioma
- classification-cpv — Códigos CPV del objeto
- contract-nature — Naturaleza (works, supplies, services)
- procedure-type — Procedimiento (open, restricted, neg-w-call...)
- place-of-performance — Lugar de ejecución (NUTS y país)
- estimated-value-proc / estimated-value-lot — Valor estimado del procedimiento y de cada lote
- winner-name / winner-identifier / winner-country — Adjudicatario, su identificador fiscal y su país
- result-value-notice — Valor total adjudicado en el aviso
- deadline-receipt-tender-date-lot — Fecha límite de presentación de ofertas
Ejemplos de código
curl -s -X POST "https://api.ted.europa.eu/v3/notices/search" \
-H "Content-Type: application/json" \
-d '{
"query": "buyer-country=ESP AND notice-type=can-standard AND publication-date>=today(-7)",
"fields": ["publication-number", "publication-date", "buyer-name", "winner-name", "result-value-notice", "classification-cpv"],
"limit": 10
}'
# XML eForms completo de un aviso concreto
curl -s -o 599896-2026.xml "https://ted.europa.eu/en/notice/599896-2026/xml"
# Paquete diario completo (todos los países) del DOUE S 198/2026
curl -sL -o ted_202600198.tar.gz "https://ted.europa.eu/packages/daily/202600198" import csv
import requests
URL = 'https://api.ted.europa.eu/v3/notices/search'
consulta = {
'query': 'buyer-country=ESP AND notice-type=can-standard AND publication-date>=20260901 AND publication-date<=20260930',
'fields': ['publication-number', 'publication-date', 'buyer-name',
'winner-name', 'winner-identifier', 'result-value-notice',
'classification-cpv', 'procedure-type'],
'limit': 250,
'paginationMode': 'ITERATION',
}
avisos = []
while True:
r = requests.post(URL, json=consulta, timeout=60)
r.raise_for_status()
datos = r.json()
avisos.extend(datos['notices'])
token = datos.get('iterationNextToken')
if not token or not datos['notices']:
break
consulta['iterationNextToken'] = token
print(datos['totalNoticeCount'], 'avisos en total;', len(avisos), 'descargados')
def es(valor):
# Los textos vienen por idioma: {'spa': [...]} o {'mul': [...]}
if isinstance(valor, dict):
valor = valor.get('spa') or next(iter(valor.values()), [])
return '; '.join(valor) if isinstance(valor, list) else (valor or '')
with open('ted_adjudicaciones_es.csv', 'w', newline='', encoding='utf-8') as f:
w = csv.writer(f)
w.writerow(['aviso', 'fecha', 'comprador', 'adjudicatario', 'nif', 'importe_eur', 'cpv'])
for a in avisos:
w.writerow([a['publication-number'], a['publication-date'][:10],
es(a.get('buyer-name')), es(a.get('winner-name')),
es(a.get('winner-identifier')), a.get('result-value-notice', ''),
es(a.get('classification-cpv'))]) import tarfile
import xml.etree.ElementTree as ET
NS = {
'cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2',
'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2',
'efac': 'http://data.europa.eu/p27/eforms-ubl-extension-aggregate-components/1',
}
def comprador_espanol(root):
# El comprador se referencia por ID (ORG-0001) en ContractingParty
# y sus datos están en las extensiones eForms (efac:Organization)
ids = {e.text for e in root.findall(
'.//cac:ContractingParty/cac:Party/cac:PartyIdentification/cbc:ID', NS)}
for org in root.findall('.//efac:Organization/efac:Company', NS):
org_id = org.findtext('cac:PartyIdentification/cbc:ID', namespaces=NS)
pais = org.findtext('cac:PostalAddress/cac:Country/cbc:IdentificationCode', namespaces=NS)
if org_id in ids and pais == 'ESP':
return org.findtext('cac:PartyName/cbc:Name', namespaces=NS)
return None
total = espanoles = 0
with tarfile.open('ted_202600198.tar.gz', 'r:gz') as tar:
for m in tar:
if not m.name.endswith('.xml'):
continue
total += 1
root = ET.parse(tar.extractfile(m)).getroot()
nombre = comprador_espanol(root)
if nombre:
espanoles += 1
tipo = root.findtext('cbc:NoticeTypeCode', namespaces=NS)
print(m.name, tipo, nombre)
print(f'{espanoles} avisos de compradores españoles de {total}')