Tender Data API – EU European Tenders, TED Europa, CPV Codes avatar

Tender Data API – EU European Tenders, TED Europa, CPV Codes

Pricing

$3.00 / 1,000 result items

Go to Apify Store
Tender Data API – EU European Tenders, TED Europa, CPV Codes

Tender Data API – EU European Tenders, TED Europa, CPV Codes

Search the EU's official TED public-procurement journal by country, CPV code, date or open submission deadline. Returns clean, deduplicated JSON in your choice of 24 EU languages: buyer, value, deadlines, CPV codes and notice link. Includes tender notices, contract awards and corrigenda.

Pricing

$3.00 / 1,000 result items

Rating

0.0

(0)

Developer

Fetch Smith

Fetch Smith

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

4 hours ago

Last modified

Share

EU & European Tenders (TED) — Public Procurement & Government Tenders Europe Scraper

Search TED (Tenders Electronic Daily), the EU's official public-procurement journal, and get clean, structured JSON back — no EU login, no scraping tricks, just the official free api.ted.europa.eu Search API normalized into a usable shape.

Use cases

  • Bids and tenders monitoring — track new contract notices in your CPV codes and countries so a sales team hears about a tender the day it's published, not weeks later browsing ted.europa.eu by hand.
  • Government-spending research & journalism — pull every notice for a buyer, country or sector over a date range with contract value, buyer and deadline already flattened into one row.
  • Market-sizing — aggregate totalValue/totalValueCurrency across CPV codes or countries to estimate how much a government is spending in a given category.
  • Lead generation — buyerEmail/buyerPhone/buyerUrl give a direct contact point for the procurement office behind each notice, most of them real role mailboxes (einkauf@…, vergabestelle@…).
  • Bid pipeline / deadline tracking — deadlineDate (earliest submission deadline across all lots), daysUntilDeadline and deadlineReceiptRequestDate let you build a reminder feed instead of re-checking the site. Set onlyOpenDeadlines to get only tenders you can still bid on, and minDaysUntilDeadline: 14 to skip the ones closing too soon to prepare a bid for.
  • Corrigendum tracking — a filter for noticeTypes plus procedureIdentifier lets you group a notice with any corrigenda published against the same procedure, and changeReasonDescription tells you in plain text what changed (deadline, specs, opening date) without opening the PDF.

Input

