TED EU Tender Monitor — Search & Delta-Track Tenders
Pricing
from $0.01 / 1,000 results
TED EU Tender Monitor — Search & Delta-Track Tenders
Search EU public procurement notices (TED) by keyword, CPV code, country and date as clean JSON, with a delta mode returning only new tenders per run. Built for logistics/freight bid teams. No API key needed.
Pricing
from $0.01 / 1,000 results
Rating
0.0
(0)
Developer
Daniel Posztos
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
0
Monthly active users
7 days ago
Last modified
Categories
Share
TED EU Tender Monitor — search & delta-track EU public tenders as JSON
SEO title: TED EU Tender Monitor — Search Public Tenders as JSON SEO description: Search EU public tenders (TED) by keyword, CPV code, country and date as clean JSON, with a delta mode returning only new tenders per run. Built for logistics/freight bid teams. No API key needed.
Search TED (Tenders Electronic Daily) — the official EU public procurement portal — by keyword, CPV code, buyer country, and publication/deadline date, and get clean, structured JSON back instead of TED's own XML/eForms notice format. Add a scheduled run with mode: "delta" and get only the tenders you haven't seen before, so you can watch a saved search for new opportunities without re-processing everything every time.
No scraping, no login, no API key: TED's Search API v3 is public and unauthenticated.
Why this exists
TED's own Search API is real and free, but not turnkey:
- Its query language ("expert query") is a small custom DSL with undocumented gotchas (see "TED API discoveries" below) — dates in
YYYYMMDD, not ISO-8601; countries in ISO alpha-3, not alpha-2; response field types that don't always match what TED's own OpenAPI spec declares. - Responses are nested, multi-language objects (a title is
{"eng": "...", "deu": "...", ...}for all 24 EU languages) rather than a flat row. - There's no "give me only what's new since last time" mode — every scheduled run gets everything again, and building your own dedup layer is exactly the kind of undifferentiated work this actor exists to remove.
This actor wraps all of that: one flat JSON object per tender, one input schema, and a delta mode with its own persistent "already seen" tracking.
Input
| Field | Type | Default | Description |
|---|---|---|---|
keywords | array of strings | — | Full-text search terms, OR'ed together (any match). E.g. ["logistics", "freight forwarding"]. Cannot contain " or \. |
cpv_codes | array of strings | — | CPV codes, 2/4/6/8 digits (chapter/group/class/full), e.g. "60" (transport services), "6024" (freight transport by road), "63120000" (storage and warehousing). The official -N check-digit suffix is accepted and ignored. |
countries | array of strings | ["DE", "HU"] | ISO 3166-1 alpha-2 buyer-country codes, e.g. "DE", "HU". This is the contracting authority's country, not the notice's publication language. |
published_after | string ("YYYY-MM-DD") | "2026-01-01" | Only notices published on/after this date. |
deadline_before | string ("YYYY-MM-DD") | — | Only notices whose earliest tender-submission deadline is on/before this date. Omit to not filter by deadline (also returns notice types with no deadline at all, e.g. contract award notices). |
mode | string | "full" | "full": every matching tender every run. "delta": only tenders not already returned by a previous run with the exact same keywords/cpv_codes/countries/date filters. See "Delta mode" below. |
max_items | integer | 20 | Max tenders to return (and charge for) per run, 1–500. |
At least one of keywords, cpv_codes, or countries is required — published_after/deadline_before narrow a search, they don't define one on their own.
Why published_after defaults to a fixed "2026-01-01" instead of a relative "N days ago": a static JSON Schema default can't compute "today minus 90 days" — it can only be a fixed value. A fixed, sufficiently-recent-when-written date keeps matching plenty of notices for a long time after this actor was built (TED publishes hundreds of notices a day), which is what actually matters for a default Run/daily health-check to keep succeeding — not that the date stays "recent" forever. If this actor is still running years from now and the default Run starts returning few/no results, that default is the thing to bump, not a sign of a bug.
Worked example — input
{"keywords": ["logistics", "freight forwarding", "warehousing"],"countries": ["DE", "HU"],"published_after": "2026-01-01","mode": "full","max_items": 20}
Worked example — output (real data, 2 of 20 items, fetched 2026-07-06)
[{"tender_id": "464044-2026","title": "Germany – Storage and warehousing services – Logistikdienstleistung Hilfsmittelversorgung","buyer": "BG Klinikum Hamburg gGmbH","country": ["DE"],"cpv": ["63120000", "64110000", "33141000", "33140000"],"deadline": "2026-07-13","value_estimate": null,"currency": null,"url": "https://ted.europa.eu/en/notice/464044-2026/html","publication_date": "2026-07-06","notice_type": "cn-standard"},{"tender_id": "442473-2026","title": "Germany, Belgium – Water transport services – On-/offshore Cable Jointing Services & Offshore Cable Repair Logistic Services","buyer": "50Hertz Transmission GmbH: Vertragsschließende Einheit / Auftraggeber (Contracting Entity) für Lots 1–5","country": ["DE", "BE"],"cpv": ["60600000", "45314300", "50532400"],"deadline": null,"value_estimate": 110000000.0,"currency": "EUR","url": "https://ted.europa.eu/en/notice/442473-2026/html","publication_date": "2026-06-29","notice_type": "cn-standard"}]
value_estimate/deadline are null on plenty of real notices — TED doesn't require every field for every notice type (e.g. a contract award notice, notice_type: "can-standard", has no submission deadline; not every buyer discloses an estimated value). This is expected, not a bug — see "Known limitations".
Output schema
| Field | Type | Notes |
|---|---|---|
tender_id | string | TED's own publication number, e.g. "461933-2026". Globally unique; also the delta-mode dedup key. |
title | string or null | Notice title (English; TED auto-translates into all 24 EU languages, so this is present for effectively every notice). |
buyer | string or null | Contracting authority name. |
country | array of strings | Buyer country/ies, ISO 3166-1 alpha-2. A list because joint-procurement notices can have buyers from multiple countries. |
cpv | array of strings | CPV codes (main + additional), deduplicated. |
deadline | string or null | Earliest tender-submission deadline across the notice's lots, "YYYY-MM-DD". |
value_estimate | number or null | Estimated value of the notice's first lot only — see "Known limitations". |
currency | string or null | ISO 4217 currency for value_estimate. |
url | string | Link to the full notice on ted.europa.eu (English UI). |
publication_date | string or null | "YYYY-MM-DD". |
notice_type | string or null | TED's own code, e.g. "cn-standard" (contract notice), "can-standard" (contract award notice), "pin-standard" (prior information notice). |
Delta mode
Set "mode": "delta" on a scheduled run to get only tenders not already returned by a previous run with the exact same keywords/cpv_codes/countries/published_after/deadline_before (changing max_items or mode itself doesn't count as a different search — only the filter fields do).
How it works: a named Key-Value store (ted-tender-monitor-seen) holds one "seen tender IDs" record per distinct filter combination, keyed by a stable hash of those filter fields. Each delta run loads that record, skips any matching tender already in it, charges/delivers only the rest, and then adds the newly-delivered tenders' IDs back into the record. A tender dropped by a budget stop (ACTOR_MAX_TOTAL_CHARGE_USD) is never marked "seen" — it was never paid for or delivered, so it's offered again on the next run. The seen-set is bounded (pruned past 400 days old relative to its own newest entry, and hard-capped at 50,000 IDs) so it can't grow forever across months of scheduled runs.
0 new tenders in delta mode is a normal, expected result — most days, nothing new matches a narrow saved search. This actor logs an explicit INFO line ("Delta mode: 0 new tenders since last run (...)") and sets that as the run's status message, rather than the WARNING that full mode uses for its own "valid search, genuinely 0 matches" case (see "Error messages"). full mode runs never read or write the delta store at all, so alternating full and delta runs on the same search can't corrupt the delta baseline.
Verified locally: running the same delta-mode input twice in a row returned 15 real tenders on the first run and exactly 0 on the second, with the INFO-not-WARNING message, and the Key-Value store persisting the 15 tender IDs between runs.
Pricing (pay-per-event)
| Event | Price | When it's charged |
|---|---|---|
tender-result | $0.005 | Once per returned tender. In delta mode, only tenders not already returned by a previous run with the same search are charged. |
Plus Apify's own apify-actor-start synthetic event (first 5 seconds of compute free, platform-managed).
Examples: full mode, 20 matching tenders → 20 × $0.005 = $0.10. delta mode on a daily schedule with typically 2–3 genuinely new tenders/day → ~$0.01–0.015/day; a quiet day with 0 new tenders costs $0.00.
If a run's cost would exceed the Max total charge USD you set for it, the actor stops producing further tenders at exactly that point — every delivered tender was paid for, and nothing paid-for is ever dropped. Verified locally (ACTOR_TEST_PAY_PER_EVENT=1): $3 budget → 1 tender charged = 1 delivered; $5 → 2 = 2; both stopped gracefully with exit code 0, no crash.
Error messages
- No input at all / no search criteria: explains the minimal valid input — at least one of
keywords/cpv_codes/countries. - Dates alone, no other criteria: "'published_after'/'deadline_before' alone are not enough -- they narrow a search, they don't define one."
- Unrecognized country code: "'countries' entry 'ZZ' is not a recognized ISO 3166-1 alpha-2 country code..." — also rejects alpha-3 codes (e.g.
"DEU") since this input field is alpha-2 by convention with the rest of this portfolio. - Malformed CPV code: explains the required 2/4/6/8-digit shape with worked examples.
- Keyword containing
"or\: rejected with a suggested fix (these characters can't appear in a TED full-text search term as constructed by this actor). - Malformed date /
published_afterafterdeadline_before/ unknownmode/max_itemsout of range: each rejected with the exact rule and an example fix. - 0 tenders with otherwise valid input,
fullmode: the run still succeeds (exit 0), but the log carries an explicitWARNING: 0 results produced despite valid input (...)line and the run's status message says so — never a silent "nothing happened." - 0 tenders in
deltamode: explicitINFO, notWARNING— this is the expected steady state, not a problem. See "Delta mode". - TED API unreachable: logged as an
ERRORwith the underlying cause; the run still completes (0 tenders, same as above) rather than crashing outright.
TED API discoveries (undocumented outside this actor's code comments)
Recorded here because there is no user-facing grammar/field reference for TED's Search API v3 beyond its interactive Expert Search page — these were found by hitting the API directly and reading its own error messages (2026-07-06):
- No API key needed.
POST https://api.ted.europa.eu/v3/notices/searchworks fully unauthenticated; no rate-limit headers were observed. - POST only — a GET on the same path returns HTTP 405. The full (otherwise unlinked) OpenAPI spec is at
https://api.ted.europa.eu/api-v3.yaml. - Expert-query dates are
YYYYMMDD, not ISO-8601 (confirmed via the server's own format-error message). buyer-countrytakes ISO 3166-1 alpha-3 ("DEU", not"DE") — this actor'scountriesinput stays alpha-2 for consistency with the rest of the portfolio and converts internally (src/countries.py).estimated-value-lotis declared asnumberin TED's own OpenAPI spec but is actually returned as a numeric string (e.g."110000000") — this actor coerces it to a float. Its companion arrayestimated-value-cur-lotisn't even guaranteed to be the same length (observed: 6 values, 1 shared currency).- Notice titles are auto-translated into all 24 EU languages regardless of the notice's original language — the English version was present on every notice fetched during development, including ones from non-EU buyers (e.g. a Kazakhstan buyer).
Known limitations (documented, not hidden)
value_estimate/currencyreflect the first lot only. A multi-lot tender's lots can have different estimated values and even different currencies — summing them would silently mix currencies or misrepresent the tender. The full per-lot breakdown is always one click away aturl.deadlineis the earliest across all lots, not necessarily every lot's deadline (most multi-lot tenders share one deadline anyway).- Delta mode's "seen" tracking is per exact filter combination. Changing
keywords/cpv_codes/countries/dates starts a fresh dedup baseline for that new combination (by design — it's a different saved search) rather than trying to guess overlap with a previous one. max_itemsis capped at 500 per run — TED's own Search API supports far more via pagination (up to 15,000 retrievable notices per query in the default pagination mode), but this actor stays well under that ceiling to keep runs fast and costs predictable; there's no scroll/iteration-mode support (not needed at this ceiling).
Data source (public, no scraping)
TED (Tenders Electronic Daily) Search API v3, the European Union's official public procurement notice portal: https://api.ted.europa.eu/v3/notices/search. Public, unauthenticated, official EU data — the same API TED's own Expert Search page uses.
Example use cases (logistics & freight, this actor's niche)
- Freight forwarders / 3PLs: watch for new road/rail/sea freight tenders (
cpv_codes: ["60", "63"]) across the countries you operate in, on a dailydeltaschedule. - Warehousing operators:
keywords: ["warehousing", "storage and distribution"]to catch contract notices for third-party logistics/fulfillment contracts. - Bid/tender teams at logistics companies: combine
countries(your target markets) withdeadline_beforeto get a rolling shortlist of tenders still open for bidding, sorted newest-first. - Consultancies tracking a client's procurement pipeline: a
full-mode run against a client's buyer-country + relevant CPV codes as a standing research query.