# EU TED Tender Lots & Award Outcomes Collector (`8tp/eu-ted-tender-lot-award-collector`) Actor

Collect EU procurement opportunities and award outcomes from the official TED Search API and TED Open Data. Get one typed row per explicitly linked tender lot or award outcome, plus exact notice versions, winners, no-award reasons, change relationships, and coverage receipts. No login or API key.

- **URL**: https://apify.com/8tp/eu-ted-tender-lot-award-collector.md
- **Developed by:** [Hunter M.](https://apify.com/8tp) (community)
- **Categories:** Lead generation, Business, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 procurement records

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/platform/actors/running/actors-in-store#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 Lots & Award Outcomes Collector](assets/actor-art.png)

Collect procurement notice versions, tender opportunities by lot, award and no-award outcomes, and corrigenda from official European Union sources. The Actor is keyless, deterministic, bounded, and designed for search, AI-agent, MCP, spreadsheet, and automated-data use.

> **Independent software:** This Actor is not affiliated with, sponsored by, or endorsed by TED, the Publications Office of the European Union, or any EU institution. TED and other names and marks belong to their respective owners.

It uses two official sources for two different jobs:

- [TED Search API v3](https://docs.ted.europa.eu/api/latest/search.html) selects notice versions with date, type, buyer-country, CPV, and full-text filters.
- [TED Open Data](https://data.ted.europa.eu/) supplies the exact eProcurement Ontology (ePO) relationships between procedures, lots, award outcomes, tenders, and winner organizations.

Search's parallel field arrays are **never zipped or treated as relational data**. A lot or award row is emitted only from an explicit path in the official RDF graph.

### Source attribution and reuse

**Source:** European Union, Publications Office of the European Union, [Tenders Electronic Daily (TED)](https://ted.europa.eu/). Every data row retains an official notice URL and relational rows retain the official Open Data graph URI.

TED's [legal notice](https://ted.europa.eu/en/legal-notice) says that, unless otherwise noted, procurement notices published in the Supplement to the Official Journal of the European Union may be reused for commercial or non-commercial purposes. SIMAP metadata is dedicated to the public domain under [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/). SIMAP website editorial content is licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), which requires appropriate credit and an indication of changes.

This Actor selects, normalizes, groups, validates, and may truncate source data under documented bounds. Its output is therefore a processed representation, not an unchanged official notice or an authentic legal text. The source links, receipts, and issue records identify those transformations and omissions. The reuse notice does not grant rights in third-party works, trademarks, logos, names, or identifiable-person data; additional rights or a lawful basis may be required for a user's specific purpose.

TED and SIMAP names are used only to identify the data source. This Actor does not use a TED, SIMAP, Publications Office, or EU logo, seal, or emblem. It remains independent software and is not affiliated with or endorsed by an EU institution.

### What you get

- `notice_version`: one source-linked metadata row per selected TED notice version.
- `opportunity_lot`: one row per exact `Procedure → hasProcurementScopeDividedIntoLot → Lot` relation, with title, main CPV, estimated value, currency, and tender deadline when present.
- `award_outcome`: one row per exact `LotAwardOutcome → concernsLot` relation. Related tenders and winner organizations follow `comprisesTenderAwardOutcome → concernsTender → isSubmitedBy → playedBy`. Outcomes with `clos-nw` and a non-award justification are retained even when no tender exists.
- `notice_change`: self-contained change identifiers, section identifiers, reasons, and document-change flags.
- `source_issue`: machine-readable missing-graph, invalid-value, relationship-conflict, timeout, retry-exhaustion, or cap qualification.
- `page_receipt`, `collection_summary`, and `run_meta`: free evidence for pagination, successful or failed RDF batches, completeness, request/byte totals, billing reconciliation, and execution safeguards.

The four business record kinds (`notice_version`, `opportunity_lot`, `award_outcome`, and `notice_change`) form the leading Dataset prefix. Free evidence follows that paid balance: `collection_summary`, then any issues and receipts, with `run_meta` last. This order makes the delivered business prefix directly auditable against pay-per-event charges.

### Pricing and restart safety

The Actor costs **$1 per 1,000 delivered procurement records** ($0.001 each), plus a **$0.00005 Actor-start event**. One `procurement-record` event is charged for each delivered business row of the four kinds above. Summary, source-issue, page-receipt, and run-metadata rows have no custom result-event fee. The live Monetization panel says **User pays platform usage costs: No**, so normal run platform usage is included in these event prices. The minimum maximum-charge setting is $0.002. The platform's synthetic `apify-default-dataset-item` event must have a zero price; the Actor fails before writing data if that event has a nonzero price, because otherwise evidence rows would incur an additional Dataset-item event fee. Post-run Dataset access can still incur normal storage-operation charges under Apify's Store billing rules.

Before publication, the complete bounded result is committed as an immutable, fingerprinted, chunked plan in the run's default key-value store. The Actor writes only the affordable exact business-record prefix to the default Dataset, reads it back, then charges that stored prefix through the run API with a deterministic idempotency key. It confirms the live run charge count before appending free evidence. On restart, an exact stored-but-uncharged prefix is charged without being published twice; a charged-but-missing prefix from an older charge-before-store build can be repaired without charging twice. Any fingerprint, Dataset-prefix, run-identity, storage-identity, or live-count mismatch fails closed.

If the run's maximum charge is too low for the full plan, the Dataset contains the affordable business prefix followed by free evidence. `collection_summary` reports `paid_records_planned`, `paid_records_delivered`, `paid_records_charged`, `paid_records_omitted`, and `spending_limit_reached`; its per-kind emitted counts reflect only rows actually delivered. `output_truncated=true` and `collection_complete=false` when the spending limit omits records. Once evidence has begun, a resurrected run never inserts additional paid rows ahead of it, even if the later spending allowance is higher.

### Quick start

The empty input is useful: it collects up to five opportunity notices from the seven publication days ending yesterday. Ending yesterday gives the Open Data publication process time to expose the corresponding named graphs while keeping the daily run comfortably bounded.

On a busy publication day, TED will report more than five matching notices. The saved default then emits a truthful `notice_cap_reached` issue and sets `source_truncated=true` and `collection_complete=false`; `enrichment_complete` can still be true for every notice in the selected five-notice prefix. Increase `max_notices` when prefix completeness is insufficient.

```json
{}
```

Collect recent award and no-award outcomes in Germany and France:

```json
{
  "search_mode": "awards",
  "lookback_days": 14,
  "countries": ["DEU", "FRA"],
  "max_notices": 25,
  "max_award_records": 1000
}
```

Search an explicit historical window for medical procurement:

```json
{
  "search_mode": "both",
  "published_after": "2026-08-01",
  "published_before": "2026-08-03",
  "cpv_codes": ["33190000"],
  "keywords": ["medical equipment"],
  "preferred_language": "en"
}
```

Explicit dates may include today. If Search has published a notice but its Open Data graph is not available yet, the Actor still emits `notice_version` plus `sparql_graph_missing_or_no_rows`; it does not manufacture lot or winner links.

### Example opportunity lot

```json
{
  "record_kind": "opportunity_lot",
  "publication_number": "754133-2025",
  "procedure_identifier": "533ff290-6341-4582-aeaf-17f57a6afe36",
  "lot_identifier": "LOT-0001",
  "main_cpv_code": "33190000",
  "estimated_value": "420036",
  "estimated_value_currency": "EUR",
  "deadline_receipt_tender": "2025-12-19T17:00:00+01:00",
  "relationship_coverage_status": "complete"
}
```

### Example no-award outcome

```json
{
  "record_kind": "award_outcome",
  "publication_number": "449250-2026",
  "lot_identifier": "LOT-0001",
  "award_status_code": "clos-nw",
  "non_award_justification_codes": ["no-rece"],
  "tenders": [],
  "has_awarded_tender": false,
  "relationship_coverage_status": "complete"
}
```

### Completeness and limits

- Initial release covers eForms named graphs. Older legacy TED notices can produce metadata without relational enrichment.
- Search uses bounded `PAGE_NUMBER` pagination and the public input stays below TED's documented 15,000-result limit.
- Depending on `search_mode`, one or two Open Data enrichment families are queried. Each family uses strict batches of at most five zero-padded graph IRIs. Every batch uses explicit constant `GRAPH` blocks joined by `UNION`; variable-graph scans are not used.
- Every dataset array is deterministically capped at its documented schema maximum. A semantic cap emits `nested_array_cap_reached`; RDF tender, winner, or no-award caps also set `relationship_coverage_status=partial`, and collection completeness becomes false.
- The run Dataset is capped before billing. A pay-per-event spending limit may shorten only its immutable business-record prefix; all free evidence is retained after the paid balance reconciles.
- Requests are serialized, paced, retried at most three times for 429/5xx/transport failures, and limited to 25 attempts. `Retry-After` above 30 seconds is refused.
- Each body is bounded to 5 MB compressed and 10 MB decompressed; aggregate source bytes are bounded to 50 MB.
- A SPARQL binding cap withholds the terminal group because it might be incomplete. Earlier exact groups remain usable and the summary is marked incomplete.
- Negative money values such as sentinel `-1`, non-finite JSON, invalid official identities, conflicting lot relations, and multi-valued scalar relations are withheld and surfaced as issues.
- No HTML fallback, browser automation, arbitrary SPARQL, arbitrary expert query, inferred join, popularity ranking, contact enrichment, or personal-data profiling is performed.
- Exact publication-number retrieval is planned for a later version; this release selects notices through bounded date and procurement filters.

### Run locally

```bash
python -m venv .venv
.venv/bin/pip install -r requirements-dev.txt
.venv/bin/python -m pytest
```

Set `RUN_LIVE_SMOKE=1` to run opt-in tests against the two official endpoints. Network tests are skipped by default.

### Permissions and privacy

The Actor requires `LIMITED_PERMISSIONS`, uses only public procurement data, and needs no API token. It writes the immutable delivery plan to the run's default key-value store and the reconciled result to the run's default Dataset; it does not access unrelated account storage. Buyer or winner organization names can identify a sole trader or natural person in some notices even though this Actor does not collect contact details. Users must establish a lawful purpose and appropriate access, retention, deletion, and rights-request handling for both run storages and their exports. See [SECURITY.md](SECURITY.md) for the trust boundary, source-rights limits, and reporting guidance.

# Actor input Schema

## `search_mode` (type: `string`):

Collect open/competition opportunities, result notices, or both.

## `lookback_days` (type: `integer`):

Inclusive rolling publication window ending yesterday, matching the normal TED Open Data publication lag. A paired explicit date window takes precedence.

## `published_after` (type: `string`):

Optional inclusive YYYY-MM-DD start. Must be supplied with Published before; paired explicit dates take precedence over Lookback days.

## `published_before` (type: `string`):

Optional inclusive YYYY-MM-DD end. The explicit window can span at most 366 days.

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

Optional exact three-letter TED/EU country codes, for example DEU or FRA.

## `cpv_codes` (type: `array`):

Optional exact eight-digit Common Procurement Vocabulary codes.

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

Optional bounded full-text terms. Operators are escaped; raw TED expert queries are not accepted.

## `scope` (type: `string`):

ACTIVE follows TED's active-version scope. LATEST or ALL may include notices that are no longer active.

## `only_latest_versions` (type: `boolean`):

Keep enabled for normal discovery. Disable only when version history is required.

## `preferred_language` (type: `string`):

Lowercase ISO 639-1 or ISO 639-2 language code used for multilingual source fields. Two- and three-letter TED-language aliases are normalized consistently. Falls back deterministically to English, then the first available language.

## `page_size` (type: `integer`):

Bounded below TED's field-per-page limit.

## `max_notices` (type: `integer`):

Hard cap on notice versions selected by Search before one or two RDF enrichment families, each queried in fixed-graph batches of at most five notices.

## `max_lot_records` (type: `integer`):

Hard cap on exact RDF lot records retained in the dataset.

## `max_award_records` (type: `integer`):

Hard cap on exact RDF LotAwardOutcome records, including no-award outcomes.

## `max_change_records` (type: `integer`):

Hard cap on self-contained notice change records.

## `max_issue_records` (type: `integer`):

Hard cap on machine-readable source and relationship qualification records.

## `max_output_records` (type: `integer`):

Final bound across summaries, notice metadata, lots, outcomes, changes, issues, receipts, and run metadata. It must be large enough to retain all planned receipts.

## Actor input object example

```json
{
  "search_mode": "opportunities",
  "lookback_days": 7,
  "scope": "ACTIVE",
  "only_latest_versions": true,
  "preferred_language": "en",
  "page_size": 5,
  "max_notices": 5,
  "max_lot_records": 500,
  "max_award_records": 500,
  "max_change_records": 100,
  "max_issue_records": 200,
  "max_output_records": 2000
}
```

# Actor output Schema

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

Structured records in the run's default dataset. Chargeable procurement records form the leading prefix; free collection\_summary, issue, receipt, and run\_meta evidence follows, with run\_meta last.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("8tp/eu-ted-tender-lot-award-collector").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("8tp/eu-ted-tender-lot-award-collector").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 '{}' |
apify call 8tp/eu-ted-tender-lot-award-collector --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,8tp/eu-ted-tender-lot-award-collector"
        }
    }
}

```

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/xP7gFpd5WIdXG6hMB/builds/uacI1cP8st6iQXqM5/openapi.json
