DEV Community

Steadfetch
Steadfetch

Posted on Edited on

How to monitor EU public tenders as JSON — TED procurement data without an API key

If you sell to the public sector, run market research, or build bid alerts, you need procurement notices as clean JSON: buyer, title, value, deadline, URL. The official source for EU procurement is TED (Tenders Electronic Daily), the Supplement to the Official Journal of the EU. If you are looking at Spain's national platform (PLACSP) for clean data, note that every EU-threshold contract from Spanish buyers is published on TED too, in one standard format.

The good news: TED's Search API is public and needs no key. The catch: you still have to build the query payload, respect its fair-use limits, normalize fields, and remember which notices you already saw yesterday.

This post shows both paths: the raw API, and a small monitor that does the boring parts for you.

Option 1: call the TED Search API directly

TED exposes a keyless JSON endpoint:

curl -s -X POST https://api.ted.europa.eu/v3/notices/search \
  -H 'Content-Type: application/json' \
  -d '{"query":"(FT ~ (\"software\"))","fields":["publication-number","notice-title","buyer-name"],"limit":"10"}'
Enter fullscreen mode Exit fullscreen mode

This is free and official. What you still have to build yourself:

  • the query DSL (full-text syntax, CPV codes, country filters, publication-date windows);
  • rate-limit handling (TED's fair use: 700 requests/minute, 600 downloads per 6 minutes);
  • delta handling — TED has no "only what's new since my last run";
  • normalizing notices into one schema your app can consume.

That last pair is what usually bites: a monitor that re-alerts on notices you already sent, or a parser that breaks when a field is missing.

Option 2: a monitor that returns only new rows in a fixed schema

steadfetch/ted-tender-monitor (Apify Store) wraps TED — and, in the same run, USAspending for US federal contract awards — and returns one row per notice in a unified camelCase schema. Key properties:

  • Saved queries: 1–50 per run. TED queries accept text, cpv, countries, minValueEur; USAspending queries accept text, agencies, naics.
  • Delta-only: each run skips (source, noticeId) pairs it already delivered. A daily schedule becomes a clean stream of new notices, and a run with 0 new rows emits 0 result charges (monitor-mode checks are still billed).
  • Deadline-first: every TED row carries deadlineAt, the published submission deadline (or request-to-participations for restricted/negotiated procedures). Award, planning and modification notices have no deadline, so they are never delivered from TED.
  • Tested daily against the live sources (last verified: 2026-10-30, per the Store listing); schema-invalid rows are discarded instead of delivered.

Example input — software tenders from Spanish buyers worth €100k or more:

{
  "source": "ted",
  "queries": [
    { "text": "software", "countries": ["ES"], "cpv": ["48000000"], "minValueEur": 100000 }
  ],
  "maxResults": 50,
  "monitorMode": true
}
Enter fullscreen mode Exit fullscreen mode

Running it from your code

REST (any language):

curl -s -X POST \
  "https://api.apify.com/v2/acts/steadfetch~ted-tender-monitor/run-sync-get-dataset-items?token=YOUR_API_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"source":"ted","queries":[{"text":"software","countries":["ES"],"minValueEur":100000}],"maxResults":50,"monitorMode":true}'
Enter fullscreen mode Exit fullscreen mode

JavaScript (npm install apify-client):

import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });

const run = await client.actor('steadfetch/ted-tender-monitor').call({
  source: 'ted',
  queries: [{ text: 'software', countries: ['ES'], minValueEur: 100000 }],
  maxResults: 50,
  monitorMode: true,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
Enter fullscreen mode Exit fullscreen mode

Python (pip install apify-client):

import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])

run = client.actor("steadfetch/ted-tender-monitor").call(
    run_input={
        "source": "ted",
        "queries": [{"text": "software", "countries": ["ES"], "minValueEur": 100000}],
        "maxResults": 50,
        "monitorMode": True,
    }
)

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)
Enter fullscreen mode Exit fullscreen mode

What a row looks like

Real example from the Actor's documented test fixture (a USAspending award; TED rows use the same fields):

{
  "source": "usaspending",
  "noticeId": "CONT_AWD_36C25024F0188_3600_47QTCA19D00MV_4732",
  "title": "GEO TAB - SAGINAW",
  "buyer": "Department of Veterans Affairs",
  "country": "US",
  "cpv": [],
  "value": 13162,
  "currency": "USD",
  "publishedAt": "2023-12-01",
  "deadlineAt": null,
  "periodEndAt": "2024-11-30",
  "url": "https://www.usaspending.gov/award/CONT_AWD_36C25024F0188_3600_47QTCA19D00MV_4732",
  "firstSeenAt": "2026-10-19T12:00:00.000Z",
  "scrapedAt": "2026-10-19T12:00:00.000Z"
}
Enter fullscreen mode Exit fullscreen mode

For TED rows: noticeId is the TED publication number, buyer the contracting authority, cpv the CPV codes, deadlineAt the tendering deadline, and value/currency exactly what the source publishes (no conversion, no invented amounts). url points to the official notice page.

Pricing

Pay per result, not per run: $4.00 per 1,000 new rows (plus a negligible per-run start event), and $1.00 per 1,000 records checked in monitor mode. A run that finds no new rows emits 0 rows and 0 result charges; monitor-mode checks are still billed. On the Apify free plan that is roughly 1,249 tenders out of the $5 monthly credit. The Store listing shows the current price with plan discounts.

Scope and limits (the honest part)

  • TED covers EU-threshold notices — including Spanish buyers. Below-threshold national notices stay on PLACSP; this Actor does not read PLACSP.
  • US awards come from USAspending (USD amounts, no CPV).
  • Public data only: TED contact/person/UBO fields are never requested and are scrubbed before delivery.
  • Both upstream APIs are official, keyless and, per their terms, freely reusable for commercial purposes.

Repo with ready-to-copy examples: github.com/steadfetch/examples/ted-tender-monitor (API, JavaScript, Python and a real sample output).

We build this Actor at Steadfetch. Price and numbers are from the Actor's public README; nothing here is sponsored by Apify.

Top comments (0)