# German Tenders Monitor — bund.de, TED, DTVP & evergabe (`nerdrx/german-tenders-monitor`) Actor

Collect current German public-procurement notices (Ausschreibungen) from TED and service.bund.de (+ DTVP/evergabe best-effort) in one unified schema. Filter by keywords, CPV, region and deadline, dedupe across runs. Pay per notice.

- **URL**: https://apify.com/nerdrx/german-tenders-monitor.md
- **Developed by:** [Nerdrx](https://apify.com/nerdrx) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 tender 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

## German Tenders Monitor — bund.de, TED, DTVP, evergabe

### What is German Tenders Monitor?

German Tenders Monitor is an **unofficial** Apify Actor that collects current public
procurement notices (*Ausschreibungen*) for Germany from four different portals and
returns them in **one unified schema**.

Public tendering in Germany is deliberately fragmented: the EU publishes above-threshold
notices on TED, the federal government publishes on service.bund.de, and thousands of
contracting authorities publish on commercial e-procurement platforms such as DTVP and
evergabe. Anyone tracking opportunities has to watch all of them, in different formats,
with different field names and different date conventions.

This Actor does that watching for you. It queries each portal, normalises every notice
to the same 18 fields, filters by your keywords / CPV codes / region / deadline, and
remembers what it already returned so that repeated (e.g. daily) runs only produce
**new** notices.

It is built for:

- **Sales and bid teams** who want a daily feed of relevant tenders in their niche.
- **Procurement consultants** tracking a client's market or a competitor's buyers.
- **AI agents and pipelines** that need clean, typed JSON rather than four HTML portals.

> **Unofficial.** This Actor is not affiliated with, endorsed by or operated by the
> European Union, the German federal government, cosinex GmbH (DTVP) or Healy Hudson /
> evergabe. It reads publicly accessible pages and APIs. Always confirm details on the
> original notice before acting on them — the `noticeUrl` field links straight to it.

### What can German Tenders Monitor do?

- **Query four sources in one run** — TED, service.bund.de, DTVP and evergabe — each
  switchable on and off via the `sources` input.
- **Survive partial outages.** If one portal is down, blocks the run or changes its
  markup, that source is logged as failed and the run continues with the others. A run
  never dies because one website changed.
- **Filter server-side and client-side.** Keywords and CPV prefixes are pushed into the
  TED and bund.de queries where the portal supports it, and re-applied locally to every
  source so the filters behave identically no matter where a notice came from. A CPV
  filter never discards a notice that published no CPV code — most German national
  notices have none.
- **Understand German data.** It parses `30.09.2026, 12:00 Uhr` deadlines,
  `1.250.000,00 EUR` amounts, `72200000-7` CPV codes and NUTS codes like `DE212`, and it
  resolves a place or a NUTS code to a **Bundesland** so you can filter by federal state.
- **Deduplicate across runs.** Notice IDs seen in earlier runs are stored in the
  key-value store record `SEEN`, so a scheduled daily run gives you only what is new.
- **Only show what you can still bid on.** With `onlyOpen` (default on), notices whose
  submission deadline has passed are dropped.

### What data does it extract?

Every item — regardless of source — has exactly these fields:

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | Stable, source-prefixed identifier, e.g. `ted:00512345-2026`. Used for deduplication. |
| `source` | string | `ted`, `bund`, `dtvp` or `evergabe`. |
| `title` | string | Title of the notice (*Bezeichnung der Ausschreibung*). |
| `buyer` | string | null | Contracting authority (*Vergabestelle / Auftraggeber*). |
| `description` | string | null | Short description, truncated to ~500 characters. |
| `cpvCodes` | string\[] | 8-digit CPV codes, check digits stripped. Empty if the portal published none. |
| `procedureType` | string | null | Procedure (*Vergabeart*), e.g. `Offenes Verfahren`, `open`. |
| `region` | string | null | German federal state where known, otherwise the place of performance. |
| `country` | string | Always `DE` — the Actor only collects German notices. |
| `publishedAt` | string | null | Publication date, ISO 8601 UTC. |
| `deadlineAt` | string | null | Submission deadline (*Angebotsfrist*), ISO 8601 UTC. `null` when not published. |
| `estimatedValue` | number | null | Estimated contract value as a number, without separators. |
| `currency` | string | null | Currency of `estimatedValue`, usually `EUR`. `null` when there is no value. |
| `noticeUrl` | string | null | Direct link to the notice on the source portal. |
| `documentsUrl` | string | null | Link to the tender documents / PDF, when the portal exposes one. |
| `sourceUrl` | string | The URL the record was read from. |
| `scrapedAt` | string | ISO 8601 timestamp of the run. |

Not every portal publishes every field. `estimatedValue`, `documentsUrl` and `cpvCodes`
are frequently missing on the German national portals; TED is the most complete source.

### How to use German Tenders Monitor

1. Open the Actor and click **Start** — the default input already returns recent German
   tenders from all four portals for the last 7 days.
2. Narrow the feed to your market with `keywords` (e.g. `Software`, `IT-Dienstleistung`)
   and/or `cpvPrefixes` (e.g. `72` for IT services, `48` for software packages, `45` for
   construction). Keywords use OR semantics and are case-insensitive.
3. Optionally restrict to your sales territory via `regions` (e.g. `Bayern`,
   `Nordrhein-Westfalen`).
4. Set a **schedule** (daily at 07:00 is a good default) and keep `dedupe` on. Each run
   then returns only notices you have not seen before.
5. Connect the dataset to your CRM, a Slack/e-mail integration, or read it via the Apify
   API. The dataset has a ready-made table view.

**Tips**

- Run without keywords first to see the volume, then filter.
- CPV filtering is stricter than keyword filtering: a notice with no published CPV code
  is dropped when `cpvPrefixes` is set. Use keywords if you would rather over-collect.
- `publishedWithinDays` is the lookback window; on a daily schedule 2–3 days is enough
  and keeps the run fast and cheap.
- If a run returns nothing, check the log: it prints how many notices each filter removed.

### How much does it cost?

This Actor uses **pay-per-event** pricing. You are charged for the notices it actually
delivers to your dataset — nothing else. Starting the Actor, failed sources, filtered-out
notices and duplicates are all free.

| Event | When it fires | Planned price |
| --- | --- | --- |
| `tender-notice` | Once per tender notice successfully pushed to the dataset | **$0.004** per notice |

There are no other events — no per-run, per-page or per-request charges.

Examples at the planned price:

| Scenario | Notices per run | Cost per run | Monthly (daily run) |
| --- | --- | --- | --- |
| Focused niche (CPV `72`, one Bundesland) | ~10 | $0.04 | ~$1.20 |
| Broad IT feed across Germany | ~60 | $0.24 | ~$7.20 |
| Everything, no filters, `maxItems: 200` | 200 | $0.80 | ~$24 |

Apify platform usage (compute units) is billed separately by Apify. The Actor is
HTTP-only — no browser — and runs comfortably in **256–512 MB**.

If you set a maximum cost limit for a run, the Actor stops **gracefully** as soon as the
limit is reached: everything already pushed is kept and a warning is written to the log.

### Input

| Input | Type | Default | Description |
| --- | --- | --- | --- |
| `sources` | array | `["ted","bund","dtvp","evergabe"]` | Which portals to query. |
| `keywords` | string\[] | `[]` | Case-insensitive substrings matched against title, buyer and description. OR semantics; empty (the default) = keep all. |
| `excludeKeywords` | string\[] | `[]` | Drop a notice if it contains any of these. |
| `cpvPrefixes` | string\[] | `[]` | Keep only notices with a CPV code starting with one of these, e.g. `["72","48"]`. Notices with **no** CPV code are kept regardless. |
| `regions` | string\[] | `[]` | Keep only these Bundesländer, e.g. `["Bayern"]`. |
| `publishedWithinDays` | integer | `7` | Lookback window in days (1–90). |
| `onlyOpen` | boolean | `true` | Drop notices whose deadline has passed. Unknown deadlines are kept. |
| `maxItems` | integer | `200` | Hard cap on pushed notices per run. |
| `dedupe` | boolean | `true` | Skip notices already returned by earlier runs. |
| `maxSeenIds` | integer | `20000` | How many IDs the dedupe store keeps. |
| `proxyConfiguration` | object | none | Optional Apify Proxy configuration. |

Example input:

```json
{
  "sources": ["ted", "bund"],
  "keywords": ["Software", "IT-Dienstleistung", "Cloud"],
  "excludeKeywords": ["Reinigung"],
  "cpvPrefixes": ["72", "48"],
  "regions": ["Bayern", "Baden-Württemberg"],
  "publishedWithinDays": 7,
  "onlyOpen": true,
  "maxItems": 200,
  "dedupe": true
}
```

### Output

```json
{
  "id": "ted:00512345-2026",
  "source": "ted",
  "title": "Rahmenvertrag über IT-Dienstleistungen für die Stadtverwaltung München",
  "buyer": "Landeshauptstadt München, IT-Referat",
  "description": "Gegenstand der Ausschreibung ist ein Rahmenvertrag über Beratungs- und Entwicklungsleistungen im Bereich Softwareentwicklung, Cloud-Migration und Betrieb für die Landeshauptstadt München über eine Laufzeit von vier Jahren.",
  "cpvCodes": ["72000000", "72200000"],
  "procedureType": "open",
  "region": "Bayern",
  "country": "DE",
  "publishedAt": "2026-08-28T00:00:00.000Z",
  "deadlineAt": "2026-10-05T00:00:00.000Z",
  "estimatedValue": 2450000,
  "currency": "EUR",
  "noticeUrl": "https://ted.europa.eu/de/notice/-/detail/00512345-2026",
  "documentsUrl": "https://ted.europa.eu/de/notice/00512345-2026/pdf",
  "sourceUrl": "https://ted.europa.eu/de/notice/-/detail/00512345-2026",
  "scrapedAt": "2026-09-02T10:00:00.000Z"
}
```

The run also writes a `RUN_SUMMARY` record to the default key-value store with the number
of pushed, duplicate and filtered notices, a per-source breakdown, and the list of sources
that failed.

### FAQ & support

**Which sources are reliable?**
TED is an official, documented and key-free JSON API — it is the most reliable and the
most complete source. service.bund.de is scraped from its public search (RSS first, HTML
as a fallback) and is reliable but markup-dependent. **DTVP and evergabe are best-effort**:
their endpoints are undocumented and may change without notice. If they return nothing,
the run logs a warning and continues. See `NOTES.md` for the exact assumptions.

**Does it cover every German tender?**
No, and no single source does. Below-threshold notices in particular are spread over
dozens of state and municipal platforms. This Actor covers the EU-wide (TED) and federal
(bund.de) publications plus two large commercial platforms. Treat it as a high-coverage
monitor, not a legally complete register.

**Why is `estimatedValue` so often `null`?**
Because most German notices simply do not publish a contract value. The Actor never
guesses — a missing value stays `null`.

**Why did a notice I expected get filtered out?**
Check the log: it prints a **per-source** breakdown of drop reasons (`keywords`, `cpv`,
`region`, `publishedWithinDays`, `deadlinePassed`), plus a final `RUN_SUMMARY` line with
per-source counts. Note that `cpvPrefixes` never drops a notice that published no CPV
code at all — the German national portals frequently omit it — so the usual culprit is
`keywords` or `publishedWithinDays`.

**Why did the second run return nothing?**
That is deduplication working as intended — the notices were already delivered. Set
`dedupe: false` to get the full result set again.

**How do I reset the deduplication memory?**
Delete the `SEEN` record from the Actor's default key-value store, or run once with
`dedupe: false`.

**Do I need a proxy?**
No. All four sources are queried over plain HTTPS with no authentication. A German proxy
can help if a portal rate-limits a large run.

**Is scraping these portals legal?**
The Actor reads only publicly accessible pages and an official open API, at a modest
request rate. You are responsible for how you use the data. It never collects personal
data beyond the contracting-authority contact information the portals publish themselves.

**Something broke / I need another portal.**
Open an issue on the Actor page. Portal markup changes are the usual cause; the parsers
are written defensively with fallbacks, and `NOTES.md` lists exactly what to re-verify.

# Actor input Schema

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

Which portals to collect from. A failure in one source never stops the others. 'ted' and 'bund' are the two reliable, officially documented sources; 'dtvp' and 'evergabe' are best-effort and may return nothing if those portals change.

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

Case-insensitive substrings matched against the notice title, buyer and description. OR semantics: an item is kept if it contains ANY of them. Leave empty (the default) to keep everything. Examples: "Software", "IT-Dienstleistung", "Cloud".

## `excludeKeywords` (type: `array`):

Case-insensitive substrings matched against title and description. An item is dropped if it contains ANY of them. Applied after 'keywords'.

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

Keep only notices whose CPV codes start with one of these prefixes. Examples: "72" (IT services), "48" (software packages), "45" (construction). Notices that publish no CPV code at all are kept regardless — many German national notices have none. Leave empty (the default) to ignore CPV entirely.

## `regions` (type: `array`):

Keep only notices whose detected region or place of performance matches one of these German federal states, e.g. "Bayern", "Nordrhein-Westfalen". Leave empty to keep all of Germany.

## `publishedWithinDays` (type: `integer`):

Only collect notices published in the last N days. Also used to build the TED and bund.de search queries.

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

Drop notices whose submission deadline (Angebotsfrist) is already in the past. Notices with an unknown deadline are always kept.

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

Hard cap on how many notices are pushed to the dataset in one run, across all sources.

## `dedupe` (type: `boolean`):

Remember the IDs of notices already pushed (in the key-value store record 'SEEN') and skip them on later runs. Turn off to always get a full result set.

## `maxSeenIds` (type: `integer`):

Upper bound on how many IDs the deduplication store keeps. The oldest entries are dropped first.

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

Optional. Apify Proxy is not required — all sources are plain HTTP endpoints — but a German residential or datacenter proxy can help if a portal rate-limits the run.

## Actor input object example

```json
{
  "sources": [
    "ted",
    "bund"
  ],
  "keywords": [],
  "excludeKeywords": [],
  "cpvPrefixes": [],
  "regions": [],
  "publishedWithinDays": 7,
  "onlyOpen": true,
  "maxItems": 200,
  "dedupe": true,
  "maxSeenIds": 20000,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

No description

## `resultsCsv` (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 = {
    "sources": [
        "ted",
        "bund"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("nerdrx/german-tenders-monitor").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",
        "bund",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("nerdrx/german-tenders-monitor").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",
    "bund"
  ]
}' |
apify call nerdrx/german-tenders-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nerdrx/german-tenders-monitor"
        }
    }
}

```

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/BonIrI7zkliTCpjIK/builds/aIcDtWeUpc9tiBeTT/openapi.json
