Tender pipelines · Category filters
Search CPV codes and test what your category filter retrieves.
Search a versioned CPV lookup, expand the official hierarchy and compare exact versus category filters on 217 public notice versions. Download the Python helper and reviewed cases.
A CPV code search should help you define a tender category, then test what that category retrieves. Looking up a label is the first step. You also need to decide whether matching includes descendants, additional classifications and individual lots, and whether the result contains opportunities or already-awarded work.
In our pinned public sample, exact matching on 72200000 finds 7 opportunity-stage notice versions. Expanding that code through the published hierarchy finds 59. Excluding one project-management branch reduces the result to 54. More matches do not automatically mean more relevant leads: the expanded category includes a library-refurbishment project-management notice.
This guide provides a versioned official lookup projection, a Python helper, reproducible results and eight documented case reviews. You can run it locally without an account or API key.
Search a versioned CPV lookup
The downloadable lookup contains 9,454 codes, English labels and parent relationships projected from the Publications Office's eForms SDK 1.15.0. The vocabulary version is CPV 2008; the SDK version identifies the particular distribution used by this example. The official CPV reference describes the vocabulary and its use in eForms.
The file keeps three columns:
| Column | Purpose |
| --- | --- |
| code | Eight-digit classification code stored as text |
| label_en | English label for lookup and review |
| parent_code | Published parent relationship; empty for a root |
Keep codes as strings so leading zeroes survive CSV imports. The helper accepts the eight-digit form used in this lookup. It rejects abbreviated prefixes, unknown codes and display forms with a hyphen suffix; it does not claim to validate display check digits.
After extracting the download, search the labels with Python 3.10 or later:
python3 -B filter_cpv.py --search "software implementation"
This returns 72263000, which belongs below the broader 72200000 software programming and consultancy category. The search is an English substring search, not semantic matching or translation. Try several service descriptions and inspect the parents before choosing a root.
The lookup receipt records the source URL, retrieval date, upstream SHA-256, projected-file SHA-256 and transformation. The attribution and CC BY 4.0 license are included. Parent relationships come directly from the versioned SDK code list; the helper does not infer them by removing zeroes.
Preserve main, additional and lot scope
There are two different distinctions to keep clear. The CPV main vocabulary is the classification vocabulary used here. Within a notice, a classification can have a main or additional role. An additional classification may still use a code from this same main vocabulary; that is different from CPV supplementary vocabulary. The eForms CPV reference links the main and additional classification business terms. Official code-list usage.
Our normalized sample contains a flattened cpv_codes array. It does not preserve main/additional roles or the lot associated with each code. The first array element is therefore not reliable evidence of a main classification.
The downloadable helper implements any-code matching. It can answer whether a notice version contains an eligible code. It cannot answer which lot matched or whether that code was the main classification.
For a production collector, retain these fields together:
| Field | Why it matters | | --- | --- | | Notice ID and version | Keeps separate publications and corrections traceable | | Procedure or lot ID | Preserves the scope to which the classification applies | | Classification scheme | Prevents codes from different vocabularies being mixed | | Main/additional role | Makes main-only filtering possible | | Code | Supports exact matching and explicit hierarchy expansion |
A notice-level match may justify further review. It does not establish that every lot is relevant to your service.
Expand the published parent relationships
An exact filter for 72200000 includes rows explicitly carrying that code. It does not inherently include a row carrying only 72263000. Expansion is a separate operation.
The helper follows parent_code edges and returns the selected root plus all descendants. It validates the graph for missing parents and cycles before searching. For the pinned lookup, 72200000 expands to 185 eligible codes. Overlapping roots are deduplicated.
lookup = load_lookup()
included = expand(lookup, ["72200000"])
excluded = expand(lookup, ["72224000"])
eligible = included - excluded
This leaves 182 codes. The exclusion removes 72224000 and its two descendants from the eligible set. A notice with an excluded code can still match through another eligible code. That is a code-level exclusion, not a blanket rejection of the whole notice.
Do not treat a shorter numeric prefix as an interchangeable API query. The lookup is an explicit hierarchy, and an API may interpret an exact code, wildcard or category parameter differently. Exporting the actual eligible code set makes your intended behavior inspectable.
Compare exact and expanded filters on real notices
The input is the same pinned September 1, 2026 public tender sample used in our procurement feed evaluation pack. It contains 217 notice versions and 664 CPV references: 197 TED, 13 Contracts Finder and 7 Find a Tender rows.
The sample was already selected because at least one code starts with 72. All notice stages were retained. That makes it useful for this comparison, but it cannot establish market-wide recall or find work classified exclusively outside that selection.
Run the chosen definition:
python3 -B filter_cpv.py --root 72200000 --exclude 72224000 --output reproduced
python3 -B -m unittest test_filter_cpv.py
| Filter | Eligible codes | All stages | Opportunity stage |
| --- | ---: | ---: | ---: |
| Exact 72200000 | 1 | 12 | 7 |
| Root and descendants | 185 | 133 | 59 |
| Expanded, excluding project-management branch | 182 | 124 | 54 |
These are notice-version counts, using (source, source_id, source_release_id) as the row key. Several codes on a single row do not count as several matches. Separate versions or related notices can still refer to the same procurement.
The opportunity-stage result is not a live bidding list. The example does not test deadlines, bid eligibility or geographic suitability. Adding a stage filter prevents awards and contracts from appearing in this candidate list; it does not prove the remaining notices are open today.
Review omissions and unwanted matches
Our illustrative task is to find candidates for software-delivery work. The review file records assessments of the archived titles, codes and stages. Live source pages could not be retrieved during preparation, so these are field-level reviews, not full-document judgments or an independently labelled relevance benchmark.
Three cases explain the main tradeoffs:
| Case in the pinned sample | Filter behavior | What to review |
| --- | --- | --- |
| TED 599864-2026: ERP/CRM implementation and integration | 72263000 is missed by exact matching and recovered by expansion | A plausible software-delivery candidate; detailed scope and eligibility still need review |
| TED 600938-2026: project management for a library refurbishment | Expansion matches 72224000; the chosen exclusion removes it | The title suggests construction-related work, showing why code membership alone is insufficient |
| Contracts Finder “Purchase of IT Equipment for Summer Grads, New Starters and Storeroom Replenish” | 72222300 belongs to the expanded set | The title describes equipment, and the stage is award; the opportunity-stage gate removes it |
The review file includes all five opportunity rows removed by the exclusion, plus the implementation, equipment and “CaseFlow” cases. CaseFlow carries only the broad 72000000 code. A descendant search rooted at 72200000 cannot include that ancestor. Its short title alone does not establish whether it is a relevant omission; it needs document review and is an award in this sample.
The exclusion has a cost. Software project-management work classified only in the removed branch would also disappear. If that service belongs in your offering, keep the branch and review the results. A broader 72000000 search can recover coarsely classified work, while software packages in division 48 deserve a separate test on a broader input sample.
Do not label the 52 additional opportunity matches as 52 confirmed relevant tenders, or calculate a precision score from eight selected cases. Use them to improve a review process, then label a representative sample against a written buyer requirement.
Export a filter you can reproduce
The command writes five files to the output directory. matches.csv preserves source fields and adds the matching codes. expanded-codes.csv lists eligible codes and labels. comparison.csv and report.json record the counts. filter.json captures the rules and input versions.
The filter definition includes roots, excluded branches, descendant behavior, classification scope, stage, notice-version grain, input hashes and the full eligible code set. It also explicitly records that deadline checks were not performed.
Nine automated checks cover lookup integrity, leading-zero codes, hierarchy expansion, overlapping roots, invalid roots, measured comparisons, duplicate-code handling, exclusion behavior and stage filtering. The bundled expected outputs can be reproduced locally. This tests the implementation and pinned inputs; relevance still requires an appropriate review set.
When changing a root or exclusion, compare the added and removed notice IDs as well as the headline count. Retain the previous definition so a user can understand why an alert appeared yesterday and disappeared today.
Translate the definition into your tender pipeline
The exported JSON is a local contract, not TED API syntax or a hosted CPV codes API. An integration should map it to the target service's documented classification fields and operators, then check a few known inclusions and exclusions through that service.
Confirm whether its search field covers main classifications, additional classifications, lot-level codes or a combined index. Also verify whether a parent code expands automatically. If a query requires batching a large code set, deduplicate the returned notice-version keys across batches.
For TED collection and source-specific query behavior, continue with the TED Search API guide. For the wider analytical model, see government contract analysis. This helper does not supply a universal CPV-to-NAICS mapping: a crosswalk would need its own scope, evidence and ambiguity policy.
Request a feed against the tested definition
Start with the free lookup and comparison. For recurring delivery, specify the category roots, exclusions, classification roles, countries, notice stages, date semantics and correction handling. Include examples that must match and examples that must be excluded.
That gives a feed pilot a testable requirement. A request for “software tenders” leaves too much interpretation in the supplier's hands; a versioned filter and a reviewed acceptance set make the intended result much clearer.
Frequently asked questions
Does searching a CPV parent code include its children?
That depends on the search system. The helper explicitly includes the root and every descendant in the pinned parent graph. Verify the equivalent behavior in your API instead of assuming an exact-code parameter expands categories.
Can I filter only the main CPV classification?
Only if the input preserves classification role. This sample contains flattened code arrays, so the helper matches any listed code. Array position is not sufficient to reconstruct main and additional roles.
Are all 54 results relevant, open software tenders?
No. They are opportunity-stage notice versions matching the chosen code rule in one historical sample. Detailed relevance, deadlines, eligibility and distinct-procurement counts require further checks.