DEV Community

Emir Mrkaljević
Emir Mrkaljević

Posted on Originally published at emirmrkaljevic.github.io

Free EU tender alerts with the official TED API (Python, no scraping)

Every public tender above the EU thresholds is published on TED (Tenders Electronic Daily), and TED has an official, free search API. Paid alert services charge hundreds of euros a year for what is essentially a saved query. Here's the DIY version.

The query

TED's search API (https://docs.ted.europa.eu/api/latest/search.html) takes an "expert query", e.g. IT services (CPV 72000000) from German or Austrian buyers published today:

import requests, datetime
q = "classification-cpv IN (72000000) AND buyer-country IN (DEU AUT) AND publication-date=" + datetime.date.today().strftime("%Y%m%d")
r = requests.post("https://api.ted.europa.eu/v3/notices/search", json={
    "query": q, "fields": ["publication-number", "notice-title", "buyer-name", "deadline-receipt-tender-date-lot"], "limit": 100})
for n in r.json()["notices"]:
    print(n["publication-number"], n.get("notice-title"))
Enter fullscreen mode Exit fullscreen mode

Turning it into alerts

  1. Store the publication numbers you've already sent (a small JSON file or a sheet).
  2. Run the script daily (cron, GitHub Actions) and send only new notices to email/Slack.
  3. Link each notice as https://ted.europa.eu/en/notice/-/detail/<publication-number>.

Gotchas

  • Field names are multilingual objects; pick your language and fall back to the original.
  • Values and winners live in different fields for contract notices vs award notices.
  • Paging: use the iterationNextToken/page parameters, and keep queries narrow.

TED data is EU open data and can be reused, including commercially, with attribution.

Disclosure: I maintain a hosted version, TED Tenders Scraper on Apify, that flattens all of this into one row per notice with an "only new" alert mode ($2.50 per 1,000 notices). The snippet above is enough for a single saved search. If you'd rather get a ready Excel every Monday with all open IT tenders across Europe (English titles included), I also publish EU IT Tenders & Grants Weekly.

Top comments (1)

Collapse
 
alexcodebytes profile image
Oleksandr •

Providing a DIY blueprint for TED API monitoring is a fantastic service to the community. You are completely right—paying hundreds of euros annually for basic saved queries and cron jobs is an unnecessary tax on small agencies and indie developers.
Your technical breakdown highlights some very real production challenges when interacting with EU open data:

  1. The Multilingual Payload Trap: TED's multilingual object structure is notoriously tricky. If your script strictly looks for notice-title["en"] but the notice was only published in German, the pipeline will throw a silent key error or drop the notification entirely.
  2. Contract vs. Award Variance: The structural shift between procurement notices and award data schemas is a massive blind spot. Expecting a single static parsing model to handle both without breaking on schema mismatches is a common cause of script failures. To turn your snippet into a bulletproof, production-grade DevOps setup without spinning up heavy database servers, here are two infrastructure patterns you can consider: • Serverless State Management via GitHub Actions & Artifacts: If you are running this daily via a GitHub cron workflow, you don't need an external database or cloud storage to keep track of already sent publication-number objects. You can use GitHub Actions Cache or programmatically upload the JSON file as a build artifact at the end of the run, downloading it at the start of the next day's execution. This keeps the entire alerting engine 100% serverless, zero-cost, and self-contained within a single repository. • Decoupled Multi-Language Normalization: Instead of writing complex language extraction logic directly inside your notification loops (Slack/Email), pass the raw multilingual payload through a lightweight normalization mapper first. Enforce a strict fallback chain (e.g., en -> de -> fr -> first available key) at the ingestion gate. This guarantees that your alert templates receive a predictable flat string structure, ensuring that your notification pipeline never crashes over a missing localized field.