Skip to article

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.

Published August 12, 202624 min readBy DanielReviewed by Alexandra

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.

The TED Search API v3 request contract

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.

  1. query

    Expert-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.

  2. fields

    Explicit 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.

  3. scope

    LATEST · 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.

  4. checkQuerySyntax

    false by default

    Purpose

    Set true to validate the expert query without executing the search.

    Guardrail

    Treat validation and collection as separate recorded calls.

  5. onlyLatestVersions

    false 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.

  6. paginationMode

    PAGE_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.

  7. page / iterationNextToken

    page 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:

  1. Confirm that the read-only Search API is the correct TED service; do not mix it with notice-submission workflows.
  2. Define the notice population with an exact expert query, scope, version policy, and retrieval cutoff.
  3. Validate the query once with checkQuerySyntax: true, then execute it in a separate call.
  4. 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.
  5. Use PAGE_NUMBER only for bounded browsing; use ITERATION for complete collection.
  6. Stop an iteration when notices is empty, not when the response token disappears.
  7. Reject timedOut: true even when the endpoint returned HTTP 200 and some notices.
  8. Save every raw response, request body, retrieval time, response hash, and OpenAPI fingerprint before normalization.
  9. Preserve publication number, Notice ID, VersionID, Procedure ID, OJ S issue, and XML identifiers separately.
  10. Follow each result's XML link when lot relationships, source-authored language, change evidence, or schema fidelity matters.
  11. Reconcile publication-date or OJ S extracts to daily XML packages; use monthly packages for durable historical replay.
  12. 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; and
  • ALL: 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.

The response is heterogeneous by design:

  • publication-number and procedure-identifier are scalar strings;
  • notice-version is an integer in the Search response;
  • official-language is an array of three-letter language codes;
  • notice-title is a language-to-string object;
  • buyer-name is 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
  • links is 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 shape and normalization boundaryA search result is a notice projection, not a lot-relational record.

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

Representative TED Search API response fields, their returned shapes, and safe normalization readings
Requested fieldReturned shapeSafe reading
publication-numberstringPublished notice reference; preserve it separately from UUID-style source identifiers.
notice-identifierstringNotice identifier returned by TED; do not replace it with the publication number.
procedure-identifierstringSource-provided procedure reference; retain it for lifecycle linking without inventing a join.
notice-titlelanguage → stringMultilingual object keyed by three-letter language code, not one display-ready title.
buyer-namelanguage → string[]Multilingual lists can contain more than one buyer name.
classification-cpvstring[]One notice can expose repeated CPV values; retain the raw array before deduplicating in a derived layer.
deadline-receipt-tender-date-lotstring[]Lot-level deadlines are returned as a list, not as a relational lot table.
linksformat → language → URLMaps 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

Two pagination modes, two consistency contractsChoose the traversal before the first request.
PAGE_NUMBER

Addressable but stateless

15,000 notices
  1. Send page 1 with an explicit limit, then request pages 2, 3, 4… as needed.

  2. Pages can be requested independently and do not have to be read in order.

  3. 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.

ITERATION

Sequential point-in-time traversal

No total retrieval cap
  1. Send the first request without iterationNextToken and retain the token from the response.

  2. Send that opaque token in the next request, preserving the same query, fields, scope, and limit.

  3. 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.

  1. Read the official release calendar and identify the expected OJ S key.
  2. Wait until TED's stated publication/package window has passed; record the actual retrieval time.
  3. Query scope: ALL with the exact ojs-number or publication date, onlyLatestVersions: false, and ITERATION.
  4. Store and accept every Search page as one point-in-time run.
  5. Retrieve the linked XML for every accepted publication, with explicit success, not-found, throttled, and parse-failed states.
  6. Download the daily package and inventory its members.
  7. Reconcile Search publications, XML downloads, and package members by publication number.
  8. Re-run a short overlap after the next edition and capture later change notices as new source publications.
  9. 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.

Three evidence lanes, one accepted datasetUse Search for discovery, XML for structure, and packages for replay.

Normalize only after each source artifact is durable and tied to a reproducible request or package manifest.

  1. Search projection

    POST /v3/notices/search

    Retain

    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.

  2. 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.

  3. Package baseline

    Daily and monthly XML packages

    Retain

    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 notices array;
  • timedOut was exactly false for 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, and paginationMode explicitly.
  • [ ] Pin the field list and OpenAPI or field-catalogue fingerprint.
  • [ ] Keep 1 ≤ limit ≤ 250 and budget the union of requested fields with implicit publication-number and links at no more than 10,000 cells per page.
  • [ ] Use ITERATION for 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.