# German & EU Tenders: Public Procurement Monitor (`lindenwerk/german-eu-tender-matcher`) Actor

German and EU public tenders in one deduplicated list: public procurement notices for Germany from oeffentlichevergabe.de (öffentliche Ausschreibungen, Vergabe) and TED, other EU countries via TED. Tenders scored to your CPV codes, keywords and regions, with deadlines. Daily tender alerts.

- **URL**: https://apify.com/lindenwerk/german-eu-tender-matcher.md
- **Developed by:** [Lindenwerk Data](https://apify.com/lindenwerk) (community)
- **Categories:** Business, Lead generation, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 scored match returneds

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?

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 is German & EU Tenders Monitor?

**Find public tenders in Germany (öffentliche Ausschreibungen) that actually fit your business.** This Actor combines the official German
notice service **oeffentlichevergabe.de** (federal, state and municipal notices, including many *below* EU
thresholds) with the EU's **TED (Tenders Electronic Daily)**. It removes cross-posted duplicates and scores
every notice against your profile (CPV codes, German/English keywords, regions, contract type and value). You get
one clean, flat row per opportunity, with the submission deadline, a `relevanceScore` from 0 to 100 and plain-language
`matchReasons`.

Use it as a **daily tender alert** (monitor mode returns only notices you haven't seen yet), for a **one-off market
scan** of the last 31 days (backfill), or for **competitor tracking** (award notices with winners and awarded values).

### Who it's for

- **Bid managers and sales teams** at companies that sell to the public sector: one scored shortlist each morning
  instead of checking several portals.
- **SMEs and Mittelstand suppliers** (IT, construction, facility services, consulting, supplies) that want to find
  below-threshold German tenders (Unterschwellenvergaben) that TED-only tools miss.
- **Tender consultants and agencies** who monitor several client profiles (one saved task and `stateKey` per client).
- **Analysts and AI agents** that need clean, structured public procurement data (CPV, NUTS, deadlines, values,
  award winners) through an API.

### What it does

- Downloads the **daily eForms open-data export** from oeffentlichevergabe.de (CC0) and parses the eForms XML.
- Queries the **TED Search API v3** for the countries you choose (default `DE`; any EU/EEA country works).
- **Deduplicates across sources.** A notice published on both portals becomes one row, `sources: ["oeffentlichevergabe.de", "TED"]`,
  with both links. Matching uses the eForms notice UUID first (TED stores it as `notice-identifier`), then the TED
  publication number, then a conservative fuzzy match on buyer, title and deadline. Older versions of a notice are collapsed.
  The same applies when a buyer re-publishes an identical procedure under a new id; the older ids go to `relatedNoticeIds`.
- **Scores relevance** with transparent rules (no LLM, no black box):
  - **CPV codes** by prefix: `72` matches all IT services, `4521` matches all building construction work.
  - **Keywords** in DE and EN, matched in title (weight 1.0) and description (0.5). Rare words count more (IDF).
    Umlauts and compound words are handled, so `Reinigung` also finds `Unterhaltsreinigung`.
  - Hard filters: `excludeKeywords`, `regions` (German state names or NUTS prefixes), `contractTypes`,
    `minValue`/`maxValue`, `onlyOpen` (submission deadline still in the future) and `competitorNames` (awards).
- **Three modes:** `monitor`, `backfill` and `awards` (see below).
- **GDPR by default.** Named contact persons, personal e-mail addresses and phone numbers are dropped. Only organisation
  names and functional contacts (e.g. `vergabestelle@…`, "Zentrale Vergabestelle") are kept.

### Why it beats single-source tender scrapers

| | TED-only scrapers | Single German portal scrapers | **This Actor** |
|---|---|---|---|
| EU-threshold tenders (TED) | ✅ | partly | ✅ |
| German below-threshold notices (UVgO/VOB/A national) | ❌ | partly | ✅ (via oeffentlichevergabe.de, all federal states) |
| Cross-source deduplication | ❌ | ❌ | ✅ one row per notice, both links kept |
| Relevance scoring with reasons | ❌ | ❌ | ✅ `relevanceScore` + `matchReasons` |
| "Only new since last run" | rarely | rarely | ✅ persistent state |
| Award / competitor tracking | partly | ❌ | ✅ winners + awarded values |
| Login, proxies, scraping of HTML pages | sometimes | usually | **none**: official open-data APIs only |

In a 30-day test window (5 Sep to 4 Oct 2026, buyer country DE) the Actor read 14,234 German eForms notice versions and
6,981 TED notices. It merged 6,915 cross-posts, collapsed 961 re-publications and returned **13,237 unique tender
notices**. About half of the German notices (7,217 of 14,132) were never published on TED, so a TED-only alert misses
them. 66 notices appeared only on TED.

### How to get daily German tender alerts

1. Click **Start** with the prefilled IT-services example. It's a quick market scan (`backfill`) that returns up to
   25 scored German and EU tenders, so you see real output before you change anything.
2. Replace the CPV codes and keywords with your own. Optionally add regions (NUTS codes or German states), contract types and a value range.
3. Pick a mode: `backfill` for a market scan of the last days, `monitor` for daily alerts that return only notices
   you haven't seen yet, or `awards` for competitor tracking.
4. Look at the results in the **Output** tab (views "Matches" and "Awards") or download them as JSON, CSV or Excel.
5. For daily alerts, switch `mode` to `monitor`, save the input as a **task** and add a **schedule** (for example
   every weekday at 07:00).
6. Send new matches on with Apify **integrations** (Slack, Google Drive, Make, Zapier, n8n or a webhook), or call the Actor from your own code through the **Apify API**.

### Modes

| `mode` | What you get | Typical use |
|---|---|---|
| `monitor` (default) | Notices published in the last `lookbackDays` (default 1) that **previous runs with the same `stateKey` have not returned**. A first run that finds nothing looks back 7 days once (see below). | Schedule daily (e.g. 07:00 Europe/Berlin) and connect to e-mail, Slack, a CRM or a spreadsheet. |
| `backfill` | Everything between `dateFrom` and `dateTo` (max 31 days; without dates: the last 7 days). Does not touch the monitor state. | Market scan, pipeline building, testing a profile. |
| `awards` | Contract award notices only, with `winnerNames`, `awardedValue` and `numberOfTenders`. Uses `dateFrom`/`dateTo` or `lookbackDays`. | Competitor and price intelligence. |

Note: oeffentlichevergabe.de publishes a full day's export the following morning, so German notices arrive with about one
day of delay. TED publishes on working days (nothing at weekends) and is queried up to today.

#### The Console form vs. API defaults

- **The Console form opens with a short trial:** `mode` = backfill (the last 7 days), the IT profile (CPV 72/48 and five
  keywords) and `maxResults` = 25. One click shows the 25 best current matches for at most USD 0.25. This prefill is
  also what Apify's daily automated Store test runs. It keeps no state, so it returns rows on any day, weekends and
  holidays included.
- **Before you schedule it**, switch `mode` to **monitor** and raise `maxResults`, or set it to 0 for no limit.
- **API calls, tasks and schedules that leave a field out get the defaults:** `mode` = monitor, `lookbackDays` = 1,
  `maxResults` = 500. The prefill values only fill the form.
- **Warning for one-off API and AI-agent calls: leaving out `mode` runs monitor mode.** That returns only notices from
  the last `lookbackDays` (default 1) that no earlier run with the same `stateKey` has returned, and it remembers them,
  so the same call repeated returns nothing new. For a one-off search, set `"mode": "backfill"` (without
  `dateFrom`/`dateTo` it covers the last 7 days and keeps no state):

  ```json
  {
    "mode": "backfill",
    "cpvCodes": ["72", "48"],
    "keywords": ["IT-Sicherheit", "Informationssicherheit", "IT security"],
    "regions": ["Bayern", "Berlin"],
    "maxDaysUntilDeadline": 21,
    "maxResults": 50
  }
  ```
- **First monitor run on a quiet day:** a monitor run with no saved state (the first run for a `stateKey`, or one with
  `resetState`) that finds nothing new in its lookback window looks back `firstRunBackfillDays` (default 7) once. That
  way a monitor started on a Sunday or a public holiday still shows the open tenders. Everything it returns is
  remembered, so the next run is a normal monitor run. Set `firstRunBackfillDays` to 0 to turn this off.
- **Quiet days and outages:** a monitor run with nothing new still ends *Succeeded*, with 0 rows and a status message
  ("no new matching notices since the last run"). Nothing is charged. If one source is down or runs out of time (a
  failed export day, a TED error), the run keeps everything that source did deliver (finished export days, TED pages
  fetched so far) and continues with the other source. It never hides this: the status message starts with
  **PARTIAL DATA**, the log has a `PARTIAL SOURCE` line with the missing days, and `RUN_SUMMARY` lists `sourceStatus`,
  `partialSources`, `missingDays` and `warnings`. You only pay for rows that are delivered. If every source is
  unreachable, the run ends *Failed* with a clear message. Nothing is charged and the monitor state is left unchanged,
  so the next run picks those days up again. A source that hangs is given up after 210 s in short monitor runs;
  backfills get 60 s plus 25 s per day of the window (about 14 minutes for 31 days), always leaving time before the
  run timeout, so a run never gets stuck.

### Input examples

#### 1. IT services (Germany-wide, open tenders only)

```json
{
  "mode": "monitor",
  "lookbackDays": 1,
  "countries": ["DE"],
  "profileName": "it-services",
  "cpvCodes": ["72", "48"],
  "keywords": ["Software", "IT-Dienstleistung", "IT-Beratung", "Cloud", "Rechenzentrum", "Datenbank",
               "IT-Sicherheit", "Webanwendung", "software development", "IT services"],
  "excludeKeywords": ["Druckerpapier"],
  "onlyOpen": true,
  "minScore": 40
}
```

#### 2. Construction works in Bavaria and Baden-Württemberg, at least EUR 100k

```json
{
  "mode": "backfill",
  "dateFrom": "2026-09-05",
  "dateTo": "2026-10-04",
  "profileName": "construction-south",
  "cpvCodes": ["45"],
  "keywords": ["Hochbau", "Rohbau", "Sanierung", "Neubau", "Dacharbeiten", "Fassade", "Trockenbau",
               "Schule", "Kindertagesstätte", "Erweiterungsbau"],
  "excludeKeywords": ["Objektplanung", "Planungsleistungen", "Projektsteuerung"],
  "contractTypes": ["works"],
  "regions": ["Bayern", "Baden-Württemberg"],
  "minValue": 100000,
  "minScore": 60
}
```

(`minValue` only removes notices whose estimated value is known and lower. Many notices publish no value and are kept.)

#### 3. Facility management in Berlin and Brandenburg

```json
{
  "mode": "monitor",
  "lookbackDays": 1,
  "profileName": "facility-berlin-brandenburg",
  "cpvCodes": ["909", "7971", "79993", "9834113", "7731", "507"],
  "keywords": ["Gebäudereinigung", "Unterhaltsreinigung", "Glasreinigung", "Hausmeister", "Facility Management",
               "Wachschutz", "Sicherheitsdienst", "Winterdienst", "Grünpflege", "Gebäudemanagement"],
  "excludeKeywords": ["Abwasser", "Kanalreinigung"],
  "contractTypes": ["services"],
  "regions": ["Berlin", "Brandenburg"],
  "minScore": 40
}
```

#### 4. Competitor tracking (awards)

```json
{
  "mode": "awards",
  "dateFrom": "2026-09-05",
  "dateTo": "2026-10-04",
  "cpvCodes": ["72", "48"],
  "competitorNames": ["Example IT GmbH"],
  "minScore": 40
}
```

More ready-to-run inputs are in the `examples/` folder of the source code.

#### Input reference (short)

| Field | Meaning |
|---|---|
| `cpvCodes` | CPV prefixes (digits only, 2–8 digits, check digit optional). `72` = IT services, `45` = construction work, `909` = cleaning, `4873` / `72212730` = security software, `7971` = guarding / security services (not IT). |
| `keywords` / `excludeKeywords` | German or English terms. Put a term in "quotes" for whole-word matching. Terms of 3 characters or fewer always match whole words only. |
| `regions` | German state names (`Bayern`, `Bavaria`, `NRW`, …) or NUTS prefixes (`DE21`, `DE300`, `AT13`). Matches buyer address or place of performance. |
| `contractTypes` | `works`, `services`, `supplies`. |
| `minScore` | 0–100. Only notices at or above it are returned (and charged). 40 is a good default; 60 is strict. Keywords raise the score, so a high `minScore` with only a few keywords can return nothing (a CPV-only match scores 50). |
| `onlyOpen` | Drop tenders whose submission deadline has passed (default `true`). |
| `minDaysUntilDeadline` / `maxDaysUntilDeadline` | Optional deadline window in days from the run date (the `daysUntilDeadline` field: submission deadline, or the participation deadline if no submission deadline is published). `"maxDaysUntilDeadline": 21` = due within 3 weeks; `"minDaysUntilDeadline": 5` skips tenders you can't prepare in time. Not set = no filter. When either is set, notices without a published deadline are excluded. Filtered notices are never charged. |
| `countries` | ISO-2 country codes for TED (`DE`, `AT`, `FR`, …). oeffentlichevergabe.de is always Germany. |
| `sources` | `oeffentlichevergabe`, `ted` (default both). |
| `maxResults` | Hard cap on rows (and therefore on cost) per run, best matches first. Default 500 (the Console form prefills 25). |
| `sortBy` | `relevance`, `deadline` or `publicationDate`. |
| `stateKey` | Separate monitor memory per profile (e.g. one per customer). `resetState: true` starts fresh. |
| `firstRunBackfillDays` | Monitor only: how far a first run with no saved state looks back if its normal window is empty. Default 7, 0 = off. |

Without `cpvCodes`, `keywords` or `competitorNames`, the Actor returns an **unscored feed** (all notices after filters),
which is charged at the lower `notice` price.

### Sample output

One row (abridged; the full sample is in `sample-output.json`):

```json
{
  "noticeId": "552260a3-3c12-465d-8fad-f46e5319acca",
  "noticeType": "tender",
  "title": "Datenschutzmanagementsystem",
  "buyerName": "ekom21 - Kommunales Gebietsrechenzentrum Hessen",
  "buyerCity": "Gießen",
  "buyerState": "Hessen",
  "buyerNuts": "DE731",
  "buyerEmail": "vergabestelle@ekom21.de",
  "buyerContactPoint": "Vergabestelle",
  "cpvMain": "72268000",
  "contractNature": "services",
  "procedureType": "open",
  "aboveEuThreshold": true,
  "submissionDeadline": "2026-10-06T10:00:00+02:00",
  "daysUntilDeadline": 2,
  "isOpen": true,
  "publicationDate": "2026-10-01",
  "documentsUrl": "https://www.had.de/NetServer/TenderingProcedureDetails?function=_Details&TenderOID=…",
  "oeffentlichevergabeUrl": "https://oeffentlichevergabe.de/ui/de/search/details?noticeId=552260a3-3c12-465d-8fad-f46e5319acca",
  "tedUrl": "https://ted.europa.eu/en/notice/-/detail/676777-2026",
  "tedPublicationNumber": "676777-2026",
  "sources": ["oeffentlichevergabe.de", "TED"],
  "dedupMethod": "notice-id",
  "relevanceScore": 80,
  "matchReasons": [
    "Main CPV 72268000 matches profile CPV 72",
    "Keyword 'Cloud' in description",
    "Keyword 'Rechenzentrum' in description",
    "Deadline in 2 day(s)"
  ],
  "profileName": "it-services"
}
```

Award rows additionally carry `winnerNames`, `awardedValue`, `awardedCurrency` and `numberOfTenders`. Every run also
stores a `RUN_SUMMARY` record in the key-value store: notices per source, whether each source is complete
(`sourceStatus`, `missingDays`), dedup statistics, filter counts and matches.

#### Output fields

| Group | Fields |
|---|---|
| Identity | `noticeId`, `noticeVersion`, `noticeType` (`tender`, `award`, `planning`, `modification`, `change`, `other`), `formType`, `noticeSubtype`, `relatedNoticeIds` |
| Content | `title`, `description` (shortened, see `descriptionMaxLength`), `cpvMain`, `cpvCodes`, `contractNature`, `procedureType`, `legalBasis`, `aboveEuThreshold`, `lotCount` |
| Buyer | `buyerName`, `buyerCity`, `buyerPostalCode`, `buyerState`, `buyerNuts`, `buyerCountry`, `buyerWebsite`, `buyerEmail` and `buyerContactPoint` (functional only), `placeOfPerformanceNuts` |
| Money and dates | `estimatedValue`, `currency`, `publicationDate`, `submissionDeadline`, `participationDeadline`, `daysUntilDeadline`, `isOpen` |
| Links | `oeffentlichevergabeUrl`, `tedUrl`, `tedPublicationNumber`, `noticeUrl`, `documentsUrl`, `submissionUrl` |
| Matching | `relevanceScore`, `matchReasons`, `profileName`, `sources`, `dedupMethod`, `language`, `retrievedAt` |
| Awards | `winnerNames`, `awardedValue`, `awardedCurrency`, `numberOfTenders` (`awardedValue` is only filled on award and contract-modification notices; it is always null on open tenders, where `estimatedValue` holds the estimate) |

Export as JSON, CSV, Excel or XML from the dataset, or read it through the API.

### Pricing (pay per event)

| Event | Price |
|---|---|
| Actor start | USD 0.00005 |
| `scored-match`: a notice that matched your profile at or above `minScore` | **USD 0.01** |
| `notice`: a row in an unscored feed (no CPV, keyword or competitor profile) | **USD 0.003** |

Notices below `minScore`, filtered notices and duplicates are **never charged**. Platform compute is included in these
prices. You can cap cost with `maxResults` and with the run's *maximum total charge*. The Actor stops cleanly when that
limit is reached.

**Worked examples per run** (real runs on 4 Oct 2026, buyer country DE, 30-day window 5 Sep to 4 Oct 2026; the
example inputs are in `examples/`):

| Run | Rows returned | Your cost |
|---|---|---|
| Daily `monitor`, IT services profile (one day's notices) | 13 | 13 × 0.01 = **USD 0.13** |
| 30-day `backfill`, IT services, open tenders, minScore 40 | 496 | **USD 4.96** |
| 30-day `backfill`, construction, Bavaria + BW, ≥ EUR 100k, minScore 60 | 442 | **USD 4.42** |
| 30-day `backfill`, facility management, Berlin + Brandenburg | 48 | **USD 0.48** |
| 30-day `awards`, IT award notices (competitor tracking) | 411 | **USD 4.11** |
| 30-day unscored feed, capped with `maxResults: 1000` | 1,000 | 1,000 × 0.003 = **USD 3.00** |

Each run also triggers the Actor start event (USD 0.00005).

**Monthly estimate for a daily `monitor` schedule** (same volumes; a daily monitor sees tenders while they are still
open, so it returns slightly more than a backfill run at the end of the month):

| Profile | Matches per month | Cost per month |
|---|---|---|
| IT services, Germany-wide, minScore 40 | ~500–650 | ≈ **USD 5–6.50** |
| Construction works, Bavaria + BW, ≥ EUR 100k, minScore 60 | ~450–500 | ≈ **USD 4.50–5** |
| Facility management, Berlin + Brandenburg | ~50–60 | ≈ **USD 0.50–0.60** |
| Unscored full feed, all German tenders | ~13,200 | ≈ USD 40 (or set `maxResults`) |

For comparison, commercial German tender-alert subscriptions typically cost a monthly fee per user. Here you pay only
for notices that actually matched.

### Use with AI agents (MCP)

The input schema is written so that an LLM can call the Actor unambiguously: enums, defaults, examples and descriptions
in plain English. Add the Actor to the [Apify MCP server](https://mcp.apify.com) in Claude Desktop, Cursor, VS Code or
any other MCP client:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com?tools=actors,lindenwerk/german-eu-tender-matcher"
    }
  }
}
```

Then ask, for example: *"Find open IT security tenders in Bavaria and Berlin from the last 7 days with a deadline in
the next 3 weeks, and summarise the top 5 with their deadlines."* The agent maps this to `mode: backfill`,
`cpvCodes: ["72"]`, `keywords: ["IT-Sicherheit", "Informationssicherheit", "IT security"]`,
`regions: ["Bayern", "Berlin"]` and `maxDaysUntilDeadline: 21`. It can then use `matchReasons` to explain why each
result fits. Agents should always set `mode` explicitly: without it the call runs as a monitor (see above).

You can also call it via the Apify API (`POST https://api.apify.com/v2/acts/lindenwerk~german-eu-tender-matcher/run-sync-get-dataset-items`)
or the Python/JS clients, and connect it to Make, Zapier, n8n, Google Sheets or Slack through Apify integrations.

### Data sources and licences

The Actor processes **public, non-personal procurement data** that contracting authorities are legally required to
publish. It uses official open-data interfaces only: no login, no proxies, no HTML scraping.

- **oeffentlichevergabe.de**, the German national notice service (Bekanntmachungsservice) run on behalf of the Federal
  Ministry for Economic Affairs: open-data eForms export (`/api/notice-exports`), published under
  **CC0 1.0** (see the portal's publication policy at https://oeffentlichevergabe.de/ui/de/publicationPolicy).
- **TED (Tenders Electronic Daily)**, Publications Office of the European Union: Search API v3
  (`https://api.ted.europa.eu/v3/notices/search`). © European Union. Reuse is authorised under
  **Commission Decision 2011/833/EU** on the reuse of Commission documents. Source: TED, https://ted.europa.eu.
- The Actor only reshapes and scores the official data. Always check the original notice (links included) and the
  tender documents before you bid. Deadlines in the official notice prevail.

### GDPR and responsible use

Public procurement notices sometimes contain the names, e-mail addresses or phone numbers of individual employees. This
Actor **drops them by default**: it never outputs phone numbers, removes person-like e-mail addresses
(e.g. `firstname.lastname@`) and keeps only functional contact points such as "Vergabestelle" or `vergabe@…`.
The same applies to free text: phone and fax numbers, personal e-mail addresses and "Frau/Herr <name>" mentions inside
titles and descriptions are replaced with placeholders such as `[Name entfernt]`.
Organisation names (buyers and winners) are kept as published. Some winners are sole traders whose company name
contains a personal name. Use the output to find and bid for tenders, not for contact harvesting or unsolicited
marketing.

### FAQ

**How is `relevanceScore` calculated?** CPV evidence (main CPV match 0.5, additional CPV 0.35) and keyword evidence
(title hits count double, rare terms weigh more) are combined as `100 × (1 − (1 − cpv) × (1 − keywords))`. In a profile
with both CPV codes and keywords, a CPV-only match scores 50, a single title keyword about 46, and CPV plus a title
keyword about 70–80. Several strong keyword hits approach 100. If your profile has only CPV codes, a main-CPV match
scores 100 and an additional-CPV match 70. If it has only keywords, the keyword evidence alone sets the score.
`matchReasons` lists every contribution.

**Why do some rows have no estimated value?** Many buyers don't publish one. Value filters keep notices with unknown
values so you don't miss them.

**Which deadline is `submissionDeadline`?** The tender submission deadline (eForms `TenderSubmissionDeadlinePeriod`,
earliest across lots). For two-stage procedures, `participationDeadline` holds the request-to-participate deadline.
Times are Europe/Berlin unless the notice states otherwise. If no time is given, 23:59:59 is assumed.

**Can I monitor several profiles?** Yes. Create one saved task per profile, each with its own `stateKey`.

**How fresh is the data?** TED is queried up to the current day. oeffentlichevergabe.de publishes each day's export the
next morning, so German-only notices arrive about one day after publication. Schedule the monitor for the morning
(e.g. 07:00 Europe/Berlin).

**Does it cover other countries?** TED covers all EU/EEA countries: set `countries`, e.g. `["DE", "AT"]`.
The German below-threshold notices come from oeffentlichevergabe.de and are Germany only.

**Can I only get tenders due soon (or not too soon)?** Yes. `maxDaysUntilDeadline` keeps notices whose deadline is at
most that many days after the run, `minDaysUntilDeadline` at least that many. Both count from the run date to the
deadline in `daysUntilDeadline`. With either set, notices without a published deadline are dropped. Dropped notices
are not charged.

**What does a run with no matches cost?** Only the Actor start event (USD 0.00005). You never pay for notices that
were filtered out, below `minScore`, duplicates or already returned by an earlier monitor run.

**Is this an official service?** No. It is an independent tool built on the official open data. Always check the
original notice and the tender documents before you bid; the official notice prevails.

**Is it legal to use this data?** Yes. Procurement notices are published so that companies can bid. oeffentlichevergabe.de
data is CC0 and TED data may be reused under Commission Decision 2011/833/EU with source attribution. Personal contact
data is removed (see GDPR above).

### Examples

- [IT security tenders in Germany - last 7 days](https://apify.com/lindenwerk/german-eu-tender-matcher/examples/it-security-tenders-germany): a published example task with a ready-made input. Open it, adjust the input and run it in your own Apify account.

### Other Lindenwerk Data Actors

- [France Tenders Monitor: BOAMP, TED & Marchés Publics](https://apify.com/lindenwerk/france-tender-matcher): marchés publics from BOAMP and TED in one deduplicated, scored list.
- [UK Tenders Monitor: Government Contracts & Find a Tender Alerts](https://apify.com/lindenwerk/uk-tender-matcher): Find a Tender notices (above and below threshold) as daily tender alerts, scored to your profile.
- [SAM.gov Government Contract Opportunities Monitor](https://apify.com/lindenwerk/sam-gov-contract-matcher): US federal bids and RFPs from SAM.gov, scored to your NAICS and set-asides.
- [Website Technology & Tech Stack Detector: BuiltWith Alternative](https://apify.com/lindenwerk/tech-stack-detector): CMS, shop system, analytics, consent manager and email provider of any website, in bulk.
- [SEO Audit Crawler: Broken Links, llms.txt & AI Bot Check](https://apify.com/lindenwerk/seo-audit-crawler): on-page SEO, broken links, llms.txt and AI crawler check for whole websites.
- [PDF to Markdown & RAG Chunks: Document Parser (DOCX/PPTX/XLSX)](https://apify.com/lindenwerk/pdf-to-markdown-rag): PDF, Word, PowerPoint and Excel to clean Markdown with inline tables and page-cited RAG chunks; it reads the procurement documents behind each notice's `documentsUrl`.

***

### Deutsch (Kurzfassung)

**Öffentliche Ausschreibungen aus oeffentlichevergabe.de und TED in einer Liste: dedupliziert und nach Ihrem Profil
bewertet.** Der Actor lädt die offiziellen Open-Data-Bekanntmachungen (eForms, CC0) des Bekanntmachungsservice des Bundes
(auch Unterschwellenvergaben nach UVgO/VOB/A) sowie EU-weite Ausschreibungen aus TED. Doppelte Veröffentlichungen
werden zusammengeführt, und jede Bekanntmachung wird nach CPV-Codes, Suchbegriffen (DE/EN), Bundesländern/NUTS, Auftragsart und
Auftragswert bewertet (`relevanceScore` 0–100 mit Begründung in `matchReasons`).

- **monitor** (Standard bei API-Aufrufen und Zeitplänen): täglich nur neue Ausschreibungen (ideal als Ausschreibungs-Alert per Zeitplan)
- **backfill**: Marktüberblick über bis zu 31 Tage. Das Formular in der Console startet damit: die letzten 7 Tage, höchstens 25 Treffer. Vor dem Einplanen auf monitor umstellen. Achtung: Ein API-Aufruf ohne `mode` läuft als monitor; für eine einmalige Suche `"mode": "backfill"` setzen.
- **awards**: Zuschlagsbekanntmachungen mit Gewinnern und Auftragswerten (Wettbewerbsbeobachtung)

Optional: `minDaysUntilDeadline` / `maxDaysUntilDeadline` (Tage bis zur Angebotsfrist, z. B. `21` = Frist in den nächsten 3 Wochen); Bekanntmachungen ohne Frist fallen dann heraus und werden nicht berechnet.

Abrechnung pro Treffer (USD 0,01 je bewertetem Treffer, USD 0,003 je ungefilterter Bekanntmachung). Personenbezogene
Kontaktdaten (Namen, persönliche E-Mail-Adressen, Telefonnummern) werden standardmäßig entfernt. Quellen:
oeffentlichevergabe.de (CC0) und TED, © Europäische Union (Weiterverwendung gemäß Beschluss 2011/833/EU).

*Made by Lindenwerk Data.*

# Changelog

This Actor's version history is a separate document: https://apify.com/lindenwerk/german-eu-tender-matcher/changelog.md

# Actor input Schema

## `mode` (type: `string`):

monitor = only notices not returned by a previous run with the same stateKey (schedule it daily; this is the default when the field is omitted, e.g. in API calls). backfill = every notice in the date range dateFrom..dateTo (max 31 days; without dates: the last 7 days). awards = contract award notices only (who won what, for competitor tracking) in dateFrom..dateTo. The form opens with backfill so a first try shows the last 7 days right away; switch to monitor before you schedule it.

## `profileName` (type: `string`):

Free label for this search profile, copied to every output row (profileName). Also the default stateKey in monitor mode, so different profiles keep separate 'already seen' lists. Example: "it-services-berlin".

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

CPV classification codes or prefixes. A prefix matches every code that starts with it: "72" = all IT services, "48" = software packages, "45" = all construction works, "909" = cleaning services, "7971" = guarding / security services (Bewachung, not IT), "4873" = security software packages, "72212730" = security software development (for IT security, combine these with keywords). A main-CPV match gives the strongest score. Check digits like "-8" are ignored.

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

Words matched in title (full weight) and description (half weight). Case- and umlaut-insensitive. Keywords with 4+ letters also match inside German compound words ("Software" matches "Softwareentwicklung"); shorter ones ("KI", "EDV") match whole words only. Wrap a keyword in double quotes to force whole-word matching, e.g. ""Reinigung"". Rare keywords weigh more (BM25-style IDF). Keywords raise relevanceScore, so they interact with minScore: a CPV-only match scores 50, so a high minScore (60+) with only a few keywords can return nothing. Add more keyword variants (German and English) or lower minScore.

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

Notices whose title or description contains any of these words are dropped (never charged). Same matching rules as keywords. Example for a construction contractor: \["Objektplanung", "Projektsteuerung"].

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

Keep only notices whose buyer or place of performance lies in one of these regions. Accepts NUTS prefixes ("DE3" Berlin, "DE21" Oberbayern, "DEA" NRW, "AT13" Vienna) and German state names in German or English ("Bayern", "Bavaria", "NRW", "Baden-Württemberg"). Empty = no regional filter.

## `contractTypes` (type: `array`):

Keep only these contract natures. works = construction works (Bauleistungen), services = services (Dienstleistungen), supplies = goods (Lieferungen). Empty = all.

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

Drop notices whose published estimated value (awards mode: awarded value) is below this amount. Notices without a published value are kept and flagged in matchReasons.

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

Drop notices whose published estimated value (awards mode: awarded value) is above this amount. Notices without a published value are kept.

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

Drop tenders whose submission/participation deadline has already passed. Tenders without a published deadline are kept (matchReasons says 'Deadline not published'). Ignored in awards mode.

## `minDaysUntilDeadline` (type: `integer`):

Optional. Keep only notices whose deadline is at least this many days after the run (daysUntilDeadline >= value), e.g. 5 to skip tenders you cannot prepare in time. Days are counted from the run date to the submission deadline (the participation-request deadline when no submission deadline is published), the same number as the daysUntilDeadline output field. Empty = no filter (default). When set, notices without a published deadline are excluded. Filtered notices are never charged.

## `maxDaysUntilDeadline` (type: `integer`):

Optional. Keep only notices whose deadline is at most this many days after the run (daysUntilDeadline <= value), e.g. 21 = due within three weeks. Days are counted from the run date to the submission deadline (the participation-request deadline when no submission deadline is published), the same number as the daysUntilDeadline output field. Empty = no filter (default). When set, notices without a published deadline are excluded. Filtered notices are never charged.

## `minScore` (type: `integer`):

Only notices with relevanceScore >= minScore are returned (and charged as scored-match). Guide for a CPV + keyword profile: CPV match only = 50, one keyword in the title only = about 46, CPV + one title keyword = about 73. Use 40 for broad recall, 60 for precision. Keywords raise the score: with few keywords a high minScore can return nothing. Ignored when no CPV codes, keywords or competitor names are given.

## `competitorNames` (type: `array`):

Only return award notices won by an organisation whose name contains one of these strings (case-insensitive), e.g. \["Muster Bau", "Beispiel IT"]. Use together with mode = awards.

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

Monitor mode: how many days back to look on each run. 1 = yesterday's German export plus TED from yesterday and today. Larger values are safe (already-seen notices are skipped) and protect against missed scheduled runs.

## `dateFrom` (type: `string`):

First publication day (YYYY-MM-DD) for backfill and awards mode. Default: dateTo minus 6 days. Ranges longer than 31 days are capped to the last 31 days.

## `dateTo` (type: `string`):

Last publication day (YYYY-MM-DD), inclusive. Default: today. The German export is only available up to yesterday.

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

ISO country codes of the buyers to include (two- or three-letter, e.g. DE, AT, FR, NL, PL). oeffentlichevergabe.de is used only when DE is in the list; other countries come from TED.

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

oeffentlichevergabe = German Bekanntmachungsservice (all German federal, state and municipal notices, including below EU thresholds). ted = Tenders Electronic Daily (EU-wide, above thresholds). Use both for full coverage; cross-posted notices are merged.

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

Which notice types to return. tender = open calls for tenders (default for monitor/backfill), award = contract awards (forced in awards mode), planning = prior information notices, modification = contract modifications, change = corrigenda.

## `maxResults` (type: `integer`):

Maximum number of rows to return (and charge) in this run, best matches first. 0 = no limit. Default 500; the form prefills 25 for a cheap first try, so raise it (or set 0) for full results. The run also stops when your maximum total charge for the run is reached.

## `sortBy` (type: `string`):

Order of the output rows: relevance (score desc, then earliest deadline), deadline (earliest first) or publicationDate (newest first).

## `includeDescription` (type: `boolean`):

Include a shortened notice description in each row.

## `descriptionMaxLength` (type: `integer`):

Maximum number of characters of the description per row.

## `includeFunctionalContacts` (type: `boolean`):

Include the buyer's functional contact (e.g. vergabestelle@city.de, 'Zentrale Vergabestelle'). Personal names, personal e-mail addresses (firstname.lastname@) and phone numbers are never output.

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

Name of the 'already seen' list kept in the named key-value store 'tender-matcher-state'. Default: the profile name. Use a different key per profile or per client.

## `resetState` (type: `boolean`):

Forget all notices seen by earlier runs with this stateKey before running (monitor mode).

## `firstRunBackfillDays` (type: `integer`):

Monitor mode: if a run without saved state (the very first run for this stateKey, or after Reset monitor state) finds nothing new in its lookback window, for example because it starts on a weekend or holiday, it looks back this many days once instead, so you see the current open tenders. Later runs are normal monitor runs. 0 = off.

## Actor input object example

```json
{
  "mode": "backfill",
  "profileName": "it-services",
  "cpvCodes": [
    "72",
    "48"
  ],
  "keywords": [
    "Software",
    "IT-Dienstleistung",
    "Cloud",
    "Rechenzentrum",
    "Digitalisierung"
  ],
  "onlyOpen": true,
  "minScore": 40,
  "lookbackDays": 3,
  "countries": [
    "DE"
  ],
  "sources": [
    "oeffentlichevergabe",
    "ted"
  ],
  "maxResults": 25,
  "sortBy": "relevance",
  "includeDescription": true,
  "descriptionMaxLength": 500,
  "includeFunctionalContacts": true,
  "resetState": false,
  "firstRunBackfillDays": 7
}
```

# Actor output Schema

## `dataset` (type: `string`):

Dataset with one flat row per notice

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

Counts per source, dedup statistics, filter statistics and runtime

# 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 = {
    "mode": "backfill",
    "profileName": "it-services",
    "cpvCodes": [
        "72",
        "48"
    ],
    "keywords": [
        "Software",
        "IT-Dienstleistung",
        "Cloud",
        "Rechenzentrum",
        "Digitalisierung"
    ],
    "lookbackDays": 3,
    "maxResults": 25
};

// Run the Actor and wait for it to finish
const run = await client.actor("lindenwerk/german-eu-tender-matcher").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 = {
    "mode": "backfill",
    "profileName": "it-services",
    "cpvCodes": [
        "72",
        "48",
    ],
    "keywords": [
        "Software",
        "IT-Dienstleistung",
        "Cloud",
        "Rechenzentrum",
        "Digitalisierung",
    ],
    "lookbackDays": 3,
    "maxResults": 25,
}

# Run the Actor and wait for it to finish
run = client.actor("lindenwerk/german-eu-tender-matcher").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 '{
  "mode": "backfill",
  "profileName": "it-services",
  "cpvCodes": [
    "72",
    "48"
  ],
  "keywords": [
    "Software",
    "IT-Dienstleistung",
    "Cloud",
    "Rechenzentrum",
    "Digitalisierung"
  ],
  "lookbackDays": 3,
  "maxResults": 25
}' |
apify call lindenwerk/german-eu-tender-matcher --silent --output-dataset

```

## MCP server setup

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

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/ao8v5ilTPAhO7Kdya/builds/5NQj7e2IdFT1Vb5pZ/openapi.json
