# Government Tender & Contract Scraper (`eiv/tender-scraper`) Actor

Public sector tenders from TED (EU) and both UK procurement portals in one feed. Filter by CPV code, country, value and deadline, separate open opportunities from already-awarded contracts, and get the buying officer's name, email and phone where the portal publishes them.

- **URL**: https://apify.com/eiv/tender-scraper.md
- **Developed by:** [Eimantas V](https://apify.com/eiv) (community)
- **Categories:** Lead generation, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 notice founds

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

## Government Tender & Contract Scraper

Public sector contracts from **TED (EU)** and **both UK procurement portals** in one feed — filtered by CPV code, country, value and deadline, with the buying officer's name, email and phone where the portal publishes them.

```
UK Find a Tender      40 notices   tender:2   contract:38   40 contacts
TED (EU)              60 notices   tender:36  award:23      667 available
UK Contracts Finder    4 notices   award:4                   4 contacts

d0   £250,000  External Audit Services          North London Muslim Housing  moosa@nlmha.com
d24   £50,000  Queens Park Primary Fascias      St Helens Council            procurement@star-procurement.gov.uk
d31  £750,000  Learner Management System        Defence Equipment & Support  alexander.tebbitt885@mod.gov.uk
```

No API key, no registration, no browser. All three are public government APIs.

***

### A named buyer you can actually email

The UK portals publish the officer running each tender — name, email address and direct phone — attached to a live opportunity with a deadline and a budget. **58 of 58** notices in one run carried an email.

That is the difference between a list of contracts and a list of people to call. Tender alert services charge £500–5,000 a year and mostly hand you the notice.

***

### Only about a third of a tender feed is a tender

Every one of these portals mixes four kinds of notice into one stream:

| Type | What it is |
|---|---|
| **`tender`** | An open opportunity you can bid on |
| `award` | Somebody already won it |
| `contract` | A signed agreement |
| `planning` | An early heads-up, no deadline yet |

A live run returned 104 notices of which **38 were open tenders** — the rest were already decided. Set `noticeTypes: ["tender"]` and the list becomes doors that are still open. Leave it empty and you get the awards too, which is how you see who is winning work in your category.

***

### What you get

**Per notice** — `title`, `buyerName`, **`contactName` / `contactEmail` / `contactPhone`**, `valueAmount` + `valueCurrency`, `cpvCodes` + `cpvDescription`, `deadline`, **`daysUntilDeadline`**, `procedureId`, `noticeType`, `procurementMethod` + `procurementMethodDetails`, `status`, `region`, `buyerCountry`, `noticeUrl`, `noticeId`.

**Per source** — notices returned, `totalAvailable`, counts by notice type, how many are open, how many close within a week, how many carry a contact, median value, top buyers, top CPV codes.

Rows arrive **soonest deadline first within each source**, then largest value, with undated notices last — an unknown date is not urgency. Sources are written as they finish, so sort the whole dataset on `daysUntilDeadline` if you want one list across all three; the dataset view does it in a click.

***

### Who this is for

- **Anyone selling to the public sector** — construction, IT, consulting, facilities, medical, training.
- **Bid teams** — a daily run with `noticeTypes: ["tender"]` and your CPV codes is your pipeline.
- **Market intelligence** — `noticeTypes: ["award"]` shows who is winning what, at what price.
- **Lead generation** — filter on `requireContactDetails` and you have named buyers with live budgets.

***

### Input

```json
{
  "cpvCodes": ["48", "72"],
  "noticeTypes": ["tender"],
  "countries": ["GB", "IE"],
  "publishedFrom": "2026-08-07",
  "onlyOpen": true,
  "requireContactDetails": true
}
```

| Option | Default | Notes |
|---|---|---|
| `sources` | all three | ted, uk-contracts-finder, uk-find-a-tender |
| `cpvCodes` | — | A division is enough: 48 software, 45 construction, 72 IT services |
| `noticeTypes` | all | Set to `tender` for open opportunities only |
| `countries` | all | Mainly narrows TED; the UK portals are UK-only |
| `onlyOpen` | `false` | Drops passed deadlines, keeps undated notices |
| `requireContactDetails` | `false` | Effectively narrows to the UK portals |
| `minValue` / `maxValue` | — | Undisclosed budgets are kept |
| `publishedFrom` | 7 days ago | See below — this is what makes a run recent |
| `maxPerSource` | `250` | TED alone can match hundreds of thousands |

***

### Ten things worth knowing

Each was found by running against live data.

**TED cannot be sorted, so the date floor is doing the work.** It rejects a `sortField` outright, and without a date filter the very first result came back from **2016**. Every run therefore carries `publication-date >=`, defaulting to a week ago. Widen the window rather than expecting a "newest first" option that does not exist.

**A short CPV code is rejected by TED and required by everyone else.** Asking TED for `48` returns *"Value '48' is not supported for search field 'classification-cpv'"* and nothing else. Codes are padded to eight digits for TED — `48` becomes `48000000`, which matches the whole software division — and matched as prefixes when filtering the UK results.

**Find a Tender hides its CPV codes.** Contracts Finder puts the classification on the tender where you would expect it; Find a Tender buries it in the first item's `additionalClassifications`, and only some notices carry one at all. Reading a single location left every Find a Tender row unclassified, which meant a CPV-filtered search quietly returned rows nothing had checked.

**The contact has to be matched by role.** An OCDS release lists suppliers and funders in the same `parties` array as the buyer. Taking the first party with an email address would hand you a supplier's inbox labelled as the buying officer, so the buyer is matched by id and then by role.

**TED's own fields need three separate corrections.** Its publication dates arrive as `2026-08-10+02:00` — a date carrying a UTC offset but no time, which `Date.parse` reads as invalid, so every notice came back undated until the missing midnight was filled in. The amount and its currency live in different fields, so reading `total-value` alone gave figures with no unit attached and a summary that could not add anything up. And a CPV code is repeated once per lot, so a four-lot notice lists the same two codes four times — left in, the duplicates more than doubled the top-CPV counts on the summary.

**Every UK notice link was broken, and nothing could have told you.** Both portals were sent to Contracts Finder with the ocid as the path. They use different OCDS prefixes, so a Find a Tender ocid means nothing to Contracts Finder, and neither portal addresses a notice by ocid in the first place. What makes it worth spelling out is the failure mode: both answer a bad notice path with **HTTP 200** and a "You have been signed out" or "Page not found" page, so every link looked healthy and went nowhere. Both key on `release.id` instead — used whole for Find a Tender (`077478-2026`), with the trailing sequence removed for Contracts Finder (`d1d7de05-…-910704`). One rule could not serve both, because stripping trailing digits from a Find a Tender id eats the year.

**Contracts Finder accepted a page number and ignored it.** Pages 1, 2 and 3 of the same search returned byte-identical releases, so asking for 250 notices fetched the first hundred three times, threw two hundred away as duplicates, and then reported `truncated: true` off a count padded with repeats. The portal looked exhausted at 96 notices. It pages by the `links.next` cursor instead, which reaches 381 in four pages — the same way Find a Tender already worked.

**A corrected notice is a new notice, and only the procedure identifier says so.** TED republishes a corrigendum under a fresh publication number; OCDS issues another release against the same ocid. Both carry an identifier for the procedure itself, and across 1,500 live notices that grouped 22 sets of republications where matching on buyer, title, deadline and value found 8. The fourteen it adds are the ones that matter, because a corrigendum exists precisely when something changed: twelve differed on value, three on title, two on the deadline. One moved a closing date from 8 June to 16 June — as two rows, the superseded date reads as a live one. The latest publication wins, and it is chosen before the filters run, so a deadline that moved is judged on the date that applies rather than the one it replaced.

**A TED title is not the buyer's title.** Every one is assembled as `{country} – {CPV description} – {real title}`, and TED translates only the first two segments — the buyer's own words are never translated at all. Three consequences, all of which were live in the output. Searching `software` matched **39 notices where only 15 were about software**: the other 24 were cabling, helpdesk and information-security contracts filed under CPV 72, whose generated label reads "IT services: consulting, software development, Internet and support". `titleLanguage` claimed English on **230 of 250 notices** that were actually German, French or Dutch — and reported it as `eng` where the UK rows said `en`, so even the answer was in two vocabularies. And `cpvDescription` was empty on every row while its value sat in the middle of the title. The title now comes from `title-proc`, which carries the buyer's string alone and the language it is genuinely in; the label it was wrapped in becomes `cpvDescription`.

**Two columns quietly meant three different things depending on the source.** `procurementMethod` carried TED's `notice-type` — `cn-standard` on all 104 rows of a run, because that is the *form* a contract notice is published on, not how it will be awarded. The procedure is a separate TED field, and reading it gives the distribution that was always there: 74 open, 21 negotiated with a call, 5 restricted. The UK side had its own version of the fault, reading OCDS's free-text `procurementMethodDetails` into the coded column and falling back to `mainProcurementCategory` — which is goods, services or works, a different question, and one `contractNature` already answers. Both now carry the four-word OCDS codelist, with each portal's own wording kept beside it in `procurementMethodDetails`. `buyerCountry` was the same disease in three spellings: `DEU` from TED, `United Kingdom` from OCDS, `DE` in the summary and in the `countries` input. Filtering the dataset on the code the documentation asks for returned nothing.

***

### Output

```json
{
  "recordType": "tender",
  "source": "uk-find-a-tender",
  "title": "Provision of a Learner Management System",
  "buyerName": "Defence Equipment and Support",
  "contactName": "Alexander Tebbitt",
  "contactEmail": "alexander.tebbitt885@mod.gov.uk",
  "valueAmount": 750000, "valueCurrency": "GBP",
  "cpvCodes": ["48000000"],
  "cpvDescription": "Software package and information systems",
  "noticeType": "tender",
  "procurementMethod": "open",
  "procurementMethodDetails": "Open procedure",
  "buyerCountry": "GB",
  "deadline": "2026-09-14T12:00:00.000Z",
  "daysUntilDeadline": 31
}
```

Three ready-made views: **Open opportunities**, **Buyer contacts** and **Source summary**. Set `flattenOutput: true` for CSV.

***

### Honest limits

- **EU and UK only.** TED covers the EU plus Norway, Switzerland and the Western Balkans. There is no US coverage — SAM.gov requires a registered API key.
- **Value is often not published.** Buyers disclose a budget when they choose to; `valueAmount` is null otherwise, and those notices are kept rather than filtered away.
- **TED publishes no region and no status.** Its API exposes a country of performance but no NUTS code, and stuffing a country into a column the UK rows fill with "East Midlands" would repeat exactly the mistake described above. Both stay null on TED rows.
- **TED carries no contact email.** Only the UK portals name an officer, so `requireContactDetails` narrows a run to them.
- **Deadlines are missing on award and contract notices**, because the deadline has already passed. That is what `noticeType` is for.
- **`totalValue` and `medianValue` are only reported when every notice shares one currency.** Adding euros to pounds produces a number that means nothing, and so does taking the midpoint of them — one run mixed 2,247,180 RON with 915,200 EUR. `valueCurrencies` tells you when that is why they are null.
- **One opportunity is charged once.** Every publication of one procurement procedure collapses onto the latest, matched on the identifier the sources themselves use. A UK tender above threshold also reaches both portals under two unrelated ids, and those are matched on buyer, title, deadline and value.
- **Failed sources and filtered notices are never charged either.**

***

### Pricing

| Event | Price | When |
|---|---|---|
| Actor start | $0.005 | Once per run |
| Source searched | $0.008 | Per portal, including its summary |
| Notice found | $0.0012 | Per notice returned |
| Buyer contact found | $0.004 | Only when an email is present |

**$1.20 per 1,000 notices**, plus $4 per 1,000 contacts. A daily UK watch on two CPV divisions costs roughly **$0.30 a run**. Tenders Direct and BiP start in the hundreds of pounds a year; GovWin runs to thousands.

***

### Tips

- **Run daily with `publishedFrom` set to yesterday** and diff on `procedureId`, not `noticeId`. A corrected notice gets a fresh notice number but keeps its procedure id, so diffing on the number reports the same opportunity as new.
- **Sort by `daysUntilDeadline`.** Anything under 7 days needs a decision today; the summary counts those as `closingWithin7Days`.
- **Use `noticeTypes: ["award"]` on your competitors' CPV codes** to see what they are winning and at what value.
- **Two CPV divisions beat ten keywords.** Buyers classify before they write the title, so the code catches tenders whose wording you would never have guessed.
- **Feed `contactEmail` into your CRM** — a named officer with a live budget and a deadline is a warmer lead than anything a list broker sells.

# Actor input Schema

## `sources` (type: `array`):

Leave empty for all three. ted covers the EU and 30+ countries; uk-contracts-finder covers UK public sector including below-threshold work; uk-find-a-tender covers UK high-value notices.

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

Any one matching is enough. Searched in the title, description and buyer name. Leave empty to rely on CPV codes alone.

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

Common Procurement Vocabulary — how buyers actually file a tender. A division is enough: 48 is software, 45 construction, 72 IT services, 85 health, 79 business services. Short codes are padded to eight digits for TED, which rejects them otherwise, and matched as prefixes everywhere else.

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

ISO codes such as DE, FR, GB, PL. Leave empty for all. TED is told directly; the UK portals are UK-only, so this mainly narrows TED.

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

tender is an open opportunity, award names a winner, contract is a signed agreement, planning is an early heads-up. Leave empty for all four. Set it to tender if you are looking for work to bid on — the portals mix all four into one stream, and in a typical run only about a third are open tenders.

## `onlyOpen` (type: `boolean`):

Drop anything whose deadline has already passed. Notices with no published deadline are kept — no date is not evidence of a closed door.

## `requireContactDetails` (type: `boolean`):

Keep only notices naming someone you can write to. TED does not publish contact emails, so this narrows a run to the UK portals in practice.

## `minValue` (type: `integer`):

In the notice's own currency. Notices that publish no value are kept, because an undisclosed budget is not a small one.

## `maxValue` (type: `integer`):

In the notice's own currency. 0 disables.

## `publishedFrom` (type: `string`):

Defaults to seven days ago. TED has no sort parameter, so this date floor is what makes a run recent rather than returning notices from a decade ago.

## `publishedTo` (type: `string`):

Optional. A range that runs backwards is rejected with a warning rather than searched, because every one of these APIs answers an impossible window with silence.

## `language` (type: `string`):

Two-letter code. TED publishes descriptions, buyer cities and CPV labels in twenty-four languages; this picks one, falling back to English and then to whatever exists. Titles are the exception — TED never translates them, so a title stays in the buyer's language and titleLanguage records which that is.

## `maxPerSource` (type: `integer`):

TED alone can match hundreds of thousands. Summary rows carry totalAvailable where the source reports one, so you can see what was left behind.

## `includeSourceSummary` (type: `boolean`):

Add one rollup per source: counts by notice type, median value, top buyers, top CPV codes, how many close within a week and how many carry a contact email. Not billed as a notice.

## `maxConcurrency` (type: `integer`):

Sources searched in parallel. These are public sector APIs; three is plenty.

## `requestTimeoutSecs` (type: `integer`):

Per-request timeout. A TED page of 250 notices is several megabytes.

## `maxRetries` (type: `integer`):

Retries for connection resets and 5xx responses.

## `flattenOutput` (type: `boolean`):

Emit flat dot-notation columns. Mainly affects the CPV list and the top-buyer list on summary rows.

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

Optional. All three are public government APIs and none of them blocks.

## Actor input object example

```json
{
  "sources": [
    "ted",
    "uk-contracts-finder",
    "uk-find-a-tender"
  ],
  "keywords": [
    "software"
  ],
  "cpvCodes": [
    "48",
    "72"
  ],
  "noticeTypes": [
    "tender"
  ],
  "onlyOpen": false,
  "requireContactDetails": false,
  "minValue": 0,
  "maxValue": 0,
  "language": "en",
  "maxPerSource": 250,
  "includeSourceSummary": true,
  "maxConcurrency": 3,
  "requestTimeoutSecs": 60,
  "maxRetries": 2,
  "flattenOutput": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Notice records carry recordType 'tender'; rollups carry 'source-summary'.

# 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 = {
    "sources": [
        "ted",
        "uk-contracts-finder",
        "uk-find-a-tender"
    ],
    "keywords": [
        "software"
    ],
    "cpvCodes": [
        "48",
        "72"
    ],
    "noticeTypes": [
        "tender"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("eiv/tender-scraper").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 = {
    "sources": [
        "ted",
        "uk-contracts-finder",
        "uk-find-a-tender",
    ],
    "keywords": ["software"],
    "cpvCodes": [
        "48",
        "72",
    ],
    "noticeTypes": ["tender"],
}

# Run the Actor and wait for it to finish
run = client.actor("eiv/tender-scraper").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 '{
  "sources": [
    "ted",
    "uk-contracts-finder",
    "uk-find-a-tender"
  ],
  "keywords": [
    "software"
  ],
  "cpvCodes": [
    "48",
    "72"
  ],
  "noticeTypes": [
    "tender"
  ]
}' |
apify call eiv/tender-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,eiv/tender-scraper"
        }
    }
}

```

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/PZC31JY6eTEJ41LpC/builds/SDQl3B2Iih2eFShIX/openapi.json
