# TED Tender Monitor — EU Procurement Alerts (`uplifted_novice_vbl/ted-tender-watch`) Actor

TED tender monitor for new or changed EU public procurement notices. Filter by CPV, country, keyword, minimum value, or notice type and receive EU tender alerts as JSON or signed webhooks. Unofficial and not affiliated with TED, the EU, or its Publications Office.

- **URL**: https://apify.com/uplifted\_novice\_vbl/ted-tender-watch.md
- **Developed by:** [Telemark Digital](https://apify.com/uplifted_novice_vbl) (community)
- **Categories:** Automation, Lead generation, News
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 new tenders

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

**TED Tender Monitor - EU Procurement Alerts monitors official TED notices on a schedule.** It checks the official TED Search API and delivers only new or changed notices matching CPV, country, keyword, minimum known EUR value, or notice type as structured JSON and optional HMAC-signed webhooks.

> **Independent service notice:** This is an unofficial, independent community Actor. It is not affiliated with, endorsed by, or operated by TED, the European Union, or the Publications Office of the European Union.

### What it does

- Filter EU tenders by CPV prefix, buyer country, keyword, minimum known EUR value, and TED notice type.
- Receive new and changed notices as structured JSON in an Apify dataset and, optionally, as one HMAC-signed webhook per run.
- Pay only for delivered new or changed notices. Sample runs, the first prime run, duplicates, skipped records, quiet days, and failures before durable dataset/state commit trigger no tender event charge.

#### 30-second quickstart

1. Leave **Free sample mode** on and click **Start**. You will receive up to ten of the newest matching competition notices, with no state and no tender charge. The preview stops after three TED pages or 120 seconds.
2. Turn sample mode off and run the same input once. This free **prime run** records the current matching publication numbers but delivers no tenders, preventing a historical flood.
3. Save the configuration as an Apify Task and schedule it. Add a signed webhook if your CRM, agent, or automation needs push delivery.

### Why most TED monitors silently lose tenders

TED's `publication-date` has day-level granularity. Thousands of notices can share one value, so `SORT BY publication-date DESC` plus page-number pagination does not define a stable order. Repeating the same page can change its membership while a new Official Journal release arrives; page-number mode also documents that cross-page consistency is not guaranteed.

This Actor avoids that trap:

1. It queries a publication-date overlap window but sorts by deterministic `publication-number DESC`.
2. It uses TED's point-in-time-consistent `ITERATION` pagination until the result set is exhausted.
3. It normalizes and sorts deliveries by `(publicationDate ASC, publicationNumber ASC within that date)`.
4. It deduplicates on universal `publication-number`, including pre-eForms notices that have no UUID.
5. It journals each attempted dataset record, commits seen state only after the complete dataset write, and verifies an interrupted run's actual dataset before recovering IDs. Unconfirmed records remain eligible for delivery and billing is never retried for an already committed ID.

#### Replayable correctness recipe

- Run a new non-sample filter once: the dataset contains a `prime` record and zero tender deliveries.
- Run the identical filter again without a new TED release: the summary reports zero new tenders and the run costs $0 in tender events.
- Change a state-defining filter such as CPV, country, keyword, minimum value, or notice type: the canonical state key changes and the new filter primes separately.

For an independent API check, send the same keyless TED Search request twice with `SORT BY publication-number DESC`; the ordered publication numbers remain stable. Do not use date-only page 1 as a cursor.

### Choosing the right workflow

- Use official TED alerts when a person wants procurement notices delivered to a human inbox.
- Use procurement enrichment platforms when the workflow needs analytics, curation, or contractual SLAs.
- Use this Actor when an Apify workflow needs stateful new-or-changed monitoring with machine-readable datasets and webhooks.

This is a tender monitor, not an award analytics product, translation service, multi-source portal, or bid-scoring tool.

### Input reference

| Input | Default | Meaning |
|---|---:|---|
| `sampleMode` | `true` | Return up to ten newest matches within a three-page, 120-second preview; write no state; charge no tender events. Set explicitly to `false` for normal stateful monitoring. |
| `cpvCodes` | `[]` | CPV prefixes, 2–8 digits. `72` covers IT; `45` covers construction. Empty means all sectors. |
| `countries` | `[]` | Buyer countries as ISO alpha-3 codes such as `DEU`, `FRA`, `ESP`. |
| `keywords` | `[]` | OR-matched full-text words or phrases. Terms are safely quoted. |
| `minValueEur` | `0` | Minimum for known procedure-level EUR values. Unknown or non-EUR values still pass with `valueUnknown=true`. No FX conversion is performed. |
| `noticeTypes` | competition types | One or more accepted TED codes listed below. |
| `includeChangeNotices` | `true` | Include modern linked changes and automatically add `can-modif` plus legacy `corr` to the TED query. |
| `lookbackDays` | `3` | Minimum overlap. Effective lookback automatically expands to the actual run gap plus one day. |
| `maxNewPerRun` | `5000` | Delivery/billing ceiling from 1–10,000. Excess matches are explicitly deferred oldest-first. |
| `webhookUrl` | unset | Optional HTTP(S) destination; must be supplied with `webhookSecret`. |
| `webhookSecret` | unset | Secret used for `X-Watch-Signature`. |
| `stateKey` | Task + filter hash | Advanced override. Giving multiple Tasks the same value deliberately makes them share state. |

Accepted current notice types are `brin-ecs`, `brin-eeig`, `can-desg`, `can-modif`, `can-social`, `can-standard`, `can-tran`, `cn-desg`, `cn-social`, `cn-standard`, `compl`, `pin-buyer`, `pin-cfc-social`, `pin-cfc-standard`, `pin-only`, `pin-rtl`, `pin-tran`, `pmc`, `qu-sy`, `subco`, and `veat`. Live legacy code `corr` is also accepted.

The default state key is SHA-256 over the Apify Task identity and canonical sorted `cpvCodes`, `countries`, `keywords`, `minValueEur`, and `noticeTypes`. Ad-hoc runs use the Apify user identity as their scope. Separate Tasks therefore keep independent history even when their filters are identical. An explicit `stateKey` deliberately overrides this isolation so multiple Tasks can share state. Delivery-only settings do not silently fork historical state. Suppressed changes are not marked seen, so enabling change delivery later can surface them once. Runs sharing one state key are serialized with an Apify Request Queue lease; an overlapping run exits before delivery or billing.

### Output schema

The default dataset contains three explicit record types:

- `tender`: one delivered new or changed notice.
- `prime`: the required free first-run information record.
- `summary`: counts and overflow/budget flags for sample and monitoring runs.

Representative tender record:

```json
{
  "recordType": "tender",
  "publicationNumber": "488405-2026",
  "noticeIdentifier": "69d7fb9a-e58b-4f91-a2fd-7d59312fde31",
  "publicationDate": "2026-07-15",
  "noticeType": "cn-standard",
  "isChangeNotice": false,
  "modifiesPublicationNumber": null,
  "title": "Spain – Repair and maintenance services of electrical building installations – …",
  "titleLanguage": "eng",
  "buyerName": "Reus Mobilitat i Serveis SA",
  "buyerCountry": "ESP",
  "cpvCodes": ["50711000"],
  "estimatedValueEur": null,
  "valueUnknown": true,
  "deadlineDate": "2026-09-14",
  "links": {
    "html": "https://ted.europa.eu/en/notice/-/detail/488405-2026",
    "pdf": "https://ted.europa.eu/en/notice/488405-2026/pdf",
    "xml": "https://ted.europa.eu/en/notice/488405-2026/xml"
  },
  "isNew": true,
  "firstSeenAt": "2026-07-16T09:00:00.000Z",
  "stateKey": "sha256:…"
}
```

Title selection is deterministic: `eng` when present, otherwise the alphabetically first available ISO-639-2 language key. CPVs are a deduplicated notice/lot union. The value is procedure-level only; lot values are never summed. The earliest valid lot deadline is exposed. Missing fields remain `null` and are never invented.

### Pricing and billing honesty

- `new-tender`: **$0.005** per newly delivered notice.
- `changed-tender`: **$0.005** per delivered correction or modification.
- Actor start: **no start event is configured**; platform usage is included rather than added to the event price.
- Sample, prime, duplicate, malformed, skipped, and quiet paths: **$0 tender event charge**. Failures before durable dataset/state commit also trigger no tender event charge.

Charging happens immediately after filtering, deduplication, and durable dataset/state delivery. It is the first post-commit operation, before final summary persistence, webhook retries, lease release, or cleanup. The Actor reads the run's maximum-charge limit and defers work that cannot fit. A rare platform charge failure after delivery is never retried for the same publication number; preserving “never billed twice” takes priority over developer revenue.

The summary row written with the dataset records the attempted `new-tender` and `changed-tender` counts with `chargeStatus: "pending"`, because dataset delivery necessarily precedes billing. The final `OUTPUT` key-value-store summary records both attempted and actual charged counts and sets `chargeStatus` to `complete`, `partial`, or `failed`. Sample and quiet paths report zero actual tender charges.

A corrigendum is new information and is billed as its own `changed-tender` event. Therefore one procurement can produce up to two charges: the original `new-tender`, then its later correction. This is deliberate and visible in `isChangeNotice`.

### Scheduling, signed webhooks, and integrations

Save a primed input as an Apify Task, then choose the cadence that matches your filter. Narrow CPV/country filters can run hourly or daily. Broad EU-wide filters should run daily or increase `maxNewPerRun`; persistent filters whose protected in-window seen-set alone would exceed 150,000 IDs fail explicitly instead of evicting IDs and rebilling them.

When configured, the webhook body is the JSON array of tender records only. Headers:

- `X-Watch-Signature`: lowercase hex HMAC-SHA256 of the exact body using `webhookSecret`.
- `X-Watch-Run-Id`: the Apify run ID.

Webhook delivery makes one initial attempt plus three retries with backoff. Every request has a 15-second timeout and runs after durable dataset/state commit and the single non-retried billing attempt. A slow or failed webhook therefore cannot delay or suppress billing, remove dataset results, or repeat a charge.

Connect the dataset or run event through Apify integrations to Make, Zapier, n8n, Slack, cloud storage, or your own API. AI agents can discover this Actor through the Apify MCP server using Store search, inspect its schema, add it as a tool, and call it; it is not automatically preinstalled in every agent.

#### Run through the Apify API

Use an Authorization header so your token is not placed in a URL or log:

```bash
curl -X POST "https://api.apify.com/v2/acts/uplifted_novice_vbl~ted-tender-watch/runs" \
  -H "Authorization: Bearer YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"sampleMode":true,"cpvCodes":["72"],"countries":["DEU"]}'
```

The run response links to its default dataset. For scheduled monitoring, use the same JSON with `sampleMode=false`, run it once to prime, then save it as an Apify Task and attach a schedule.

### Change notices

Modern eForms changes are detected from `change-notice-version-identifier` or `modification-previous-notice-identifier`. TED also still serves legacy `notice-type=corr` records whose linkage fields can be empty, so `corr` is treated as a change for compatibility. `modifiesPublicationNumber` is set only when a linkage value is actually a TED publication number; UUID/version identifiers are not mislabeled.

If `includeChangeNotices=false`, change records are neither delivered nor added to the seen-set. Enabling the option later can deliver a previously suppressed change once.

### Limits, errors, and FAQ

#### What data source is used?

Only the official keyless TED API v3 Search endpoint. There is no HTML scraping fallback, no login, and no third-party tender source.

#### What happens during TED rate limits or outages?

The client self-caps at two requests per second. HTTP 429 and 5xx responses retry after approximately 1, 4, and 16 seconds. Persistent failure ends cleanly before state advancement or tender charges.

#### Can a malformed notice stop the batch?

No. A notice missing the universal publication number or publication date is logged, skipped, and never charged. An unknown stored state schema does stop the run with a clear error; state is never wiped or guessed.

#### Does minimum value convert currencies?

No. Current eForms procedure values come from BT-27 fields; older global aliases are a fallback. Only an unambiguous EUR procedure value is compared. Missing, ambiguous, and non-EUR values remain deliverable as unknown so a potentially relevant opportunity is not silently lost.

#### What is not included in v1?

AI relevance scoring, summaries, translation, per-lot output, email formatting, award analytics, historical backfill, national portals, currency conversion, a custom MCP server, and a separate dashboard are intentionally out of scope.

#### Where can I get support?

Open the Actor's **Issues** tab in Apify Console with the run ID, input with secrets removed, and the relevant log excerpt. Do not post webhook secrets or Apify tokens.

European teams may search for the same job using terms such as **przetargi**, **appalti**, **ausschreibung**, or **licitaciones**. The Actor is built for **TED Europa**, **eForms**, **CPV**, **EU tenders**, procurement pipelines, a **tender alert**, and a stateful **tender monitor**.

### Changelog

#### 0.1 — 2026-07-16

Initial publish-ready release: deterministic ITERATION client, stateful new-only delivery, current eForms and legacy mapping, signed webhooks, honest PPE events, complete Apify schemas, and recovery-aware deduplication.

# Actor input Schema

## `sampleMode` (type: `boolean`):

Returns up to 10 of the newest matching TED notices through a bounded three-page, 120-second preview. Writes no monitor state and triggers no tender charges. Turn this off when you are ready to prime a saved monitor.

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

Optional 2–8 digit Common Procurement Vocabulary prefixes, for example 72 for IT or 45 for construction. Empty means every sector.

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

Optional ISO-3166 alpha-3 buyer country codes such as DEU, FRA, ESP. Empty means all countries.

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

Optional full-text terms, OR-matched by TED. Empty means no keyword clause. Each term is safely quoted in the expert query.

## `minValueEur` (type: `integer`):

Client-side minimum for known procedure-level EUR values. Notices with missing, ambiguous, or non-EUR values still pass with valueUnknown=true; this Actor never performs FX conversion or silently drops unknown values.

## `noticeTypes` (type: `array`):

Official TED notice-type codes. Defaults to competition notices. Legacy corr is accepted because it remains present in the live Search API.

## `includeChangeNotices` (type: `boolean`):

When enabled, the TED query automatically includes can-modif and legacy corr alongside your selected types, and delivers linked changes as separately billed changed-tender events.

## `lookbackDays` (type: `integer`):

Overlap floor for late publication. The Actor automatically extends this to the real gap since the last successful run, plus one safety day, so weekly and fortnightly schedules remain covered.

## `maxNewPerRun` (type: `integer`):

Billing and delivery ceiling. Overflow is explicit and deferred oldest-first without advancing past undelivered notices. Broad EU-wide filters should run daily or use a higher cap.

## `webhookUrl` (type: `string`):

Optional HTTP(S) endpoint. Receives one JSON array of delivered tender records after durable dataset and state delivery, with one initial attempt plus three bounded retries. Requires webhookSecret.

## `webhookSecret` (type: `string`):

Required with webhookUrl. Used for the X-Watch-Signature HMAC-SHA256 header. Stored as a secret input value.

## `stateKey` (type: `string`):

Optional advanced override for deliberately sharing monitor state. By default, state is isolated per Apify Task and filter set; changing filters starts a fresh prime run. Supplying the same stateKey to multiple Tasks intentionally overrides Task isolation.

## Actor input object example

```json
{
  "sampleMode": true,
  "cpvCodes": [],
  "countries": [],
  "keywords": [],
  "minValueEur": 0,
  "noticeTypes": [
    "cn-standard",
    "cn-social",
    "cn-desg"
  ],
  "includeChangeNotices": true,
  "lookbackDays": 3,
  "maxNewPerRun": 5000
}
```

# Actor output Schema

## `items` (type: `string`):

Tender, prime, and summary records in the default dataset.

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

Machine-readable summary or prime record.

# 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 = {
    "sampleMode": true,
    "cpvCodes": [],
    "countries": [],
    "keywords": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("uplifted_novice_vbl/ted-tender-watch").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 = {
    "sampleMode": True,
    "cpvCodes": [],
    "countries": [],
    "keywords": [],
}

# Run the Actor and wait for it to finish
run = client.actor("uplifted_novice_vbl/ted-tender-watch").call(run_input=run_input)

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

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

```

## CLI example

```bash
echo '{
  "sampleMode": true,
  "cpvCodes": [],
  "countries": [],
  "keywords": []
}' |
apify call uplifted_novice_vbl/ted-tender-watch --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=uplifted_novice_vbl/ted-tender-watch",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/gBhCg1VukQ2CZ7LQI/builds/bAWHzeH1D0TpLzSx4/openapi.json
