# EU TED Tender & Award Monitor (`automa-flow/eu-ted-tender-history-award-tracker`) Actor

Search EU TED tenders and monitor lot deadlines, awards and winning suppliers. Export values, tender counts and verified changes for your CRM.

- **URL**: https://apify.com/automa-flow/eu-ted-tender-history-award-tracker.md
- **Developed by:** [Vadim Bezrukov](https://apify.com/automa-flow) (community)
- **Categories:** Business, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 useful history notices

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## EU TED Tender & Award Monitor

### What does EU TED Tender & Award Monitor do?

Search EU procurement notices, follow tender histories and inspect published
awards and winning suppliers. Turn TED links, procedure identifiers or search
filters into JSON/CSV-ready records for a procurement watchlist, CRM or analysis.
The first successful monitor run returns a baseline; repeat runs identify newly
observed publications and verified changes.

Use the official TED Search API and Open Data Service through this independent
Actor. The default example, `107799-2025`, includes a Czech road tender, a deadline
correction and an award for CZK 4,000,000. Published award statistics identify five
received tenders and an award decision dated 2025-04-01. Fields that TED does not
publish remain `null`; a missing field is never a cancellation or a lost contract.

This repository describes the schema-2 upgrade. Its new search inputs must be
deployed before using them on the hosted Actor. The technical Actor slug and
existing API inputs remain unchanged.

### Monitor tenders from announcement to award

Supply `procedureIds`, `publicationNumbers`, or the existing `targets` / `tedUrls`.
An entry in `targets` accepts exactly one `noticeId`, `tedUrl` or `procedureId`,
plus an optional `externalRef` for your CRM. Each input gets a `target_status`,
including invalid entries and duplicate aliases. Shared histories are delivered
once; join business-row `input_indices` to `target_status.input_index` for every
CRM reference.

Notice UUID, version, publication number and explicit procedure references carry
identity. A title never establishes a relationship. Lots are notice-scoped;
cross-notice continuity requires an explicit lot reference or a unique shared
internal lot identifier. Ambiguous references remain visible as evidence/issues.
Unresolved links do not justify moving an award onto another procedure or lot.

### Track competitor contract wins

Use `winnerNames` to discover award/result publications. Search first finds
candidates; normalized whole-token phrase matching then verifies supplier names.
`Accenture` matches `Accenture Outsourcing Services`, but not `NotAccenture`.
`winner_matches` reports `matched_winner_query`, `matched_winner_name` and
`EXACT_NORMALIZED` or `CONTAINS`. No fuzzy matching or corporate alias resolution
is claimed.

Selected notices expand into their linked histories. Those histories may include
other suppliers or older awards: use `winner_matches` on award/winner rows to
select the requested supplier, and `publication_date` for the reporting window.
Search filters select notices; they do not filter every row in an expanded history.

### Track tender and lot deadline changes

Corrections compare the full normalized states of verified lot tracks, including
lots omitted from the declared changed-section list. Deadlines retain their raw
source offset and a UTC instant. `previous_deadline` comes from a unique verified
parent; `deadline_extension_days` is the elapsed UTC difference divided by 86,400.
A negative value means a shortened deadline. Date-only or unknown-zone values
do not produce an elapsed-time delta.

### Analyze awards and procurement outcomes

Award rows contain winners, winning-tender value, currency, tender count and
award decision date when published. Contract rows retain their own lot reference,
conclusion date and value. A conflicting contract/tender relationship is reported
without transferring the tender's winner or value to the wrong contract.

Derived fields are deterministic convenience calculations, not official TED metrics:

| Field | Meaning |
| --- | --- |
| `award_vs_estimate_pct` | `(winning value - estimate) / estimate * 100`, rounded to four decimals. Requires matching currency/scope, a positive estimate and one award outcome for that lot observation. |
| `single_bid` | `true` for one received tender, `false` for more than one, otherwise `null`. |
| `competition_intensity` | 1: `SINGLE_BID`; 2–3: `LOW`; 4–7: `MEDIUM`; 8+: `HIGH`; zero/missing: `NONE_OR_UNKNOWN`. |
| `time_to_award_days` | Award decision's local calendar date minus the latest uniquely linked tender deadline's local date. Negative/unknown comparisons are `null`. Publication dates are never substituted. |

Multiple winners remain separate winner rows for the same award. Its full value
is repeated with `JOINT_AWARD_NOT_ALLOCATED`; do not sum those winner rows as
separate contracts. No FX conversion, supplier success rate or bid-win rate is
calculated: TED award publications do not expose every supplier's submitted bids.

### What data do you get?

The default Dataset preserves existing snake\_case record types and adds
`winner_observation` and `search_status`. Each row includes `schema_version: 2`,
`source`, stable `source_id`, `source_url`, UTC `scraped_at` and a deterministic
`fingerprint`. KVS state retains its existing version/layout; no baseline reset is
required. Parser version 3 causes a refresh; newly interpreted historical events
are marked `INTERPRETATION_UPDATED`, rather than presented as new buyer activity.

Views: **Opportunities**, **Changes**, **Awards**, **Winners**, plus the existing
`dossiers`, `statuses` and `evidence` views. Views select columns, not row types.
Filter downloaded records by `record_type`; award-only exports use `award_outcome`,
winner-only exports use `winner_observation`. No unsupported server-side view
filter is claimed.

Events include `NEW_PROCEDURE`, `NEW_NOTICE`, `NEW_LOT`, `DEADLINE_CHANGED`,
`ESTIMATED_VALUE_CHANGED`, `LOT_STATUS_CHANGED`, `AWARD_PUBLISHED`,
`AWARD_CORRECTED`, `AWARD_VALUE_CHANGED`, `WINNER_CHANGED`,
`TENDERS_RECEIVED_CHANGED`, `CONTRACT_PUBLISHED` and contract-modification events.
`NEW_*` means first observed in the verified history, not guaranteed earliest
publication in TED. Baseline history is backfill. For notifications use only
`crm_eligible: true`; deduplicate by `event_id`.

### Input examples

Find new EU tender publications:

```json
{"keywords":["cybersecurity"],"countries":["DEU"],"daysBack":7,"maxResults":100}
```

Monitor an existing tender:

```json
{"publicationNumbers":["107799-2025"],"monitorKey":"important-bids","mode":"changesOnly"}
```

Track competitor awards:

```json
{"winnerNames":["Accenture","Capgemini"],"countries":["DEU","FRA"],"cpvCodes":["72"],"daysBack":30,"monitorKey":"competitors"}
```

`buyerNames`, `publicationDateFrom/To`, `deadlineFrom/To`,
`minEstimatedValue`, `maxEstimatedValue` and `currency` are also supported.
Dates are inclusive `YYYY-MM-DD`. Use `daysBack` or explicit publication dates.
Countries identify buyers, not the place of performance. CPV accepts 2–8 digit
prefixes. TED's notice-level filters may match different lots in the same notice;
inspect the lot records before making a bid decision. Competition publications
are not guaranteed still open: check the current verified deadline.

Use search filters or an identifier watchlist, not both. Search defaults to a
30-day window and 100 candidate notices; arrays accept up to 20 search values.
`maxResults` bounds candidates, not the number of rows or expanded history notices.
When capped, narrow the dates/filters: incomplete searches do not advance state.
`includeLots`, `includeAwards`, `includeChanges` and `includeDerivedMetrics` control
output. They do not eliminate the source verification required for safe monitoring.

### Output examples

A subset of the live Czech award observation:

```json
{"record_type":"award_outcome","schema_version":2,"notice":"273334-2025","lot_handle":"LOT-0001","award_status":"WINNER_SELECTED","winning_value":{"amount":"4000000","currency":"CZK","value_type":"WINNING_TENDER","scope":"LOT","source_notice":"273334-2025"},"tenders_received":5,"award_decision_date":"2025-04-01","single_bid":false,"competition_intensity":"MEDIUM"}
```

`search_status` distinguishes `SUCCESS`, `NO_RESULTS`, `PARTIAL`, `SOURCE_FAILED`
and `SUSPICIOUS_EMPTY`. `target_status.outcome` retains `SUCCESS`, `PARTIAL`,
`NOT_FOUND`, `INVALID_INPUT`, `FAILED` and `SKIPPED_LIMIT`. A successful empty
search is different from a request failure. A previously populated search becoming
empty preserves state for investigation.

### Schedule monitoring

Save a monitoring input and schedule it daily in your Apify account, for example
at 07:00 UTC (`0 7 * * *`). Keep `monitorKey` unchanged. It uses the persistent
`ted-tender-award-monitor` KVS in your account, with a namespace derived from the
monitor name and search filters. The first run emits a baseline; quiet repeats
still emit statuses. Changing filters creates a separate comparison namespace.

Existing `mode: "monitor"`, `watchlistId`, and exactly one of `stateStoreId` or
`stateStoreName` remain supported. Do not combine those fields with `monitorKey`.
Use `snapshotAndChanges` with `monitorKey` for snapshots plus changes on each run;
without it, the mode is a non-persistent snapshot.

A run-success webhook can deliver the Dataset URL to n8n, Make or your CRM.
Fetch rows and process `crm_eligible` events only; inspect `RUN_SUMMARY` for partial
coverage first. Local Task drafts are in `examples/upgrade-task-drafts.json`.
Creating schedules, publishing Tasks and running paid jobs are separate actions.

### API / automation use

Start the unchanged Actor slug `automa-flow/eu-ted-tender-history-award-tracker`
through the Apify API with your input JSON and a deliberate `maxTotalChargeUsd`
run setting. Read `RUN_SUMMARY` and the default Dataset, then join aliases through
`input_indices`. `scripts/crm_adapter.py` demonstrates deduplication and CRM export.

Direct MCP endpoint:
`https://mcp.apify.com?tools=automa-flow/eu-ted-tender-history-award-tracker`.
Example: “Inspect TED 107799-2025, show winning suppliers and tender counts, flag
unresolved links, and cap the run at $0.10.” Use your own authenticated Apify
session. New hosted schema/MCP exposure must be rechecked after this upgrade is
deployed; earlier checks do not establish availability of these new inputs.

### Pricing

The existing contract remains $0.01 per useful unique notice version
(`history-notice`) and $0.01 per completed repeat procedure check (`watch-check`).
An unchanged successful check is still a paid check. Baseline has no check fee.
There is no extra fee for metrics, winner rows, retries, aliases or failure rows.
Award, lot and event rows belonging to one notice do not multiply its history fee.

For a three-useful-notice history, baseline is $0.03 and 30 successful quiet checks
are $0.30. This is an example workload, not an average. A $0.10 evaluation cap may
stop a longer history with explicit partial output. The minimum spend limit is
$0.01, not a minimum invoice. No useful billable units means zero custom events.
The upgrade's four offline baskets are in `examples/upgrade-benchmark.json`;
new hosted cost and margin remain unmeasured.

### Limitations

- Monitoring reflects published TED data. External buyer portals may publish
  clarifications earlier; the Actor does not follow third-party documents.
- ODS enrichment can lag Search. Pending, failed and partial enrichment are
  explicit and retain known values. No missing value means cancellation/no-award.
- Legacy `WINNER_SELECTED`, `NO_WINNER`, `UNKNOWN` meanings are preserved.
  Procedure-wide cancellation/status transitions are not inferred from partial
  lot results. Dedicated cancellation classification is outside the verified mapping.
- Lot/result continuity can remain unresolved. Contract identity across different
  notices is not proven by reusing `CON-0001`; term-level deltas require evidence.
- Winner-name matching is not legal-entity resolution. Published data may omit
  winners, currencies, dates, values or tender counts.
- Three transport attempts are bounded. HTTP 429 opens a circuit immediately;
  the reported Retry-After should guide the next run. No proxy rotation or browser.
- Concurrent writers fail closed. Delivery is at-least-once; keep event IDs and
  billing receipts, and do not delete an active writer's lock. Export Dataset
  history before account retention removes it.

### Data source and legal notes

TED, Publications Office of the European Union, © European Union. This Actor
normalizes and interprets published data and is not endorsed by TED. See the
[Search API documentation](https://docs.ted.europa.eu/api/latest/search.html) and
[TED legal notice](https://ted.europa.eu/en/legal-notice), rechecked 2026-09-13.
Public procurement notices are reusable subject to the stated exceptions;
third-party rights and personal-data rules still apply. No contact emails,
telephone numbers, personal addresses or bidder profiles are collected.
Organisation identifiers can identify sole traders: retain only what the
procurement workflow needs.

From the repository root, `uv run pytest actors/eu-ted-tender-history-award-tracker/tests`
runs offline tests. `scripts/local_run.py` performs a bounded public HTTP snapshot
without Apify billing; `scripts/revalidate_upgrade.py` refreshes the source evidence.
For support, provide the public publication number, run ID and disputed field;
do not send credentials or an entire CRM export.

# Actor input Schema

## `targets` (type: `array`):

Up to 100 entries. Each has exactly one noticeId, tedUrl or procedureId; externalRef carries your CRM key. Invalid entries get their own status.

## `tedUrls` (type: `array`):

Alternative to targets: official TED notice links.

## `mode` (type: `string`):

Snapshot returns available history. Monitor remembers the same list in your persistent store.

## `stateStoreId` (type: `string`):

Required for monitor. ID of a store you own and explicitly authorize this Actor to read and write. Do not use a run-specific default store.

## `stateStoreName` (type: `string`):

Alternative to the store ID for monitor: the name of a Key-value store in your own account, created on the first run if it does not exist. Portable across copied Tasks. Give exactly one of store ID or store name.

## `watchlistId` (type: `string`):

Required for monitor. A stable namespace within your store; keep it unchanged on repeats.

## `changesOnly` (type: `boolean`):

The first run always returns a dossier. Target statuses remain visible on quiet repeats.

## `keywords` (type: `array`):

Match any phrase in TED full text; discovery searches competition notices.

## `countries` (type: `array`):

Three-letter TED buyer-country codes, e.g. DEU or FRA; not place of performance.

## `cpvCodes` (type: `array`):

Two to eight digits; prefixes such as 72 include their descendants.

## `buyerNames` (type: `array`):

Match any buyer-name phrase in TED Search.

## `winnerNames` (type: `array`):

Find award notices. Exact normalized or whole-token phrase matching; no company identity resolution.

## `procedureIds` (type: `array`):

Alternative to targets: up to 100 TED procedure UUIDs.

## `publicationNumbers` (type: `array`):

Alternative to targets: up to 100 TED publication numbers.

## `monitorKey` (type: `string`):

Enables persistent monitoring in your account. Keep unchanged on repeat runs. Alternative to legacy store/watchlist fields.

## `publicationDateFrom` (type: `string`):

Inclusive YYYY-MM-DD boundary. Filters match notices, not necessarily the same lot.

## `publicationDateTo` (type: `string`):

Inclusive YYYY-MM-DD boundary. Filters match notices, not necessarily the same lot.

## `deadlineFrom` (type: `string`):

Inclusive YYYY-MM-DD boundary. Filters match notices, not necessarily the same lot.

## `deadlineTo` (type: `string`):

Inclusive YYYY-MM-DD boundary. Filters match notices, not necessarily the same lot.

## `daysBack` (type: `integer`):

Discovery only. Search defaults to 30 days and at most 100 candidates; capped results are explicitly partial.

## `maxResults` (type: `integer`):

Discovery only. Search defaults to 30 days and at most 100 candidates; capped results are explicitly partial.

## `minEstimatedValue` (type: `number`):

Notice-level TED estimated lot value filter; requires currency. No FX conversion.

## `maxEstimatedValue` (type: `number`):

Notice-level TED estimated lot value filter; requires currency. No FX conversion.

## `currency` (type: `string`):

Three-letter currency for estimated value filters.

## `includeLots` (type: `boolean`):

Include these output fields/records. Source verification is still performed.

## `includeAwards` (type: `boolean`):

Include these output fields/records. Source verification is still performed.

## `includeChanges` (type: `boolean`):

Include these output fields/records. Source verification is still performed.

## `includeDerivedMetrics` (type: `boolean`):

Include these output fields/records. Source verification is still performed.

## `maxNoticesPerProcedure` (type: `integer`):

Stops with explicit partial coverage when reached; raise deliberately to continue the same watchlist.

## `maxTotalNotices` (type: `integer`):

Stops with explicit partial coverage when reached; raise deliberately to continue the same watchlist.

## `maxDiscoveryNotices` (type: `integer`):

Stops with explicit partial coverage when reached; raise deliberately to continue the same watchlist.

## Actor input object example

```json
{
  "targets": [
    {
      "noticeId": "107799-2025",
      "externalRef": "demo-road-tender"
    }
  ],
  "mode": "snapshot",
  "changesOnly": true,
  "includeLots": true,
  "includeAwards": true,
  "includeChanges": true,
  "includeDerivedMetrics": true,
  "maxNoticesPerProcedure": 200,
  "maxTotalNotices": 500,
  "maxDiscoveryNotices": 1000
}
```

# Actor output Schema

## `opportunities` (type: `string`):

No description

## `winners` (type: `string`):

No description

## `dossiers` (type: `string`):

No description

## `awards` (type: `string`):

No description

## `changes` (type: `string`):

No description

## `statuses` (type: `string`):

No description

## `evidence` (type: `string`):

No description

## `runSummary` (type: `string`):

No description

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "targets": [
        {
            "noticeId": "107799-2025",
            "externalRef": "demo-road-tender"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("automa-flow/eu-ted-tender-history-award-tracker").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "targets": [{
            "noticeId": "107799-2025",
            "externalRef": "demo-road-tender",
        }] }

# Run the Actor and wait for it to finish
run = client.actor("automa-flow/eu-ted-tender-history-award-tracker").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "targets": [
    {
      "noticeId": "107799-2025",
      "externalRef": "demo-road-tender"
    }
  ]
}' |
apify call automa-flow/eu-ted-tender-history-award-tracker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automa-flow/eu-ted-tender-history-award-tracker"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/8lUFubgrv8eWMdrVy/builds/grzAcB5rjsdjs1hdo/openapi.json
