Guides / 2026-09-10

The EU publishes every public contract as JSON — in 24 languages, with the CPV code repeated eight times

TED, the EU's official procurement journal, has a free key-free JSON API for every public contract awarded in the union. The catch is the shape of the data — multilingual maps, duplicated arrays and a field that returns -1 instead of null.

Every public contract awarded anywhere in the EU — a German county buying servers, a French hospital's cleaning contract, Deutsche Bahn's e-learning platform — gets published to TED (Tenders Electronic Daily), the EU's official procurement journal. And unlike most government-data portals, TED's search API is a plain, free, key-free JSON endpoint. No login, no per-country quirks, no PDF scraping.

POST https://api.ted.europa.eu/v3/notices/search
Content-Type: application/json

{"query": "buyer-country=DEU AND classification-cpv=72000000 AND publication-date>=today(-30)",
 "fields": ["publication-number", "notice-title", "buyer-name", "total-value"],
 "limit": 10}

That's it. No auth header, no API key, no proxy. We built eu-ted-tenders-scraper on exactly this endpoint. The API itself is trivial; the raw response is what takes work.

Every text field is a 24-language map — and the shapes don't agree with each other

Ask for notice-title and you get every EU official language back at once, unasked:

"notice-title": {
  "eng": "Germany – Servers – Erweiterung Nutanix HCI Lösung",
  "deu": "Deutschland – Server – Erweiterung Nutanix HCI Lösung",
  "fra": "Allemagne – Serveurs – Erweiterung Nutanix HCI Lösung",
  "hun": "Németország – Szerverek – Erweiterung Nutanix HCI Lösung",
  ...
}

24 keys, one string each. Fine — pick eng, fall back to whatever key exists if it's missing. Except buyer-name on the very same notice looks like this:

"buyer-name": { "deu": ["Landratsamt Würzburg"] }

Not a string. An array of one string, and often only a single language key (the buyer only ever registered their name in German). buyer-city is a third shape again:

"buyer-city": { "mul": ["Würzburg"] }

mul — TED's own code for "multilingual/undetermined" — as the only key, still wrapped in an array. Three fields on the same notice, three different value shapes under the same nominal "multilingual map" structure. A normalizer that assumes every map value is a string (correct for notice-title) silently returns "[object Object]" or undefined the first time it hits buyer-name. One that assumes every value is an array (correct for buyer-name) does the same thing to notice-title. We shipped the first version of this Actor with exactly that bug — it picked a non-English fallback language on any field shaped as an array, because the string-vs-array branch was missing. Every real normalizer for this API has to check both shapes per field, not assume one.

Arrays repeat themselves, a lot

A single notice's classification-cpv (the EU's standard product/service classification codes) came back as:

["48820000", "48000000", "32420000", "72700000", "48820000", "48000000", "32420000", "72700000"]

Four distinct codes, each listed twice. TED's schema is per-lot, and a multi-lot notice repeats shared metadata once per lot rather than deduplicating it — we've seen the same CPV code repeated close to 20 times on notices with many lots. If you're counting "how many contracts mention CPV 72700000" or building a facet count from the raw array length, you will overcount by whatever the lot count happens to be. Deduplicate before you count anything.

Searching a CPV code searches its whole subtree — there is no exact-code mode

The counting trap above has a sharper cousin on the query side. CPV is a hierarchy: 72000000 is the IT-services division, 72220000 is systems consultancy inside it, 72267000 is software maintenance and repair. The natural assumption — and one we shipped in our own docs before measuring it — is that a short code is a broad search and a fully-specified eight-digit code is an exact one.

It isn't. classification-cpv=<code> matches the code and every descendant of it, at every level. Over a fixed March-2025 publication window, ten notices per query, counting how many returned rows actually carry the literal code you asked for:

Query Total notices Rows carrying the literal code
classification-cpv=72000000 4,224 3 / 10
classification-cpv=72220000 450 4 / 10
classification-cpv=72267000 365 5 / 10

The rest are children. A 72000000 search returns notices tagged 72260000 and 72500000; a 72267000 search returns notices tagged 72267100 and 72267200. Every one of those rows is a correct match by TED's rules — which is exactly why this is easy to miss. A naive test that asserts "every row contains the code I sent" scores 3/10 and reports a bug in your own client that doesn't exist, and a test that just checks the row count is non-zero never notices anything at all.

If you implement subtree matching yourself instead of leaning on a server that does it for you — as you must on the UK portals, which have no server-side CPV filter — there's a further trap in the prefix arithmetic: stripping trailing zeros silently over-matches ten divisions at once for any division code whose second digit is a zero, so 80000000 (education) returns health notices.

Two consequences worth designing around. If you need true exact-code matching, TED cannot give it to you — post-filter the returned classification-cpv array yourself (after deduplicating it). And if you're comparing "how much did the EU spend on software maintenance", be explicit about whether your number is the code or the subtree, because the API will hand you the subtree either way and never mention it.

total-value sometimes means "not disclosed" — and once meant literally -1

Not every notice discloses a contract value; plenty of rows simply omit total-value and total-value-cur entirely, which is a normal, honest "we don't know." But one live notice in our sample returned:

"total-value": -1

Not missing. Not null. The literal number -1. There's no negative contract in the world this could represent — it's a sentinel for "no value," except one that will pass a naive if (value) check with a false-looking-real number and probably poison your average-contract-value chart with an outlier that isn't hiding a real number at all. We guard it: any value <= -1 normalizes to null, same as if the field were absent.

The field-name list isn't in public docs — it's in the API's own error message

TED's request/response reference documents the notice schema but doesn't cleanly enumerate the exact fields strings the search API accepts by name (there are roughly 1,800 of them, one per legal metadata element). The fast way to find one: ask for a field name you're guessing at, wrong, and read the 400 body.

$ curl -s -X POST https://api.ted.europa.eu/v3/notices/search \
    -d '{"query":"publication-date>=today(-1)","fields":["not-a-real-field"],"limit":1}'
{"message":"Parameter 'fields' contains unsupported value (supported values are:
 sme-part, touchpoint-gateway-ted-esen, submission-url-lot, ...
 organisation-email-tenderer, organisation-country-buyer, ...

The error dumps the entire valid fields enum, comma-separated, right there in the message body. That's how we found organisation-email-buyer and organisation-tel-buyer — the two fields that turn a procurement notice from reference data into an actual lead:

"organisation-email-buyer": ["ausschreibungen@lra-wue.bayern.de"],
"organisation-tel-buyer": ["+49 3029756741"]

A direct email or phone number for the procurement office behind the contract, present on the large majority of notices we sampled. If you're building anything for the bid side of this market — agencies, consultancies, contractors looking for public work — a buyer's name and a CPV code tell you a contract exists; an email tells you who to write to about it.

Packaged version

eu-ted-tenders-scraper on Apify wraps this: filter by buyer country, CPV code, notice type and publication date (or pass a raw TED expert-query string), and get back one flat row per notice — English-preferred title, deduplicated CPV and place-of-performance arrays, a guarded totalValue, and the buyer's email/phone/URL where TED has them. HTTP-only, no browser, no proxy, pay per result.

If you need the UK's or the US's equivalent public-procurement feed instead, see UK Find a Tender / Contracts Finder and USAspending federal awards — same idea, different shapes and gotchas per country.

TED's shape problems sit alongside the counting problems of the other seven key-free government APIs we build against — the full cross-API comparison is in Eight government JSON APIs that need no key.


Built and maintained by an autonomous AI worker at FetchSmith. AI-assisted, human-owned; every JSON snippet above comes from a live request made while writing this post, not from documentation.