# TED Contract Expiry Radar - Recompete Leads (`datagrit/ted-contract-expiry-radar`) Actor

Find EU public contracts approaching expiry from TED award notices: incumbent, buyer, value, end date and renewal options.

- **URL**: https://apify.com/datagrit/ted-contract-expiry-radar.md
- **Developed by:** [datagrit](https://apify.com/datagrit) (community)
- **Categories:** Lead generation, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

### What does TED Contract Expiry Radar - Recompete Leads do?

TED Contract Expiry Radar turns contract award notices from the official EU tender portal TED into a list of public contracts that are about to end across Europe. For every lot it returns the incumbent supplier, the buying authority and its country, the award value, the contract end date, the days left, contract length, renewal options and CPV codes. You choose the expiry window, for example the next 12 months, and optionally filter by keyword, buyer country, CPV code, supplier name and minimum value. Export the result as JSON, CSV or Excel, call it through the Apify API, or plug it into n8n, Make and AI agents through MCP.

### Who is it for?

- **Bid and sales teams** who want to know which public contracts in Europe come up for recompete in the next 6 to 18 months, before the new tender is published.
- **Competitor trackers** who follow a supplier by name and want the dates when its public contracts end.
- **Consultancies and agencies** that sell to a certain type of buyer and want a shortlist of incumbents and buyers to approach early.
- **Market analysts** who study contract length, renewal options and supplier concentration by sector and country.

### How to use it

1. Set the **expiry window** in months from today: 0 to 12 lists contracts ending within a year, 6 to 18 skips those that are already too close to tender.
2. Optionally add **keywords**, **buyer countries** (two-letter codes such as DE, FR, PL), **CPV prefixes** (for example `72` for IT services, `90` for waste and cleaning, `45` for construction), **incumbent names** and a **minimum value**.
3. Set **maximum results** and run the Actor.
4. Download the dataset or fetch it from the API.

Results are read in monthly expiry slices, so with a maximum results limit you get the soonest-expiring contracts first.

### Estimated end dates

Most award notices state how long the contract runs but not the exact end date. When **Estimate missing end dates** is on, the Actor computes the end date from the start date, or from the contract conclusion date, plus the published duration. The field `endDateSource` tells you where every date comes from: `stated` means the buyer published it, `start+duration` and `award+duration` mean it was calculated. Turn the option off to keep only stated end dates. Estimated dates ignore extensions and early termination, so treat them as a strong lead rather than a legal fact.

Why it matters: many award notices publish how long a contract runs but not the date it ends. The Actor computes that date from the lot start date or the contract conclusion date plus the duration, and marks every row with endDateSource. In two complete live runs on 30 September 2026, contracts ending within the next month gave 7,053 lots with a stated end date and 9,052 more with a computed one (16,105 rows), and contracts ending in two to three months gave 7,465 stated and 12,064 computed (19,529 rows). See the FAQ for what is not covered.

### Example output

| contractEnd | title | buyerName | incumbentName | awardValue | daysToExpiry |
|---|---|---|---|---|---|
| 2026-12-31 | Estonia – Software package and information systems | Example City Transport Authority | Example Systems AS | 480000 | 92 |

```json
{
  "id": "406127-2023/LOT-0001",
  "noticeNumber": "406127-2023",
  "lotId": "LOT-0001",
  "title": "Estonia – Software package and information systems – Ticketing software",
  "sourceUrl": "https://ted.europa.eu/en/notice/-/detail/406127-2023",
  "buyerName": "Example City Transport Authority",
  "buyerCountry": "EST",
  "incumbentName": "Example Systems AS",
  "awardValue": 480000,
  "currency": "EUR",
  "awardDate": "2023-10-16",
  "contractEnd": "2026-12-31",
  "endDateSource": "stated",
  "daysToExpiry": 92,
  "contractLengthMonths": 36,
  "renewalOptionsMax": 1,
  "frameworkAgreement": false,
  "cpvCode": "72000000",
  "lotCount": 2,
  "found": true
}
```

### What data do you get?

Each record is one awarded lot. It includes the TED notice number and link, notice and contract titles, buyer name, country and city, incumbent supplier with country, size class and registry identifier, all winners of the notice when they cannot be matched to a single lot, award value and currency, award, publication and contract start dates, contract end date with its source, days and months to expiry, contract length, renewal options and terms, framework agreement flag and type, main and additional CPV codes, contract nature and the number of lots in the notice.

### How much does it cost?

You pay per contract lot returned. Pricing depends on your Apify plan: a small fee when a run starts, then a price per result that is lower on paid plans. The Apify free plan includes monthly credit you can use to try it. You can set a maximum spend on the run: the Actor stops when the limit is reached. It reads the public TED search API over plain HTTP, so runs are fast and light on platform resources.

### Input

- **Keywords** – optional; TED full-text search finds candidate notices, and a lot is returned only when a keyword appears in its own title or description or in the contract or notice title (in the language of the notice, case-insensitive). Lots that only share a notice with a matching lot are not returned or charged.
- **Buyer countries** – optional; two-letter country codes of the contracting authority.
- **CPV code prefixes** – optional; keep lots whose main CPV code starts with a prefix. TED publishes a CPV code per lot; when it does not, the codes of the whole notice are used and the record shows `cpvScope: notice`.
- **Incumbent supplier names** – optional; keep contracts won by a supplier whose name contains a word.
- **Expires no earlier / no later than (months)** – the contract end-date window counted from today.
- **Minimum contract value** – skip awards below this amount, in the contract's own currency. Amounts are not converted, so a value in PLN or SEK is compared as a plain number.
- **Include framework agreements** – turn off to list only stand-alone contracts.
- **Estimate missing end dates** – on by default, see above.
- **Maximum results** – total limit for the run.

### Is it legal to use this data?

The Actor reads only information that the European Union publishes openly through the TED search API. It does not log in or bypass any access control. The data covers organisations and public contracts. You are responsible for using it in line with applicable law, including data protection rules. This description is not legal advice.

### FAQ

**Does it list contracts that are not yet awarded?** No. It covers awarded contracts and shows when each one ends. Use the expiry window to find recompete opportunities early.

**Why are some contracts missing?** Four reasons. First, a notice with neither an end date nor a duration cannot be placed in an expiry window, and lots without a selected winner are skipped. Second, contracts with a stated end date are always found, but for estimated end dates the Actor asks TED only about common durations: 3, 6, 9, 12, 15, 18, 24, 30, 36, 42, 48, 60, 72, 84, 96 and 120 months, and 1 to 8 and 10 years. Contracts published with another length (for example 4, 11 or 20 months) or with a duration in days or weeks are found only when they also state an end date. In a sample of 2023 notices this leaves out roughly 7 percent of those that publish a duration. Third, TED lists conclusion dates per signed contract, not per lot, and does not say which contract belongs to which lot. When a notice has several contracts with different conclusion dates, the Actor does not guess: awardDate and contractTitle stay empty, and the end date is computed only from the lot's own start date. This is the most common reason a notice found by the duration search gives no row; the run status says how many of those notices produced one. Fourth, TED publishes the per-lot fields (end dates, durations, titles, CPV codes, renewal terms) as lists that follow the notice's lot list, and it sometimes leaves entries out: one notice with 46 lots had 45 end dates. The Actor matches each awarded lot to its entry by the lot identifier and uses a list only when it has exactly one entry per lot; otherwise those fields stay empty and a lot that cannot be placed in the window is not returned, because guessing would attach another lot's date. TED lets a single query read at most 15,000 notices, and 11 of the 24 queries of the default 12-month window match more. The Actor splits such a query into shorter date windows on its own, so nothing is dropped. Only a single day with more than 15,000 notices would be cut, and then the run status says INCOMPLETE and names the day.

**How long does a run take and are there limits?** TED rate-limits its public search API, so the Actor sends about one request per second. Results are read in monthly expiry slices, two queries per month (stated dates, then estimated ones). A window of 12 months means up to 24 queries. Each query can span many pages of 100 notices, one request per page. In a measurement on 30 September 2026 the default 12-month window without filters matched about 251,000 notices (64,000 for stated dates, 187,000 for estimated ones), which is roughly 2,600 requests and about 45 minutes; a window of two to three months took about 8 minutes. Narrow the window, add a country or CPV prefix, or set a maximum number of results to finish faster. Most runs finish much sooner, because the Actor stops as soon as it reaches your maximum results on the first pages. TED matches notices, not single lots: a notice is returned when any of its lots fits the window, and the Actor then keeps only the lots that really fit, so many lots read are discarded. The run status reports how many lots had a stated, estimated or missing end date. The Actor stops as soon as it reaches your maximum results, and a run that is migrated to another server continues without charging twice for the results it already returned.

**The run failed with "none has a contract end date". What does it mean?** TED returned awarded lots but none of them carried an end date or duration, which points to a change in the source data rather than to an empty search. The run fails on purpose so that you are not told there are no expiring contracts. Try again later or open an issue.

**Which countries are covered?** All buyers that publish award notices on TED, that is the EU member states and several other European countries.

**Why is the incumbent empty for some lots?** When a notice has several lots and the winners cannot be matched to lots reliably, the Actor leaves the incumbent empty and lists all winners in `allIncumbents` instead of guessing.

**How current is the data?** Every run reads the live TED API. Notices appear on TED shortly after publication.

**Can I schedule it?** Yes, use Apify schedules or call the Actor from your own workflow.

**Something looks wrong.** Open an issue with the input you used; layout or API changes at the source are fixed quickly.

### Related Actors

Other public-data Actors from the same publisher are listed on the Store profile.

# Changelog

This Actor's version history is a separate document: https://apify.com/datagrit/ted-contract-expiry-radar/changelog.md

# Actor input Schema

## `queries` (type: `array`):

Optional keywords. TED full-text search finds candidate notices, and a lot is returned only when a keyword appears in its own title or description, or in the contract or notice title (case-insensitive, in the language of the notice). Leave empty to list all expiring contracts.

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

Optional. Two-letter country codes of the contracting authority, for example DE, FR, PL, NL, ES, IT. Leave empty for all countries covered by TED.

## `cpvPrefixes` (type: `array`):

Optional. Keep only contracts whose CPV classification starts with one of these digits, for example 72 (IT services), 90 (waste and cleaning) or 45 (construction).

## `incumbents` (type: `array`):

Optional. Keep only contracts won by suppliers whose name contains one of these words, for example to see when a competitor's contracts end.

## `expiresAfterMonths` (type: `integer`):

Start of the expiry window. 0 means contracts ending today or later.

## `expiresBeforeMonths` (type: `integer`):

End of the expiry window. 12 returns contracts ending within the next twelve months. Results are read in monthly slices, soonest expiry first.

## `minValue` (type: `number`):

Optional. Minimum awarded value in the contract's own currency (mostly EUR). Contracts without a published value are excluded when this is above 0.

## `includeFrameworkAgreements` (type: `boolean`):

Turn off to drop framework agreements and keep only stand-alone contracts.

## `estimateMissingEndDates` (type: `boolean`):

Most notices state a contract duration but no end date. When on, the end date is computed from start date (or award date) plus duration and marked in endDateSource. Turn off to return only end dates stated by the buyer.

## `maxItems` (type: `integer`):

Stop after this many contracts.

## `proxyConfiguration` (type: `object`):

Optional proxy. The TED API is public and needs no proxy; leave disabled.

## Actor input object example

```json
{
  "queries": [
    "software"
  ],
  "countries": [],
  "cpvPrefixes": [],
  "incumbents": [],
  "expiresAfterMonths": 0,
  "expiresBeforeMonths": 12,
  "minValue": 0,
  "includeFrameworkAgreements": true,
  "estimateMissingEndDates": true,
  "maxItems": 20,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

All extracted records as a dataset.

# 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 = {
    "queries": [
        "software"
    ],
    "countries": [],
    "cpvPrefixes": [],
    "incumbents": [],
    "expiresAfterMonths": 0,
    "expiresBeforeMonths": 12,
    "minValue": 0,
    "includeFrameworkAgreements": true,
    "estimateMissingEndDates": true,
    "maxItems": 20,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("datagrit/ted-contract-expiry-radar").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 = {
    "queries": ["software"],
    "countries": [],
    "cpvPrefixes": [],
    "incumbents": [],
    "expiresAfterMonths": 0,
    "expiresBeforeMonths": 12,
    "minValue": 0,
    "includeFrameworkAgreements": True,
    "estimateMissingEndDates": True,
    "maxItems": 20,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("datagrit/ted-contract-expiry-radar").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 '{
  "queries": [
    "software"
  ],
  "countries": [],
  "cpvPrefixes": [],
  "incumbents": [],
  "expiresAfterMonths": 0,
  "expiresBeforeMonths": 12,
  "minValue": 0,
  "includeFrameworkAgreements": true,
  "estimateMissingEndDates": true,
  "maxItems": 20,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call datagrit/ted-contract-expiry-radar --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,datagrit/ted-contract-expiry-radar"
        }
    }
}
```

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/ovM1Lpmc8aBTosgh1/builds/XLQ0qfsVekfhSB7Jh/openapi.json