FieldTypeDescription
countriesarrayISO 3166-1 alpha-3 buyer country codes (e.g. DEU, FRA). Empty = all.
cpvCodesarray8-digit CPV codes to filter by (e.g. 72000000 for IT services). Empty = all. TED matches the entire CPV subtree under each code you supply, at every level of the hierarchy — there is no exact-code match. 72000000 returns notices tagged only 72313000 or 72220000, and even a narrow code like 72267000 returns its children 72267100/72267200. See the FAQ below for the measurement and the workaround.
noticeTypesarrayTED notice-type codes, picked from a dropdown of all 22 codes TED accepts (e.g. cn-standard contract notice, can-standard award notice, pin-only/pin-buyer/pin-rtl prior information, veat, corr, can-modif, qu-sy, pmc). Empty = all. There is no pin-standard code — see the FAQ.
procedureTypearrayTED procedure-type codes, picked from a dropdown of all 17 codes TED accepts: the 8 modern eForms codes (open, restricted, neg-w-call, neg-wo-call, comp-dial, oth-single, oth-mult, innovation) plus 9 legacy pre-2023 single-character codes. Empty = all. Server-side filter, same OR-group as noticeTypes.
publishedWithinDaysintegerOnly notices published in the last N days. Default 7. Ignored if publicationDateFrom/publicationDateTo is set.
publicationDateFrom / publicationDateTostringAbsolute date window, YYYYMMDD or YYYY-MM-DD. Either or both — overrides publishedWithinDays.
keywordsstringFree-text search using TED's FT~ operator — matches the whole notice text TED indexes, not just title/description/buyer name (see FAQ).
expertQuerystringRaw TED expert-query string — overrides all the filters above entirely.
maxResultsintegerStop after this many notices. Default 100.
minValue / maxValueintegerOnly keep notices whose totalValue falls in this range. Compares the raw number regardless of currency (TED reports EUR, CZK, RON, SEK, etc. per notice — check totalValueCurrency). Roughly half of all notices carry no value at all; those are dropped whenever either is set.
onlyOpenDeadlinesbooleanKeep only notices whose submission deadline is today or later — the tenders you can still bid on. Notices with no published deadline (award/result notices, most prior-information notices) are dropped while this is on, since an absent deadline can't be shown to be open.
minDaysUntilDeadlineintegerKeep only notices with at least this many whole days left before the deadline (daysUntilDeadline in the output), counted in Brussels local time (the zone every TED deadline is stamped in). Implies onlyOpenDeadlines.
outputLanguagestringPreferred language for title/description/buyerName/buyerCity/noticeUrl (24 EU languages, e.g. deu, fra, spa). Default eng. Falls back to English, then to whatever TED provided, if a notice has no translation into your chosen language.
flattenbooleanDefault false (JSON arrays). Set true to join cpvCodes, contractNature, placeOfPerformanceCountry and placeOfPerformanceCity into a single comma-separated string per field, instead of a JSON array — cleaner for CSV/Excel export (Apify's default array export otherwise splits each into numbered field/0, field/1 columns that shift between rows with different array lengths).
watchLabelstringOptional. Name a saved query and get only what is new since your last run — see below.
webhookUrlstringOptional. POST a small JSON completion summary (notices pushed, pages scanned, TED's reported total, dataset ID, watch new count) here when the run finishes — see FAQ.

Example: German construction/engineering notices from the last 2 weeks

{
"countries": ["DEU"],
"cpvCodes": ["71000000"],
"publishedWithinDays": 14,
"maxResults": 100
}

Multiple values within one field (e.g. two country codes) are OR'd together; different fields (country AND CPV code) are AND'd. Leave every filter empty to pull all recent notices across the whole EU/EEA.

Only what's new since last run (watchLabel)

A bid-monitoring job is a subscription, not a search: you want the notices published since you last looked, not the same hundreds of rows re-delivered (and re-charged) every morning.

Set watchLabel to a name for the query — de-it-services, say — and this Actor keeps track of which notices it has already given you under that name:

  • The first run on a new label is a free baseline. It records what already matches, returns zero results and charges nothing. It walks notice ids only, not full notices.
  • Every run after that returns only the new notices. Already-delivered rows are dropped before they are built or billed, so you never pay for the same notice twice.
  • A notice counts as delivered only once it has actually been charged. Anything cut off by maxResults or a charge limit stays "new" for the next run rather than vanishing.
  • Changing a filter starts a fresh baseline — a different filter is a different question, so you don't get a dump of everything the old, narrower query happened to exclude. Setting expertQuery also starts its own baseline, keyed on the raw query string.
  • The rolling publishedWithinDays window is deliberately not part of that identity. It moves every day; if it counted, a daily schedule would re-seed forever and never deliver anything. publicationDateFrom/publicationDateTo, when you set them explicitly, do count.

The baseline lives in a key-value store named fetchsmith-ted-watch on your own account, so it survives between runs and you can inspect or reset it yourself. Point an Apify schedule at the Actor and you have a TED procurement alert.

Baseline size cap. A baseline holds up to 60,000 notice ids in one saved record. If a label's baseline grows past that, the oldest-first-seen ids are dropped — and a dropped id is no longer recognised, so that notice comes back as "new" on a later run and is charged again. The run that drops them says so explicitly: a warning in the log and baselineTruncated / baselineTruncatedTotal (this run / the whole life of the label) in the saved record and on the webhookUrl payload. If you see it, narrow the query (a country, a CPV code, a shorter publication-date window) or split it across several labels so each baseline stays under the cap. A baseline run also stops walking at 20,000 notice ids, and already warns separately when it hits that.

Why there's no snapshot-diff watchChanges here — and what to use instead

Some of our other Actors (grants.gov, openFDA recalls, USAspending, ClinicalTrials.gov, NIH RePORTER) offer a watchChanges input that re-delivers a record when a field on the same id mutates. TED doesn't work that way: we checked live before building anything, and TED never edits a published notice. A correction (deadline moved, specs revised, opening date pushed back) is published as a brand-new notice with its own publicationNumber — the original notice's own record never changes, so there is nothing for a snapshot diff to catch. (Confirmed on real examples: publication 493171-2026 was corrected twice, by 566391-2026 and 625075-2026, and 493171-2026 itself never mutated — three separate publication numbers, one shared procedureIdentifier.)

watchLabel already surfaces a corrigendum correctly, since it's a genuinely new publicationNumber your baseline hasn't seen. What's new in this version is changeReasonDescription — on a corrigendum notice, TED's own field explaining exactly what changed, e.g. "Deadline for receipt of tenders: INSTEAD OF 24/08/2026 PLEASE READ 10/09/2026" — and procedureIdentifier, a stable UUID shared by a notice and every corrigendum against it, so you can group them into one procurement thread instead of treating each as unrelated. Zero extra API calls: both fields ride along on the same page fetch every other field already uses.

Output

One row per notice:

FieldDescription
publicationNumberTED's own ID for the notice, e.g. 596425-2026.
noticeType, noticeSubtype, procedureTypeTED's notice-type code, subtype code, and procedure type (e.g. open, restricted).
title, titleLanguageNotice title in your chosen outputLanguage (or its fallback), and which language it actually came back in.
buyerName, buyerCountry, buyerCityThe contracting authority and its location.
buyerEmail, buyerPhone, buyerUrlThe buyer's own published contact details, when TED has them.
placeOfPerformanceCountry, placeOfPerformanceCityWhere the contract is performed (arrays, deduplicated — TED repeats these per lot).
contractNature, cpvCodese.g. ["services"], ["71000000"] (arrays, deduplicated).
descriptionFree-text lot description, in outputLanguage.
totalValue, totalValueCurrencyContract value, when the notice type carries one — see the FAQ.
deadlineDate, daysUntilDeadline, deadlineTypeEarliest submission deadline across all lots, how many whole days are left before it (negative = already closed, null = no deadline published), and which TED field the date came from: tender (the tender-receipt deadline — 48/50 live call-for-competition notices in a fresh measurement, see the deadline-fields guide), generic (1/50), or expressions (the expressions-of-interest deadline on two-stage procedures, 0/50). Result/award notices (can-standard) carry none of the three fields — 0/50 in the same measurement — which is why onlyOpenDeadlines treats "no deadline published" as closed.
deadlineReceiptRequestDateThe separate tender-documents/information-request deadline, where TED publishes one.
publicationDateWhen TED published the notice.
noticeUrlLink to the notice's PDF (or XML) on ted.europa.eu, in your chosen language when available. Kept for backward compatibility — prefer the three fields below for a specific format.
pdfUrl, htmlUrl, xmlUrlThe same notice as a downloadable PDF, a browsable HTML page, and machine-readable XML, each in your chosen language when available. null for a format TED doesn't publish for that notice.
procedureIdentifierA stable UUID shared by a notice and every corrigendum published against it — group them into one procurement thread.
changeReasonDescriptionTED's own plain-text explanation of what changed. Only present on corrigendum notices; null on an original notice.

Sample row (real output, German construction-engineering notice)

{
"publicationNumber": "596425-2026",
"noticeType": "cn-standard",
"procedureType": "restricted",
"title": "Germany – Architectural, construction, engineering and inspection services – BLB NRW Köln / Universität Bonn / Neubau Molekulare Biologie (ImBIG) / Bauphysik",
"titleLanguage": "eng",
"buyerName": "Bau- und Liegenschaftsbetrieb NRW Köln",
"buyerCountry": "DEU",
"buyerCity": "Köln",
"noticeSubtype": "16",
"buyerEmail": "BLBVergabe@blb.nrw.de",
"buyerPhone": "+49 0",
"buyerUrl": "http://www.blb.nrw.de",
"placeOfPerformanceCountry": ["DEU"],
"placeOfPerformanceCity": ["Bonn"],
"contractNature": ["services"],
"cpvCodes": ["71000000"],
"description": "Leistungen der Fachplanung Bauphysik (Wäreschutz, Raumakustik, Bauakustik)",
"totalValue": null,
"totalValueCurrency": null,
"deadlineDate": "2026-09-16",
"deadlineType": "tender",
"daysUntilDeadline": 12,
"deadlineReceiptRequestDate": "2026-09-29",
"publicationDate": "2026-08-31",
"noticeUrl": "https://ted.europa.eu/en/notice/596425-2026/pdf",
"pdfUrl": "https://ted.europa.eu/en/notice/596425-2026/pdf",
"htmlUrl": "https://ted.europa.eu/en/notice/-/detail/596425-2026",
"xmlUrl": "https://ted.europa.eu/en/notice/596425-2026/xml",
"procedureIdentifier": "8041f035-c752-48c6-8772-62de0a83aa8b",
"changeReasonDescription": null
}

totalValue is null here because this particular notice type doesn't carry one — see the FAQ. daysUntilDeadline is computed at run time (whole days, counted in Brussels local time — the zone TED itself stamps every deadline in), so the same notice returns a smaller number each day and goes negative once it closes. changeReasonDescription is null because this is an original notice, not a corrigendum; on a corrigendum it reads like "Deadline for receipt of tenders: INSTEAD OF 24/08/2026 16:00 +02:00 PLEASE READ 10/09/2026 16:00 +02:00".

Pricing

result — $0.003 per returned notice. The run start is free, no minimum spend.

The niche's Store leader by users, foxlabs/ted-tenders (39 users), charges $0.004 per result plus a $0.00005 Actor-start fee — we're ~25% cheaper per row with no start fee at all. 1,000 notices costs $3.00 here vs. $4.00+ there. Re-verified against their live pricing 2026-10-01.

On features, read this comparison carefully, because an earlier version of this paragraph overstated it. foxlabs does ship keyword search over notice text and the same 24 output languages we do, and its "expert query" field accepts TED's native DSL, which can express a contract-value floor (total-value>=1000000) — so we do not beat them on full-text search, language coverage, or value filtering as such. What this Actor adds over their live input schema: procedureType filtering (their noticeTypes filters the notice kind — contract notice, award, corrigendum — not the procurement procedure), deadline-based filtering via onlyOpenDeadlines/minDaysUntilDeadline against the computed daysUntilDeadline, and watch-mode alerts with watchLabel/webhookUrl. The structural difference on value: their DSL documents that an expert query overrides every other filter, so there you pick either a value floor or your country/CPV/date filters, while minValue/maxValue here compose with everything else in the same run. Verified against their live input schema 2026-10-01.

FAQ

Why not just call the TED API myself? You can — it's free and public. What you get here is normalization: TED's raw fields are multilingual maps and duplicated per-lot arrays, which are painful to consume directly. This Actor gives you one flat row per notice with an English-preferred title, a deduplicated CPV list and a single deadline date.

Does this cover contract value? Yes, when TED has it (totalValue/totalValueCurrency) — not every notice type carries a value (e.g. prior-information notices often don't). Use minValue/maxValue to filter on it directly instead of filtering the output yourself; notices with no value are dropped whenever either is set.

Which notice-type and procedure-type codes actually exist? All of them are in the two dropdowns — we exhausted TED's own search index to get the complete lists rather than documenting a handful of examples: 22 notice types (excluding all 22 leaves exactly 0 notices across TED's entire history) and 17 procedure types (8 modern eForms codes plus 9 legacy pre-2023 single-character codes TED still accepts). Two upstream traps this closes: there is no pin-standard code (prior-information notices are split across six pin-* codes) and no exp-int-rest procedure type — both are plausible-looking values TED rejects outright with HTTP 400, which used to fail the run with a misleading "TED outage, please re-run" message. One asymmetry remains and it is TED's, not ours: the value 7 appears in the output procedureType field on some older notices, but TED's expert search refuses 7 as a filter value, so it is not selectable.

Can I filter by procedure type (open vs. restricted vs. negotiated)? Yes — set procedureType to any TED procedure-type codes (e.g. open, restricted, neg-w-call). It's a server-side filter sent straight to TED alongside noticeTypes/cpvCodes/countries, so it doesn't cost extra requests. About 90% of notices carry this field in a recent live sample; the rest have no procedure-type recorded by TED and are excluded from any procedureType match the same way an unset value would exclude them from any other filter.

Can I search full text? Yes — set keywords (e.g. "cloud hosting"), which is sent as TED's FT~ full-text operator. It searches the whole notice text TED indexes, not just title/description/buyer name — verified live (2026-09-28): a search for "software" matched a German architectural-services notice whose only occurrence of the word was buried in its technical-capacity/selection-criteria requirements ("Angaben zur eingesetzten CAD- und AVA-Software"), a section this Actor doesn't expose as its own output field. So a returned row is a genuine match, but you may not be able to see why it matched by reading the output alone. For anything beyond that, expertQuery gives raw access to TED's expert-search syntax.

Can I pull a specific historical month or quarter, not just "the last N days"? Yes — set publicationDateFrom/publicationDateTo (either or both) to an absolute YYYYMMDD window; it overrides publishedWithinDays.

Can I get titles/descriptions in a language other than English? Yes — set outputLanguage to any of 24 EU language codes (e.g. deu, fra, spa, pol). TED publishes every notice in every EU language, so this returns the same notice in your chosen language instead of English, including a matching-language notice URL. Notices without a translation into that language fall back to English automatically.

How do multiple filters combine — AND or OR? Values within the same field are OR'd (countries: ["DEU", "FRA"] matches either), and different fields are AND'd (country + CPV code together narrows, doesn't widen). Set expertQuery if you need anything more precise than that — it replaces all the filters above with TED's own raw expert-search syntax.

Is buyerEmail/buyerPhone a personal contact? No — these are the contracting authority's own published contact point for the procedure (organisation-email-buyer/organisation-tel-buyer in TED's schema), republished verbatim from an official EU government source. Most are role mailboxes (vergabestelle@…, einkauf@…); we don't enrich or cross-reference them.

Why did my first watchLabel run return nothing? By design — the first run on a new label + filter combination is a baseline: it records everything currently matching so the next run can tell you what's new, and charges nothing.

Can I reset or inspect a watch baseline? Yes. It is a plain JSON record in the fetchsmith-ted-watch key-value store on your own account, keyed by your label plus a fingerprint of your filters. Delete the record to start over, or read seenIds to see exactly what has been delivered.

A tender I'm tracking got its deadline extended — will watchLabel catch that? Yes, but as a new row, not an edited one: TED publishes the extension as a corrigendum with its own publicationNumber, so on your next watch run it arrives as a genuinely new notice. Match it back to the original via procedureIdentifier (both share it), and read changeReasonDescription for TED's own explanation of exactly what changed — no need to diff the two rows yourself.

Why do my results carry CPV codes I never asked for — and sometimes not the one I did? Because TED's classification-cpv filter matches the whole CPV subtree beneath the code you supply, at every level of the hierarchy, and a notice is usually tagged with several codes. There is no exact-code mode in TED's API, so there isn't one here either. Measured live on 2026-09-21 (publication window 2025-03-01..2025-03-31, 10 notices each): cpvCodes: ["72000000"] returned 10/10 notices carrying at least one 72… code but only 5/10 carrying the literal 72000000 (the rest were tagged 72313000, 72220000, 72200000, 72262000); cpvCodes: ["72220000"] returned only 5/10 with the literal code (others: 72222100, 72222300, 72224000); and even the narrow-looking cpvCodes: ["72267000"] returned only 6/10 with the literal code, because its children 72267100/72267200 match too. Picking a longer code narrows the subtree but never collapses it to a single code — a code only matches exactly when the CPV hierarchy gives it no children at all, and you can't tell which those are from the code alone. Workaround: filter the cpvCodes output array yourself after the run (e.g. keep rows where cpvCodes contains your exact code), or use expertQuery if you need TED's raw syntax.

Can the same notice be returned — and charged — twice in one run? No. TED is paged by page number over a live index and its search endpoint takes no sort parameter, so a notice published while a walk is in progress shifts later rows onto a page that was already read, and TED then serves the same publication-number on two pages. Every repeat is dropped before it is normalized, pushed or charged, so you pay for each notice exactly once; the run log names the count when it happens (N notice(s) were served more than once by TED's own paging) and the webhookUrl payload carries it as duplicateRowsDropped. Measured live on 2026-09-22 — 2,000 rows over 8 pages of a 4,486-match query, 0 repeats — so on short walks this is rare; the guard exists because the rate depends on how busy TED is while your run is walking, and a silent double charge is not something you should have to audit for yourself.

I'm exporting to Excel/CSV and the CPV codes column looks broken. That's Apify's default array export splitting cpvCodes (and the 3 other array fields) into numbered columns like cpvCodes/0, cpvCodes/1 that shift position between rows with different counts. Set flatten: true and re-run — those 4 fields become a single comma-separated string column instead.

Why are deadlineReceiptRequestDate/changeReasonDescription null on most rows? Both are gated by notice type, not a bug or a filter you're missing. Measured directly against TED's own API (250-notice live sample, last 30 days, all countries): deadline-receipt-request-date-lot fills on 8.3% of cn-standard (open contract notices — TED lets a buyer publish a separate request-to-participate deadline distinct from the tender deadline, but most don't) and 0% of can-standard (award notices — the tender window is already closed, there's nothing left to request). change-reason-description fills on 20% of cn-standard (a contract notice with a live corrigendum against it) and ~0-1% of can-standard/can-modif. Neither field is meaningful on an award notice; if you want them populated, filter noticeTypes: ["cn-standard"].

How is webhookUrl different from Apify's own platform webhooks? Apify's platform webhooks are configured separately per Task/Actor via the Console or the Webhooks API — useful if you already live in the Apify Console, but extra setup if you're calling this Actor's API directly and just want a completion ping. webhookUrl is a plain input field: set it on the run itself and it POSTs a JSON body (actorRunId, defaultDatasetId, finishedAt, pushed, pagesScanned, duplicateRowsDropped, totalNoticeCount, and — if watchLabel is set — watchSeeding/watchNewCount/baselineTruncated/baselineTruncatedTotal) once the run finishes and every row is already pushed and charged. It's best-effort — a slow or failing webhook only logs a warning, it never fails the run, changes the result set, or affects billing.

Notes

Only public data from an official EU government API is collected — no ToS or anti-bot risk. Issues or feature requests: support@fetchsmith.com. Also available as a hosted API at https://fetchsmith.com

Source code: https://github.com/Fetchsmith/fetchsmith/tree/main/actors/eu-ted-tenders-scraper

Engineering write-ups behind this Actor:

More tools: fetchsmith.com/tools