# Danish Public Tenders (TED): Udbud Alerts by CPV & Keyword (`nordic_open_data/dk-public-tenders`) Actor

Danish public tenders (udbud) from the official EU TED API: buyer, CPV, value, deadline, link. CPV/keyword filters and daily only-new alerts.

- **URL**: https://apify.com/nordic_open_data/dk-public-tenders.md
- **Developed by:** [Nordic Open Data](https://apify.com/nordic_open_data) (community)
- **Categories:** Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.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?

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

## Danish Public Tenders (TED): Udbud Alerts by CPV & Keyword

Get **Danish public tenders (offentlige udbud)** as clean, ready-to-use data: title, buyer, CPV codes, estimated value, bid deadline and a link to the notice. The data comes from TED, the EU's official procurement journal. Add Sweden, Norway, Finland or Iceland if you bid across the Nordics.

**Daily tender alerts that cost nothing on quiet days.** Switch on **Only new since last run** and schedule the Actor. Each notice is delivered once, ready for Slack, email, Google Sheets or your CRM. A run with nothing new is free.

### What it does

- Searches all TED notices from Danish buyers (kommuner, regioner, styrelser, forsyninger, boligselskaber …), with optional other Nordic countries.
- Filters by **CPV code** (e.g. `72` IT, `45` construction), **keywords** in any language (`software`, `rengøring`, `rådgivning`), **notice type** and **publication date**.
- **Only open for bids**: drop tenders whose deadline has passed. **Minimum value** drops small or unpriced tenders.
- **Contract awards**: who won, with the winner's company number and the awarded value (notice type "Contract awards").
- **Buyer CVR number**, NUTS region, number of lots, framework agreement flag (rammeaftale) and direct links to the **tender documents** and **bid submission** page.
- **Only new since last run**: remembers what you already received, per saved search (`monitorId`).
- **Every lot** of a notice: title, CPV, value and deadline per lot (nested `lots` array), or **one row per lot** for spreadsheets.
- **Nordic presets** (Denmark, Scandinavia, all Nordic) with values converted to **DKK and EUR** using the ECB reference rates.
- **English where it exists**: an English CPV category for every notice, plus the buyer's own English title and summary when they published one. Nothing is machine-translated.
- **Changes and cancellations are flagged**: each notice has a `status` (published, changed, cancelled, awarded, noAward). In alert mode, corrections, cancellations and awards for tenders you already received are marked `updatesSeenNotice: true`.
- Titles and buyer names in Danish, English, Swedish, Norwegian or Finnish.
- Fast and cheap: one official API, no scraping, no login and no proxies. A 20-notice run takes a few seconds.

### Who it's for

- **Bid managers and sales teams** who want a morning list of new Danish tenders in their category.
- **Consultants and tender-writing agencies** who run one saved search per client.
- **Market analysts** who size public demand by CPV, buyer or value.
- **AI agents and automations** that need a small, flat record per tender.

### How to use it

1. Click **Try for free**. The default (Denmark, keyword "software", calls for tender from the last 7 days, 20 notices) runs in seconds.
2. Set your **CPV codes** and/or **keywords**.
3. For alerts, turn on **Only new since last run**, give the search a **Monitor ID**, and add a **Schedule** (e.g. every weekday at 07:00).
4. Export as JSON, CSV or Excel, or connect to Google Sheets, Slack, Make, Zapier or n8n.

#### Example inputs

Daily IT tender alert, Denmark only, open tenders only:

```json
{ "cpvCodes": ["72", "48"], "onlyOpenForBids": true, "onlyNew": true, "monitorId": "it-dk", "publishedWithinDays": 3, "maxItems": 200 }
```

Construction tenders in Denmark and Norway in September 2026, in Danish:

```json
{ "countries": ["DNK", "NOR"], "cpvCodes": ["45"], "dateFrom": "2026-09-01", "dateTo": "2026-09-30", "preferredLanguage": "dan", "maxItems": 500 }
```

Who won Danish IT contracts in the last 30 days:

```json
{ "cpvCodes": ["72"], "noticeTypes": ["result"], "publishedWithinDays": 30 }
```

### Recipes: daily alerts in 5 minutes

1. **Save a task.** Open the Actor, enter one of the inputs below and click **Save as a new task**. Name it e.g. "IT tenders DK".
2. **Schedule it.** In Apify Console go to **Schedules → Create new**, set e.g. `0 7 * * 1-5` (weekdays 07:00, Europe/Copenhagen) and add your task.
3. **Get notified.**
   - **Email:** on the task, open **Integrations** and add the email integration (or a Zapier/Make "Actor run succeeded" trigger). Have it send the dataset link or items.
   - **Slack, Teams or your system:** under **Integrations → Webhooks**, add a webhook for `ACTOR.RUN.SUCCEEDED` pointing to your endpoint (Slack incoming webhook via Make/Zapier/n8n, or your API). Fetch the items from `https://api.apify.com/v2/datasets/{{resource.defaultDatasetId}}/items?view=overview`.
   - **Know when something breaks:** add a second notification for `ACTOR.RUN.FAILED` / `TIMED_OUT`.
4. Quiet days cost nothing: a run with no new notices isn't charged.

#### Example inputs per industry

IT and software (Denmark, open tenders only):

```json
{ "cpvCodes": ["72", "48"], "onlyOpenForBids": true, "onlyNew": true, "monitorId": "it", "publishedWithinDays": 3 }
```

Construction across Scandinavia, values in DKK, over 5 million:

```json
{ "countryPreset": "scandinavia", "cpvCodes": ["45"], "minValue": 5000000, "onlyNew": true, "monitorId": "construction", "publishedWithinDays": 3 }
```

Cleaning and facility services (Danish and English keywords):

```json
{ "cpvCodes": ["909"], "keywords": ["rengøring", "cleaning", "facility"], "onlyNew": true, "monitorId": "cleaning", "publishedWithinDays": 3 }
```

Consulting, management and engineering advice (one row per lot):

```json
{ "cpvCodes": ["79400000", "71300000"], "keywords": ["rådgivning", "konsulent"], "outputMode": "lots", "onlyNew": true, "monitorId": "consulting", "publishedWithinDays": 3 }
```

Note: `minValue` is compared in the notice's own currency.

### Output

One row per notice, newest first. Example:

```json
{
  "id": "711403-2026",
  "title": "Danmark – Aflæsning af målere – LEVERING AF FORBRUGSMÅLERE OG -REGNSKABER SAMT HÅNDTERING AF FORBRUGSKLAGER (2026)",
  "buyer": "Boligselskabet Sjælland",
  "buyerCountry": "DNK",
  "cpv": ["65500000", "38421100", "72314000"],
  "mainCpv": "65500000",
  "value": 25000000,
  "currency": "DKK",
  "deadline": "2026-11-25",
  "deadlineType": "tender",
  "daysToDeadline": 41,
  "publicationDate": "2026-10-15",
  "noticeType": "cn-standard",
  "noticeGroup": "competition",
  "contractNature": "services",
  "procedureType": "open",
  "buyerCvr": "42008419",
  "placeOfPerformance": ["DK021", "DK022"],
  "lotCount": 1,
  "isFrameworkAgreement": false,
  "documentsUrl": "https://permalink.mercell.com/292506803.aspx",
  "submissionUrl": "https://permalink.mercell.com/292506803.aspx",
  "winners": [],
  "awardedValue": null,
  "description": "Dette udbud vedrører levering og installation af forbrugsmålere …",
  "url": "https://ted.europa.eu/en/notice/-/detail/711403-2026",
  "pdfUrl": "https://ted.europa.eu/en/notice/711403-2026/pdf",
  "source": "TED"
}
```

Contract award example (`noticeTypes: ["result"]`):

```json
{ "id": "710026-2026", "buyer": "FORSYNING HELSINGØR SPILDEVAND A/S", "buyerCvr": "32654266", "isFrameworkAgreement": true,
  "winners": [{ "name": "Init Denmark A/S", "identifier": "19256707" }], "awardedValue": 4590000, "awardedCurrency": "DKK" }
```

Views: **Tenders** (date, title, buyer, value, deadline, documents, link), **Contract awards** (buyer, CVR, winners, awarded value) and **All details** (every field).

### Pricing

Pay per event. Platform usage is included.

| Event | Price | When |
|---|---|---|
| Actor start (`actor-start`) | $0.002 | Once per run that returns at least one notice |
| Tender notice (`tender-notice`) | $0.003 | Per notice returned ($3 per 1,000) |

**Runs that fail or return no notices cost nothing**, so a daily alert with nothing new is free. Examples: a 20-notice test costs $0.062. A daily alert with about 5 new notices a day costs about $0.50 a month. A 1,000-notice export costs about $3. Use **Max notices** (or the run's maximum cost) to cap spending.

### FAQ

**Does it include all Danish tenders?** It includes everything Danish buyers publish on TED: all EU tenders above the thresholds, plus voluntary below-threshold publications. Tenders published *only* on udbud.dk (below the EU thresholds) are not included yet. See Limitations.

**How does "only new" work?** The IDs of notices you received are stored in a key-value store named `dk-tenders-state` in your own Apify account. The record is keyed by your **Monitor ID plus a fingerprint of your search filters**: countries, CPV codes, keywords (case and order don't matter), notice types, minimum value and open-only. Changing the date range, max notices, language, row format or description setting keeps your history, so a rolling "last 3 days" schedule works. Changing a filter starts a fresh list, so you get the full matching set once. The first run returns everything that matches and saves the baseline. Notices that correct, cancel or award a tender you already received are delivered again as their own notices, with `updatesSeenNotice: true`.

**Why zero results?** Your filters are narrow or, in "only new" mode, nothing new was published. You're not charged for that. TED publishes on working days (Tue–Sat).

**Can I search in Danish?** Yes. Keywords match the full notice text in any language. Combine e.g. `["rengøring", "cleaning"]`.

### Limitations

- **TED only (v1).** udbud.dk's below-threshold notices are not included. udbud.dk allows reuse with attribution, but only through its official API, which needs a registered system-provider account. Support is planned.
- The value is TED's total value or, if that's missing, the sum of the lot estimates. It's in the notice's own currency, and many notices publish no value.
- Currency conversion uses the latest ECB rate, not the rate on the publication date.
- English titles and summaries exist only when the buyer wrote them in English. Most Danish notices are Danish-only, apart from the English CPV category.
- The deadline is the earliest lot deadline for submitting a tender. For restricted and negotiated procedures it's the deadline to request participation instead (`deadlineType`).
- At most 1,000 notices per run and a 366-day date range.
- No contact persons, e-mails or phone numbers are returned. Contract winners are the company names and identifiers published on TED. A winner can occasionally be a sole trader trading under a personal name.
- Winner identifiers are paired with names only when TED's lists line up one-to-one. Otherwise the identifier is left empty rather than guessed.
- Not every award notice names a winner (e.g. cancelled lots or no tenders received).

### Technical

#### Input reference

| Field | Default | Notes |
|---|---|---|
| `countryPreset` | `denmark` | denmark, scandinavia (DK/SE/NO), nordic (+FI, IS), custom |
| `countries` | `["DNK"]` | Custom list (countryPreset = custom): DNK, SWE, NOR, FIN, ISL (2-letter codes accepted) |
| `keywords` | – | Any keyword matches (TED full-text `FT~`) |
| `cpvCodes` | – | 2–8 digits, padded to 8; check digit `-5` stripped; matched as TED does with `classification-cpv IN (…)` |
| `noticeTypes` | `["competition"]` | competition (cn-*), result (can-*), planning (pin-\*); `[]` = all |
| `publishedWithinDays` / `dateFrom` / `dateTo` | last 7 days | Max 366 days |
| `onlyOpenForBids` | false | Requires deadline ≥ run date |
| `onlyNew` / `monitorId` | false / `default` | State in named KV store `dk-tenders-state`, key `STATE_<monitorId>_<12-hex search hash>` = {ids (last 20,000), procedures→ids}. A v1 `SEEN_<monitorId>` list is migrated automatically |
| `preferredLanguage` | `eng` | eng, dan, swe, nor, fin |
| `minValue` | – | Drops notices with lower or missing `value` (falls back to `awardedValue`), notice currency |
| `includeDescription` | true | Description and English summary truncated to 2,000 chars |
| `outputMode` | `notices` | `lots` = one dataset row per lot (still billed per notice) |
| `convertCurrency` | true | Adds valueDkk, valueEur, awardedValueDkk, fxRateDate |
| `maxItems` | 50 | 1–1,000 |

#### OUTPUT record

`OUTPUT` in the run's default key-value store holds `status` (OK, PARTIAL, NO_DATA, CHARGE_LIMIT_REACHED, ERROR), `message`, `charged`, the TED `query`, `matched`, `returned`, `skippedSeen`, `skippedClosed`, `skippedValue`, filters and attribution. A failed run (e.g. invalid input) still writes `status: "ERROR"`, the message and `charged: false`.

#### Currency conversion

`valueDkk` and `valueEur` use the latest daily **ECB euro foreign exchange reference rates** (https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml), converted via EUR and rounded to 2 decimals. `fxRateDate` is the rate date, and OUTPUT `fx` lists the rates used. The ECB publishes these for information only. They're not a transaction rate, and estimated values are often set months before publication. If the ECB can't be reached, the conversion fields are null and OUTPUT has a warning; the run still succeeds.

#### Status and changes

`status` comes from eForms fields: `changed` when `change-notice-version-identifier` is set (corrigendum), `cancelled` when the change reason is `cancel`/`cancel-intent`, `noAward` when every lot result is `clos-nw`, `awarded` for other `can-*` notices, otherwise `published`. `changeOf` identifies the changed notice: usually its TED publication number, sometimes its eForms notice ID + version (UUID-01). Matching to notices you already have therefore also uses `procedureId`. In only-new mode, `relatedSeenIds` lists earlier notices you received from the same procedure (`procedureId`) or the notice it changes. `isExpired` compares the deadline with the run date.

#### Lots

Per-lot values, deadlines and CPV codes are mapped only when TED returns exactly one entry per lot. Otherwise those lot fields are null rather than guessed (the notice-level `value` and `deadline` are still filled).

#### Billing events

`actor-start` (once per run with at least one notice) and `tender-notice` (per notice). Notices are saved first and charged after. If your maximum cost per run is reached, the run returns what it could pay for (status PARTIAL), and only those notices are marked as seen.

#### Data source and licence

Data: TED API v3 (`api.ted.europa.eu`), Publications Office of the European Union. **Source: TED – Tenders Electronic Daily (https://ted.europa.eu), © European Union.** Reused under Commission Decision 2011/833/EU on the reuse of Commission documents. This Actor processes the data and is not endorsed by the Publications Office of the EU. Every run's OUTPUT carries this attribution.

# Actor input Schema

## `countryPreset` (type: `string`):

Buyer-country preset. Pick 'Custom list below' to choose countries yourself.

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

Only used when Countries = 'Custom list below'.

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

Optional. A notice matches if ANY keyword appears in its full text (any language), e.g. software, rådgivning, rengøring.

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

Optional CPV codes: 8 digits (72000000) or a 2-4 digit division/group padded with zeros (72 = IT services, 45 = construction, 79 = business services).

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

Which kinds of notices to return.

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

Quick date filter. Default 7 days. Leave empty when you use the custom From/To dates.

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

Custom range start, YYYY-MM-DD (inclusive). Don't combine with 'Published within'.

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

Custom range end, YYYY-MM-DD (inclusive). Default: today. Max range 366 days.

## `onlyOpenForBids` (type: `boolean`):

Drop notices whose deadline has passed or that have no deadline.

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

Optional. Drop notices whose estimated (or awarded) value is lower or not published. Value is in the notice's own currency (DKK/EUR/SEK/NOK).

## `onlyNew` (type: `boolean`):

Remember which notices you already received (per monitor ID) and return only new ones. Schedule the Actor daily for tender alerts. The first run returns everything that matches and saves the baseline.

## `monitorId` (type: `string`):

Name of this saved search for 'only new' mode. History is kept per monitor ID and search filters (dates and max notices can change freely).

## `preferredLanguage` (type: `string`):

Language for titles and buyer names when the notice has it; otherwise English or the original language.

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

Add the procedure description (up to 2,000 characters). Turn off for smaller output.

## `outputMode` (type: `string`):

Every notice always has a nested 'lots' array. 'One row per lot' flattens it for spreadsheets. Billing is per notice either way.

## `convertCurrency` (type: `boolean`):

Add valueDkk/valueEur using the latest ECB euro reference rates (SEK, NOK and ISK tenders become comparable).

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

Maximum number of notices to return in one run (each is one billable result). Newest first.

## Actor input object example

```json
{
  "countryPreset": "denmark",
  "countries": [
    "DNK"
  ],
  "keywords": [
    "software"
  ],
  "noticeTypes": [
    "competition"
  ],
  "publishedWithinDays": 7,
  "onlyOpenForBids": false,
  "onlyNew": false,
  "monitorId": "default",
  "preferredLanguage": "eng",
  "includeDescription": true,
  "outputMode": "notices",
  "convertCurrency": true,
  "maxItems": 20
}
```

# Actor output Schema

## `tenders` (type: `string`):

Dataset view 'overview': publication date, title, buyer, value, deadline, link.

## `details` (type: `string`):

Dataset view 'details': every normalized field incl. CPV codes, notice type and procedure.

## `allFields` (type: `string`):

All dataset items.

## `summary` (type: `string`):

Key-value record OUTPUT: status (OK, PARTIAL, NO_DATA, CHARGE_LIMIT_REACHED, ERROR), message, charged, query, matched/returned counts, skipped (already seen / closed / below minValue), filters, attribution. On failure: status ERROR, message, charged:false. Also: updatesToSeen, stateKey, outputMode, datasetRows, fx (ECB date and rates), warnings.

## `awards` (type: `string`):

Dataset view 'awards': buyer, buyer CVR, winners and awarded value (for runs with noticeTypes 'result').

## `lots` (type: `string`):

Dataset view 'lots' (use outputMode 'lots'): one row per lot with lot title, CPV, value and deadline.

## `updates` (type: `string`):

Dataset view 'updates': status, change reason and links to notices you already received.

# 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 = {
    "countryPreset": "denmark",
    "countries": [
        "DNK"
    ],
    "keywords": [
        "software"
    ],
    "noticeTypes": [
        "competition"
    ],
    "publishedWithinDays": 7,
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("nordic_open_data/dk-public-tenders").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 = {
    "countryPreset": "denmark",
    "countries": ["DNK"],
    "keywords": ["software"],
    "noticeTypes": ["competition"],
    "publishedWithinDays": 7,
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("nordic_open_data/dk-public-tenders").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 '{
  "countryPreset": "denmark",
  "countries": [
    "DNK"
  ],
  "keywords": [
    "software"
  ],
  "noticeTypes": [
    "competition"
  ],
  "publishedWithinDays": 7,
  "maxItems": 20
}' |
apify call nordic_open_data/dk-public-tenders --silent --output-dataset

```

## MCP server setup

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

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/XnQcY8o4MkKRyUowe/builds/S7foweiL8lAgRfibW/openapi.json
