EU procurement data · API implementation
The TED Search API: retrieve published notices reliably.
Use the anonymous TED Search API v3 to retrieve published EU procurement notices with expert queries, iteration pagination, XML capture and validation.
A reliable TED Search API v3 collector uses an explicit expert query and field projection, walks complete result sets in ITERATION mode, fails any response with timedOut: true, preserves notice versions and raw XML, and reconciles each publication window to the corresponding OJ S package. Published-notice search is anonymous; no API key is required.
The production endpoint is:
POST https://api.ted.europa.eu/v3/notices/search
Page-number mode is convenient but stateless and limited to 15,000 retrievable notices. Iteration mode creates a consistent point-in-time traversal with no total-result ceiling. Both modes allow at most 250 notices and 10,000 effective field cells per page. (TED Search API; official v3 contract)
Those facts produce a valid request. The production work begins with the details: scope and onlyLatestVersions change the population, multilingual values are objects rather than strings, many lot-level fields are arrays, a notice is not a unique procurement, tokens expire, and a successful HTTP response can still report a timed-out search.
Published-notice search
POSThttps://api.ted.europa.eu/v3/notices/search- Authentication
- Anonymous; no API key required
- Environment
- Production only; Search is unavailable in Preview
Per-page budget
- Notice limit
- ≤ 250
- Field cells
- ≤ 10,000
limit defaults to 10. TED documents fields.length × limit, but live enforcement budgets the union of requested fields with the automatically added publication number and links. At 250 notices, that union can contain at most 40 fields.
queryExpert-search expression
Purpose
Define the published-notice population with TED search fields and operators.
Guardrail
Keep the exact query string in the run manifest; do not reconstruct it from normalized filters.
fieldsExplicit response projection
Purpose
Request only the fields the collector stores, profiles, and validates.
Guardrail
The field catalogue can change inside v3; retain the list and the OpenAPI fingerprint used by the run.
scopeLATEST · ACTIVE · ALL
Purpose
Choose the current OJ S release, active notices, or the website's rolling ten-year searchable population.
Guardrail
The default is ALL. Set the scope explicitly so a collector cannot change population by omission.
checkQuerySyntaxfalse by default
Purpose
Set true to validate the expert query without executing the search.
Guardrail
Treat validation and collection as separate recorded calls.
onlyLatestVersionsfalse by default
Purpose
Choose whether TED should apply its latest-version search filter.
Guardrail
Set it explicitly; this is not one row per procedure or the same as notice-version == 1.
paginationModePAGE_NUMBER · ITERATION
Purpose
Select addressable pages or a sequential point-in-time traversal.
Guardrail
The default is PAGE_NUMBER. Use ITERATION when complete retrieval matters.
page / iterationNextTokenpage starts at 1
Purpose
Send a page number in PAGE_NUMBER mode or the prior response token in ITERATION mode.
Guardrail
Do not send a token on the first iteration request or mix the two cursor models.
TED Search API v3: the short answer
Use this sequence:
- Confirm that the read-only Search API is the correct TED service; do not mix it with notice-submission workflows.
- Define the notice population with an exact expert query,
scope, version policy, and retrieval cutoff. - Validate the query once with
checkQuerySyntax: true, then execute it in a separate call. - Request a documented field list whose effective projection, including TED's implicit publication number and links, stays at or below 10,000 field cells per page.
- Use
PAGE_NUMBERonly for bounded browsing; useITERATIONfor complete collection. - Stop an iteration when
noticesis empty, not when the response token disappears. - Reject
timedOut: trueeven when the endpoint returned HTTP 200 and some notices. - Save every raw response, request body, retrieval time, response hash, and OpenAPI fingerprint before normalization.
- Preserve publication number, Notice ID, VersionID, Procedure ID, OJ S issue, and XML identifiers separately.
- Follow each result's XML link when lot relationships, source-authored language, change evidence, or schema fidelity matters.
- Reconcile publication-date or OJ S extracts to daily XML packages; use monthly packages for durable historical replay.
- Publish page, identity, XML, package, schema, and missing-field checks with the accepted data.
This guide begins after TED has been selected as the source. If the decision is still TED versus SAM.gov, Find a Tender, or Contracts Finder, use the government tender source comparison. If the goal is to inspect normalized current data rather than own a source connector, start with the free tenders and RFP dataset.
Use Search for published notices, not submission
TED API is a family of services, not one interchangeable endpoint. The TED API v3 overview distinguishes published-notice search from publication, validation, visualisation, and conversion operations.
Search API: anonymous read access
The Search API retrieves notices already published on the TED website. It accepts the same expert-query language used by the website and returns requested fields plus links to available XML, PDF, signed-PDF, and HTML representations. It is intended for data reusers, researchers, commercial organisations, and developers. It does not require authentication.
Use it for jobs such as:
- collecting a dated publication window or OJ S issue;
- finding notices by buyer, CPV, NUTS, procedure, publication number, notice family, or source date;
- retaining current and historical notice versions under a written policy;
- discovering linked XML for schema-aware parsing; and
- feeding a downstream lifecycle, alerting, or market-analysis system.
Publication and Validation APIs: authenticated write workflows
Submitting, managing, validating, or previewing unpublished eForms notices is a different operational domain. Those routes require API keys or eSender context and have their own lifecycle rules. Do not send a read-only reuser through API-key setup, and do not use Search documentation to design a notice-submission system.
TED says v2 remains supported until v4 becomes available; it does not publish a v2 shutdown date. New work should target the unified v3 gateway, but a migration plan should test real response contracts rather than call v2 retired. (TED API v3 transition)
Write the source contract before the first request:
Published procurement notices returned by TED Search API v3 for the recorded expert query, scope, version policy, fields, pagination mode, and retrieval cutoff, supplemented by successfully retrieved official XML and stated package reconciliations.
That sentence prevents the result from silently becoming “all EU tenders.”
Define the notice grain, scope, and publication clock
Search returns notices. One result is not automatically one unique procedure, competition, lot, tender, contract, buyer, or currently actionable bid.
TED publishes consultation, planning, competition, direct-award prenotification, result, contract-modification, completion, and change forms. One procedure can therefore accumulate several notices and versions. A result notice can contain several lot results, tenders, winners, and contracts. A change notice has its own notice identity and refers to the notice version it changes. (eForms schema; change notices)
Set scope explicitly
The v3 request offers three scopes:
LATEST: notices in the current OJ S release;ACTIVE: TED's convenience population of recent planning notices, unexpired competition notices, and recent result notices; andALL: the website's rolling ten-year searchable population.
ACTIVE does not mean “open for bids.” TED's current help definition includes planning notices sent during the previous 364 days and result notices sent during the previous 99 days. ALL is not an unlimited legal archive; the website search covers the rolling last ten years. Use the release packages and preservation routes for durable historical replay. (TED search scopes)
Set onlyLatestVersions explicitly
The default is false. Setting it to true applies TED's latest-version search filter: older published versions are filtered and, when a change notice exists, the latest change notice can become the displayed result. It does not collapse all notices belonging to the same procedure, convert change notices into a final master record, equal notice-version == 1, or preserve the states that were superseded.
For monitoring and reproducible history, collect all versions and model current state downstream. For a current discovery interface, latest-only may be appropriate—but it remains a declared dataset policy, not a harmless display option.
Follow the OJ S release calendar
TED publishes daily editions nominally Monday through Friday, with holiday exceptions governed by the release calendar. The website publishes during the release morning, and TED states that the daily XML package is available by 09:30 at the latest. Treat that as a scheduled edition, not a continuous modified-since event stream. (TED data reuse)
Make the first TED Search API v3 request
This request retrieves a bounded publication-day slice, keeps all notice versions, and asks only for fields the example will store:
curl --fail-with-body \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"query": "publication-date = 20260812 AND form-type = competition",
"fields": [
"publication-number",
"publication-date",
"ojs-number",
"notice-identifier",
"notice-version",
"procedure-identifier",
"form-type",
"notice-type",
"official-language",
"notice-title",
"buyer-name",
"classification-cpv",
"deadline-receipt-tender-date-lot"
],
"page": 1,
"limit": 100,
"scope": "ALL",
"checkQuerySyntax": false,
"paginationMode": "PAGE_NUMBER",
"onlyLatestVersions": false
}' \
'https://api.ted.europa.eu/v3/notices/search'
TED's prose formula would budget 13 × 100 = 1,300 cells. Live enforcement budgets the union of those fields with the automatically added publication-number and links. Because publication number is already requested here, the effective budget is 14 × 100 = 1,400—still comfortably below 10,000. The page is one-based. The response includes notices, totalNoticeCount, iterationNextToken, and timedOut; each notice also receives a links object for available formats.
On August 12, 2026 at approximately 11:23 UTC, a live bounded smoke test of publication-date = 20260812 returned HTTP 200, timedOut: false, OJ S 154/2026, and publication 556964-2026. The full publication-day query reported 3,257 notices at that retrieval cutoff. That count verifies the route and request contract; it is not used elsewhere as a market-size claim.
The live v3 OpenAPI document used for this article had SHA-256:
ee05c02f74d29d006147964d5f85cfe070764eca7c5bad6491e72724201d3e58
Pinning the contract matters because TED explicitly excludes the changing field catalogue from major-version guarantees. A field can be added, removed, or updated without changing /v3/. (v3 versioning statement)
Build and check expert queries
The expert-query language combines a search field, an operator, and one or more values. Expressions can use AND, OR, NOT, parentheses, comparison operators, IN, NOT IN, and SORT BY. TED publishes both descriptive names and aliases. These three date expressions are equivalent:
publication-date = 20260812
BT-05(a)-notice = 20260812
PD = 20260812
Prefer the descriptive name in durable application code and retain the exact string. Use IN for code lists rather than repeating several OR expressions:
publication-date = (20260801 <> 20260812)
AND notice-type IN (cn-standard cn-social)
AND buyer-country IN (ROU DEU FRA)
AND classification-cpv = 72*
TED's help documents the date-range <> syntax, wildcard rules, exact phrases, aliases, stemming, and operator constraints. Prototype the population in the Expert Search interface, then copy the accepted query into the collector. (TED search syntax)
Run syntax validation separately
Set checkQuerySyntax to true before a newly deployed or changed query. The API checks the expression but does not execute the search. A live validation call returned an empty notices array, totalNoticeCount: null, and timedOut: false; those values are not a zero-result search.
{
"query": "publication-date = 20260812 AND form-type = competition",
"fields": ["publication-number"],
"page": 1,
"limit": 1,
"scope": "ALL",
"checkQuerySyntax": true,
"paginationMode": "PAGE_NUMBER",
"onlyLatestVersions": false
}
Record the validation response, then execute a second request with checkQuerySyntax: false. Do not treat a syntax-check envelope as collected data.
The 400 response family distinguishes syntax errors, unknown fields, invalid formats, unsupported operations or values, and expired or invalid iteration tokens. Persist the structured error body; it is more actionable than a generic “bad request” log.
Select fields without breaking the page budget
The fields array defines the notice projection. It must contain at least one unique field. TED documents the page budget as:
fields.length × limit ≤ 10,000
limit ≤ 250
Live v3 testing showed that the service budgets the union of requested fields with its automatically added publication-number and links fields. Use this production rule:
size(set(fields) ∪ {publication-number, links}) × limit ≤ 10,000
1 ≤ limit ≤ 250
At the maximum 250 notices, the effective union can contain at most 40 fields. A live request with 39 projected fields including publication-number succeeded because the implicit links field brought the union to 40. Forty requested fields failed because their union with links contained 41 fields, or 10,250 cells. The OpenAPI schema permits limit: 0, but the live endpoint rejects it; contract tests should enforce the observed range of 1–250 and alert if the service changes.
Choose fields by downstream contract, not by “everything that might be useful.” A practical discovery projection usually includes:
- publication, notice, version, procedure, OJ S, and date identifiers;
- form and notice types;
- official language plus multilingual title and buyer fields;
- buyer identifiers and country;
- CPV and NUTS classifications;
- lot identifiers and the distinct tender/request deadline fields;
- value plus currency at its published scope;
- change or related-notice references where available; and
- the automatically returned format links.
TED maintains a downloadable search-field list with aliases and eForms references. Pin that list or the OpenAPI document used by each deployed projection. Contract tests should check both field acceptance and returned type when present; optional source fields can be legitimately absent.
Do not flatten estimated procedure value, estimated lot value, framework maximum, notice result value, contract value, and currency into one unlabeled amount. Likewise, tender deadlines, requests-to-participate deadlines, expression-of-interest deadlines, and other dates are different business terms.
Parse multilingual values, arrays, and format links
The response is heterogeneous by design:
publication-numberandprocedure-identifierare scalar strings;notice-versionis an integer in the Search response;official-languageis an array of three-letter language codes;notice-titleis a language-to-string object;buyer-nameis a language-to-array-of-strings object;- CPV, buyer identifiers, lot IDs, and deadline fields can be arrays, and even CPV arrays can repeat a code; and
linksis a nested format-to-language-to-URL object.
In the August 12 smoke test, a Dutch-source notice returned official-language: ["NLD"], translated title keys across EU languages, buyer-name as a language map of arrays, and an XML URL under links.xml.MUL. Choose source-authored display text from the official language when available. Keep translations as attributed derivatives rather than replacing the source string.
The returned publication date looked like 2026-08-12+02:00, while the expert query used 20260812. Preserve the raw string and parse it with a source-specific contract. Do not promise timestamp precision or coerce it through a generic ISO-datetime parser.
Response envelope
notices[]- Requested projection plus the automatically returned publication-number and links fields
totalNoticeCount- Total notices matching the query, scope, and latest-version policy
iterationNextToken- Opaque continuation token for ITERATION mode
timedOut- Whether the search request timed out
Representative requested-field shapes
| Requested field | Returned shape | Safe reading |
|---|---|---|
publication-number | string | Published notice reference; preserve it separately from UUID-style source identifiers. |
notice-identifier | string | Notice identifier returned by TED; do not replace it with the publication number. |
procedure-identifier | string | Source-provided procedure reference; retain it for lifecycle linking without inventing a join. |
notice-title | language → string | Multilingual object keyed by three-letter language code, not one display-ready title. |
buyer-name | language → string[] | Multilingual lists can contain more than one buyer name. |
classification-cpv | string[] | One notice can expose repeated CPV values; retain the raw array before deduplicating in a derived layer. |
deadline-receipt-tender-date-lot | string[] | Lot-level deadlines are returned as a list, not as a relational lot table. |
links | format → language → URL | Maps XML, PDF, signed PDF, HTML, and direct HTML links by available language. |
Do not zip parallel arrays into invented lot rows. TED has documented that Search is notice-based and that associating repeated field values with the correct lot can be difficult. Retain the raw projection, then use the linked XML and its schema-aware identifiers when lot-level relationships matter.
Search projections do not reconstruct lots
The Publications Office has acknowledged that matching repeated Search values to the correct lot can be difficult because the Search API is notice-based. Parallel arrays do not establish positional relationships. If three lot identifiers, two values, four CPV codes, and three deadlines are returned, zipping them by index invents evidence. (TED reuser workshop Q&A)
Use Search fields for discovery, filtering, and bounded profiling. Follow the XML and parse the eForms or legacy schema when the product needs exact lot, result, tender, winner, or contract relationships.
Choose page-number or iteration mode
PAGE_NUMBERAddressable but stateless
Send page 1 with an explicit limit, then request pages 2, 3, 4… as needed.
Pages can be requested independently and do not have to be read in order.
If an OJ S release changes the index between calls, notices can be missed or duplicated across pages.
Best fit
Use for bounded browsing and queries whose full result stays safely below the retrieval ceiling.
ITERATIONSequential point-in-time traversal
Send the first request without iterationNextToken and retain the token from the response.
Send that opaque token in the next request, preserving the same query, fields, scope, and limit.
Continue until the response contains no notices; pages cannot be selected or skipped.
Best fit
Use for complete extracts. The point-in-time view prevents index changes from shifting records between pages.
- Shared page limit
- 250 notices
- Shared field budget
- 10,000 field cells
- Iteration token expiry
- Next OJ S release + 24 hours
The iteration point in time is held until the documented expiry, giving the collector at least 24 hours to finish. Restart deliberately after an expired or invalid token; do not silently switch to page-number mode.
Page-number mode can move underneath a collector
PAGE_NUMBER is the default. It supports direct access to one-based pages and is useful for interactive views. It is stateless: TED does not freeze the index between calls. If a new OJ S edition arrives while pages are being walked, records can shift and produce gaps or duplicates. It also stops at 15,000 retrievable notices even when totalNoticeCount is larger.
Use page-number mode only when the query is intentionally bounded, the count stays below the ceiling, and the consumer accepts its consistency model. Date partitioning reduces the risk but does not turn a stateless search into a snapshot.
Iteration mode freezes the result set
ITERATION uses an Elasticsearch point in time. The first request omits iterationNextToken; every subsequent request sends the opaque token returned by the previous response. The point in time expires at the next OJ S release plus 24 hours, which TED describes as giving at least 24 hours to finish the walk. Iteration has no overall result ceiling, but each page retains the 250-notice and 10,000-field limits. (official pagination contract)
The stopping rule is important: finish when notices is empty. In a live one-record test on August 12, the first iteration page returned one notice and a non-empty 120-character token. The next page returned zero notices and another non-empty 100-character token. A collector that waits for a missing token does not have a valid completion rule.
Treat expired and invalid tokens as failed partitions. Restart the same bounded query from the beginning, retain both attempts, and reconcile source identities. Do not fall back silently to page-number mode or reuse a token with changed fields, scope, version policy, or query.
Build a restartable Python iteration collector
This collector validates the query, calculates a legal page size, stores exact raw responses, fails timed-out pages, detects a repeated page, records input and output token hashes for diagnostics, and stops only on an empty notice array.
import hashlib
import json
import random
import time
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from pathlib import Path
import requests
ENDPOINT = "https://api.ted.europa.eu/v3/notices/search"
def retry_delay(response, attempt):
value = response.headers.get("Retry-After")
if not value:
return min(60, 2 ** attempt)
try:
return max(0, float(value))
except ValueError:
try:
retry_at = parsedate_to_datetime(value)
except (TypeError, ValueError):
return min(60, 2 ** attempt)
if retry_at.tzinfo is None:
retry_at = retry_at.replace(tzinfo=timezone.utc)
return max(0, (retry_at - datetime.now(timezone.utc)).total_seconds())
def post_with_retry(session, payload, attempts=6):
for attempt in range(attempts):
try:
response = session.post(
ENDPOINT,
json=payload,
timeout=(10, 90),
headers={"Accept": "application/json"},
)
except (requests.Timeout, requests.ConnectionError):
if attempt == attempts - 1:
raise
time.sleep(min(60, 2 ** attempt) + random.random())
continue
if response.status_code not in {429, 500, 502, 503, 504}:
response.raise_for_status()
return response
if attempt == attempts - 1:
response.raise_for_status()
time.sleep(retry_delay(response, attempt) + random.random())
def token_hash(token):
return hashlib.sha256(token.encode()).hexdigest() if token else None
def collect_iteration(query, fields, output_dir):
if not fields or len(fields) != len(set(fields)):
raise ValueError("fields must be non-empty and unique")
# TED budgets the union with its implicit publication-number and links fields.
effective_fields = set(fields) | {"publication-number", "links"}
limit = min(250, 10_000 // len(effective_fields))
if limit < 1:
raise ValueError("field projection cannot fit the page budget")
output = Path(output_dir)
output.mkdir(parents=True, exist_ok=False)
session = requests.Session()
common = {
"query": query,
"fields": fields,
"limit": limit,
"scope": "ALL",
"onlyLatestVersions": False,
}
syntax_response = post_with_retry(session, {
**common,
"page": 1,
"checkQuerySyntax": True,
"paginationMode": "PAGE_NUMBER",
})
(output / "syntax-check.json").write_bytes(syntax_response.content)
token = None
page_number = 0
accepted = 0
seen_page_hashes = set()
seen_publications = set()
first_total = None
while True:
page_number += 1
payload = {
**common,
"checkQuerySyntax": False,
"paginationMode": "ITERATION",
}
if token is not None:
payload["iterationNextToken"] = token
input_token = token
response = post_with_retry(session, payload)
raw = response.content
raw_hash = hashlib.sha256(raw).hexdigest()
(output / f"page-{page_number:05d}.json").write_bytes(raw)
body = response.json()
if body.get("timedOut") is not False:
raise RuntimeError(f"page {page_number} timed out")
notices = body.get("notices")
if not isinstance(notices, list):
raise TypeError("response notices must be an array")
if first_total is None:
first_total = body.get("totalNoticeCount")
output_token = body.get("iterationNextToken")
manifest_line = {
"page": page_number,
"retrieved_at": datetime.now(timezone.utc).isoformat(),
"response_sha256": raw_hash,
"notice_count": len(notices),
"input_token_sha256": token_hash(input_token),
"output_token_sha256": token_hash(output_token),
}
with (output / "pages.jsonl").open("a", encoding="utf-8") as target:
target.write(json.dumps(manifest_line, sort_keys=True) + "\n")
if not notices:
token = output_token
break
identities = []
for notice in notices:
identity = (
notice.get("publication-number"),
notice.get("notice-identifier"),
notice.get("notice-version"),
)
if not identity[0]:
raise RuntimeError("notice lacks publication-number")
identities.append(identity)
if identity[0] in seen_publications:
raise RuntimeError(f"duplicate publication {identity[0]}")
seen_publications.add(identity[0])
page_hash = hashlib.sha256(
json.dumps(identities, separators=(",", ":")).encode()
).hexdigest()
if page_hash in seen_page_hashes:
raise RuntimeError(f"repeated result page {page_number}")
seen_page_hashes.add(page_hash)
accepted += len(notices)
token = output_token
if not token:
raise RuntimeError("non-empty iteration page lacks next token")
manifest = {
"endpoint": ENDPOINT,
"query": query,
"fields": fields,
"scope": "ALL",
"only_latest_versions": False,
"pagination_mode": "ITERATION",
"limit": limit,
"first_total_notice_count": first_total,
"accepted_notices": accepted,
"completion_page": page_number,
"completion_token_present": bool(token),
"completion_token_sha256": token_hash(token),
}
(output / "manifest.json").write_text(
json.dumps(manifest, indent=2, sort_keys=True),
encoding="utf-8",
)
if first_total is not None and accepted != first_total:
raise RuntimeError(f"accepted {accepted}, expected {first_total}")
return manifest
The example intentionally refuses to overwrite an existing output directory. A restart should create a new attempt with a new manifest, not splice a fresh point-in-time walk into an expired one. In production, add a bounded request-rate controller, structured 400-error capture, content-type checks, token-safe logs, and run-level acceptance status.
Preserve notice, version, procedure, and lot identities
The eForms identifiers answer different questions:
- Publication number: assigned by the Publications Office to a published notice, such as
556964-2026. - Notice ID (BT-701): a UUID assigned to the notice; retain it with the version.
- Notice Version (BT-757): the two-digit editorial version for one Notice ID; it starts at
01, increases for later versions, and is final once published. A higher value does not by itself make that publication obsolete. - Procedure ID (BT-04): a UUID linking notices in a procedure where the form supports it; planning notices do not have one.
- OJ S issue: the Gazette issue containing the publication.
- Lot, part, result, tender, contract, and organisation references: technical IDs whose scope must be read from the XML schema.
The Search response currently returns notice-version as an integer, while the eForms source specification defines a two-digit VersionID and the live XML contained 01. Store the Search value as received, but keep the raw XML value and its schema semantics. Do not let integer coercion destroy the source representation. A change notice has its own Notice ID and VersionID and points to the notice version it changes; it is not merely the next VersionID on the parent.
Publication-number formatting can also differ between surfaces. In a live OJ S 153 sample, Search returned 553788-2026 while the XML publication metadata used 00553788-2026. Preserve both raw forms and use a documented canonical comparison key; do not discard zero padding inside the immutable source artifact.
Procedure ID is the main cross-notice link for eForms, but not every historical or form family has one. Legacy TEDXML, previous-format references, planning relationships, and explicit change references require their own fields. A matching title, buyer name, or internal reference is evidence for review—not a safe primary key. (TED identifiers; TED reuser Q&A)
Use an immutable source-version key such as:
(publication_number, notice_identifier, raw_notice_version)
Then model procedure, lot, result, tender, contract, buyer, and supplier tables separately. Keep source relationships and their evidence rather than replacing them with one “tender ID.”
Collect each OJ S release with an explicit cutoff
A defensible daily sync follows the edition rather than an arbitrary server midnight.
- Read the official release calendar and identify the expected OJ S key.
- Wait until TED's stated publication/package window has passed; record the actual retrieval time.
- Query
scope: ALLwith the exactojs-numberor publication date,onlyLatestVersions: false, andITERATION. - Store and accept every Search page as one point-in-time run.
- Retrieve the linked XML for every accepted publication, with explicit success, not-found, throttled, and parse-failed states.
- Download the daily package and inventory its members.
- Reconcile Search publications, XML downloads, and package members by publication number.
- Re-run a short overlap after the next edition and capture later change notices as new source publications.
- Emit current-state and material-change tables only after the source evidence has passed.
Use the OJ S number as a publication-edition key. Do not assume every weekday has an issue; the calendar contains holiday exceptions. Do not call a zero before the expected edition or package window a source outage.
LATEST can be useful for a “what is in the current issue?” display, but an exact OJ S query is more reproducible in a collector manifest. ACTIVE is useful for discovery, not as an incremental cursor. It has a moving, mixed-stage definition and cannot show which source records changed since the prior run.
For a recurring cross-source monitor, keep TED's edition clock separate from SAM.gov posted-date windows and UK update windows. The government contract tracker guide explains why each source needs its own cursor and SLA.
Use notice XML and packages for replay
Every Search result includes official format links. The XML route follows this form:
https://ted.europa.eu/en/notice/{publication-number}/xml
The August 12 sample returned application/xml, a filename tied to OJ S 154, eForms UBL, CustomizationID eforms-sdk-1.13, source Notice ID, VersionID 01, the procedure folder UUID, change references, organisation blocks, and lot-level elements. That is much richer relational evidence than a flat field projection.
Since November 14, 2022, TED has carried both eForms and TED-schema notices. Historical collectors must identify the XML family and parser contract from namespaces and schema metadata rather than force every document through one eForms mapping. Preserve unparsed XML when a new schema or notice type arrives. (TED developers' corner)
Daily packages
The working daily route is:
https://ted.europa.eu/packages/daily/{yyyynnnnn}
Here {yyyynnnnn} combines the four-digit year with the five-digit OJ S issue number. A live HEAD request for OJ S 154 on August 12 confirmed:
https://ted.europa.eu/packages/daily/202600154
HTTP 200 · application/gzip · 16,976,765 bytes
Do not hardcode the archive size; retain it with response headers and the archive hash. Inventory every member before parsing. TED says eForms filenames use eight digits plus year, while TED-schema members use six digits plus year. (official bulk-download routes)
A completed-edition reconciliation on OJ S 153/2026 provides a stronger control than a route-only smoke test. With onlyLatestVersions: false, Search reported 3,176 notices and the daily package contained exactly 3,176 XML members. The same Search query with latest-only enabled returned 3,152, showing that the version switch changed the issue population by 24 publications. The package also contained three eForms SDK fingerprints: 341 members on 1.12, 1,980 on 1.13, and 855 on 1.14. One OJ S issue is therefore not guaranteed to be one SDK version.
Monthly packages
Monthly packages use:
https://ted.europa.eu/packages/monthly/{yyyy-n}
TED publishes a monthly package on the fifth working day of the next month by 09:30 at the latest. A missing package for an unfinished month is therefore expected, not a failed archive. Use monthly packages for backfill and reconciliation, daily packages for edition-level operations, and direct notice XML for targeted repair.
Normalize only after each source artifact is durable and tied to a reproducible request or package manifest.
Search projection
POST /v3/notices/searchRetain
Expert query, fields, scope, latest-version policy, mode, tokens, retrieval time, response body, status, and hash.
Acceptance evidence
Syntax was checked; pages or tokens form one complete run; notice count, unique identifiers, timedOut, and page hashes reconcile.
Notice XML
links.xml[language]Retain
Raw XML bytes, source URL, language, publication number, retrieval result, hash, and eForms or legacy schema evidence.
Acceptance evidence
Every attempted notice has a classified retrieval state; parsed identifiers, lots, dates, and values remain traceable to XML evidence.
Package baseline
Daily and monthly XML packagesRetain
Archive bytes, package URL, OJ S or month, member inventory, checksums, parse counts, and schema distribution.
Acceptance evidence
Search and package populations reconcile by publication number and source date, with late, missing, duplicate, and parse-failed notices disclosed.
Documented fair-use limits
- 700 HTTP requests in last minute
- 600 single-IP notice visualizations/downloads in <6 min
- 3 concurrent package downloads
The lanes answer different questions: Search proves what matched a query at a cutoff; notice XML preserves source structure; packages test historical coverage through a separate retrieval path. A row-count match in one lane cannot replace the other two.
Respect all three published fair-use limits. Add pacing below the ceiling, honor service responses, bound retries, and keep package concurrency at or below three. The official policy recommends the public API and direct download routes rather than automating CMS pages.
Validate every TED extraction run
A run is acceptable only when the evidence supports its declared population.
Transport and request contract
- endpoint, method, status, content type, elapsed time, retrieval time, and raw bytes are recorded;
- query, field list, scope, version policy, pagination mode, limit, and contract fingerprint are immutable in the manifest;
- the live effective-field budget
size(set(fields) ∪ {publication-number, links}) × limit ≤ 10,000,1 ≤ limit ≤ 250, and all requested fields are unique; and - structured 400 responses and retryable service responses are classified, not converted to empty data.
Pagination and timeout
- syntax validation passed in its own call;
- every non-empty iteration page supplied the next token;
- the walk ended on an empty
noticesarray; timedOutwas exactlyfalsefor every accepted page;- no page identity hash repeated; and
- accepted notice count reconciles to the first point-in-time
totalNoticeCount.
Identity and versions
- every accepted row has a publication number;
- publication numbers are unique inside the accepted run unless a documented source exception proves otherwise;
- Notice ID, version, Procedure ID, OJ S, and form type are retained separately;
- latest-version filtering matches the declared product policy; and
- change, previous-notice, and lifecycle references remain source relationships.
Field and language quality
- returned types are checked when fields are present;
- unknown response keys and type changes produce schema-drift evidence;
- official language, available translations, and selected display language are measured separately;
- array fields remain arrays until a schema-aware relation is available; and
- missing optional values remain missing rather than being filled from an unrelated translation or lot.
XML and package reconciliation
- every notice has an XML retrieval state and raw hash;
- XML namespaces, schema family, SDK/customization version, publication number, Notice ID, and VersionID are profiled;
- every package has a hash, member inventory, parse totals, schema distribution, and rejected-member log; and
- Search, direct XML, and package populations reconcile by publication number at explicit cutoffs, with every discrepancy explained.
Publication and authenticity boundary
TED's legal notice says OJ S procurement notices may generally be reused for commercial or non-commercial purposes unless otherwise stated. It does not label every notice CC BY 4.0; that license applies to SIMAP editorial content, while metadata is CC0. Third-party works, identifiable-person content, logos, and other rights can require separate treatment. TED also says electronically signed OJ S notices are the authentic publications. Keep source links and signed-PDF availability when legal authenticity matters. (TED legal notice)
The August 2026 procurement data-quality benchmark shows why transport success, stage-conditioned completeness, and mapping quality must be measured separately. An HTTP 200 does not prove the collector captured the correct source universe.
Choose direct TED access or normalized delivery
There are three sensible operating models.
Build directly on TED
Choose the Search API plus XML packages when you need source-native expert queries, exact field selection, every notice version, specialized XML relationships, or a TED-only product. Budget for schema monitoring, package storage, two XML families, translation policy, retry operations, and reconciliation.
Use the normalized public edition
Choose the government tenders and RFP dataset when you need a prepared current view, observed history, material changes, governance artifacts, and comparable SAM.gov and UK fields. The public edition has explicit source and mapping limits; it is not a replacement for the full TED archive or every source field.
The August demand snapshot is a worked example: TED supplied 8,278 of the seven-day selected records and 99.29% of its CPV panel, while the report kept source mix and taxonomy denominators visible.
Request scoped managed delivery
Use a scoped feed when the requirement adds a precise CPV/NUTS portfolio, longer history, qualification rules, linked documents, additional national sources, enrichment, change monitoring, or delivery into a database, cloud bucket, CRM, or API-shaped internal contract. That is a managed delivery option; no public WebTruffle API is being promised.
TED Search API production checklist
- [ ] Confirm Search API v3 is the published-notice route the product needs.
- [ ] Store the exact expert query and validate it before execution.
- [ ] Set
scope,onlyLatestVersions, andpaginationModeexplicitly. - [ ] Pin the field list and OpenAPI or field-catalogue fingerprint.
- [ ] Keep
1 ≤ limit ≤ 250and budget the union of requested fields with implicitpublication-numberandlinksat no more than 10,000 cells per page. - [ ] Use
ITERATIONfor complete extraction and finish on an empty notice page. - [ ] Fail
timedOut: true, invalid tokens, missing tokens on non-empty pages, and unexplained count differences. - [ ] Preserve every raw request, response, retrieval time, and SHA-256 hash.
- [ ] Keep publication number, Notice ID, source version, Procedure ID, OJ S, and XML technical IDs separate.
- [ ] Select source-authored text through
official-language; retain translations with provenance. - [ ] Never zip parallel Search arrays into invented lot relationships.
- [ ] Retrieve and retain notice XML for lot- or lifecycle-sensitive products.
- [ ] Reconcile exact publication windows to daily or monthly packages.
- [ ] Support eForms and legacy TED-schema XML without destructive fallback parsing.
- [ ] Follow the release calendar and classify expected quiet or not-yet-published windows.
- [ ] Pace requests below all documented fair-use limits.
- [ ] Publish transport, pagination, identity, XML, package, schema, and field-quality evidence.
- [ ] Retain official links and state the reuse and authenticity boundaries.
Frequently asked questions
Does the TED Search API require an API key?
No. TED says published-notice Search API access is anonymous. API keys apply to services that manipulate unpublished notices, such as authenticated publication workflows. Call the read-only Search endpoint from a responsibly paced backend or research client without adding a key.
What is the current TED Search API v3 endpoint?
Send an HTTP POST with a JSON request body to https://api.ted.europa.eu/v3/notices/search. The body contains an expert query, requested fields, scope, version policy, pagination mode, and the applicable page or iteration token.
Should I use PAGE_NUMBER or ITERATION?
Use PAGE_NUMBER for a bounded interactive query where random page access matters and the result stays below 15,000. Use ITERATION for complete extraction: it provides a consistent point-in-time walk and has no total-result ceiling, while retaining the 250-notice and 10,000-field-cell page limits.
When is an ITERATION walk complete?
Stop when the response's notices array is empty. Do not wait for iterationNextToken to become empty. A live one-record test returned another non-empty token on its empty completion page, consistent with TED's documented notice-based stopping rule.
What does onlyLatestVersions do?
It applies TED's latest-version search filter when set to true: older published versions are filtered and a later change notice can become the displayed latest result. It is not equivalent to notice-version == 1, does not merge every notice in a procedure, and does not preserve superseded states. Set it explicitly and collect all versions when auditability or change tracking matters.
Does scope ACTIVE return only open tenders?
No. TED defines ACTIVE to include recent planning notices, competition notices whose deadlines have not passed, and recent result notices. A bid-ready view still needs form and notice type, lot-level deadline, cancellation or change state, documents, and the official source notice.
Does scope ALL contain TED's entire history?
No. The website's ALL search covers the rolling last ten years. Use daily and monthly XML packages, release-calendar evidence, and other official preservation routes for durable or older historical replay.
Is one Search API result one tender?
Not necessarily. It is one published notice projection. A procedure can have several notice types and versions, and one result notice can contain several lots, tenders, winners, or contracts. Preserve source identifiers and model those grains separately.
How do I associate values and deadlines with the correct TED lot?
Do not join repeated Search arrays by position. Follow the returned XML link and parse schema-aware lot identifiers and nested elements. The Publications Office has acknowledged that the notice-based Search projection can make repeated lot values difficult to associate correctly.
How should I backfill TED notices?
Use monthly XML packages for broad historical replay, daily packages for edition-level reconciliation, and direct notice XML for targeted repair. Record package URLs, hashes, member inventories, schema families, parse failures, and the release cutoff. Search remains useful for targeted discovery inside its searchable window.
Can TED procurement notices be reused commercially?
TED's legal notice says OJ S procurement notices can generally be reused commercially or non-commercially unless otherwise noted. That is distinct from the CC BY 4.0 license for SIMAP editorial content and CC0 dedication for metadata. Check third-party content and other listed rights, retain provenance, and use the authentic signed publication when legal reliance matters.