Developer guides

How to find who won a government contract: award data from TED, Find a Tender and national portals

Winners of public contracts are published in award notices on TED, Find a Tender and national portals, free of charge; the work is in matching a winner name to a company, handling frameworks and joint bids, and leaving out private persons. Source: Arcadium Data, data as of .

"Who won the contract" is a question journalists, rival bidders, sales teams and investors ask all the time. The answer is public by law: EU procurement rules oblige buyers above the thresholds to publish a contract award notice, and UK buyers do the same under the Procurement Act 2023. The notices are free to read and reuse. They are also inconsistent: the winner may be missing, spelled three ways, a joint venture or a sole trader.

This guide shows the official sources, a minimal reader, and the traps. Then it shows the shortcut: supplier profiles and winner lookups by company website.

The official route: award notices

Where to look

A minimal winner reader for TED

The eForms fields to ask for: winner-name, winner-internet-address, result-value-notice (and per lot total-value, tender-value), buyer-name, buyer-country and classification-cpv. Notice types for results start with can- (contract award notice).

curl

curl -s https://api.ted.europa.eu/v3/notices/search \
  -H 'Content-Type: application/json' \
  -d '{"query":"publication-date=20261023 AND notice-type=can-standard AND winner-name=*","fields":["publication-number","buyer-name","buyer-country","winner-name","winner-internet-address","result-value-notice","classification-cpv"],"limit":250,"page":1,"paginationMode":"PAGE_NUMBER"}'

Python

import re
import requests

def pick(obj, lang_order=("eng",)):
    """TED text fields are objects keyed by 3-letter language, each a list. Return one string."""
    if isinstance(obj, dict):
        for lang in (*lang_order, *obj.keys()):
            if obj.get(lang):
                return obj[lang][0]
        return None
    if isinstance(obj, list):
        return obj[0] if obj else None
    return obj

body = {
    "query": "publication-date=20261023 AND notice-type=can-standard AND winner-name=*",
    "fields": ["publication-number", "buyer-name", "winner-name", "winner-internet-address", "result-value-notice"],
    "limit": 250, "page": 1, "paginationMode": "PAGE_NUMBER",
}
r = requests.post("https://api.ted.europa.eu/v3/notices/search", json=body, timeout=60)
r.raise_for_status()
for n in r.json()["notices"]:
    winners = n.get("winner-name") or []
    names = [pick(w) if isinstance(w, dict) else w for w in (winners if isinstance(winners, list) else [winners])]
    # Drop what could be a private person before you publish: keep names with a company form.
    firm = [x for x in names if x and re.search(r"\b(GmbH|AG|S\.?A\.?|S\.?L\.?|SRL|Ltd|Limited|B\.?V\.?|N\.?V\.?|AB|AS|Oy|Sp\. z o\.o\.)\b", x, re.I)]
    print(n["publication-number"], firm)

TypeScript

const body = {
  query: 'publication-date=20261023 AND notice-type=can-standard AND winner-name=*',
  fields: ['publication-number', 'buyer-name', 'winner-name', 'winner-internet-address', 'result-value-notice'],
  limit: 250,
  page: 1,
  paginationMode: 'PAGE_NUMBER',
};
const res = await fetch('https://api.ted.europa.eu/v3/notices/search', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(body),
});
const { notices } = (await res.json()) as { notices: Record<string, unknown>[] };
console.log(notices.length, 'award notices with a winner name');

Joining a winner name to a company

A name alone is a weak key. Use these signals in order, and store which one matched: (1) a website stated in the notice (TED: winner-internet-address), which is the strongest because the buyer or winner typed it; (2) a registry identifier stated in the notice (TED: winner-identifier; UK OCDS: parties[].identifier with scheme GB-COH), checked against the register; (3) an exact legal-name match inside one country, after you strip legal forms and punctuation. Do not match on name across countries: "Siemens" is a group of dozens of legal entities, and a Portuguese subsidiary winning a rail contract is not the Munich parent. Keep the entity that won and the group it belongs to as two separate fields.

What you can honestly say

A winners table built from notices is a lower bound. Some awards are never published (below-threshold or exempt contracts), some notices omit the winner, and values are partial. Say "won at least N contracts worth at least EUR X in the notices we read between these dates", not "earned". Notices also appear days to weeks after the decision, so the latest month is always incomplete.

Traps

The fast route: winners by company, supplier profiles, leaders

Arcadium Data keeps the award notices of TED, Find a Tender and the national boards in one deduplicated table, with winners matched to company websites where the notice states one, frameworks flagged and winner names that could be private persons withheld (winnersWithheld counts them).

All awards a company won, by its website, with no key:

Calls on this page work without a key from one IP (10 requests a minute, 100 a day, short answers, point lookups). A free key lifts the limits: sign up. Routes, parameters and fields: OpenAPI.

curl

curl -s "https://api.arcadiumdata.io/v1/tenders?winnerDomain=siemens.com&limit=1"

Response (abridged)

{
  "count": 1,
  "data": [{
    "id": "ted:726751-2026",
    "source": "ted",
    "kind": "award",
    "buyer": { "name": "Infraestruturas de Portugal S.A.", "country": "PT" },
    "cpvCodes": ["71311230"],
    "awardValue": { "amount": 498817, "currency": "EUR", "eur": 498817 },
    "winners": ["SIEMENS MOBILITY, UNIPESSOAL, LDA"],
    "winnerDomains": ["siemens.com"],
    "winnersWithheld": 0,
    "publishedOn": "2026-10-21",
    "url": "https://ted.europa.eu/en/notice/-/detail/726751-2026"
  }]
}

A supplier profile aggregates every public award of one supplier since October 2023: buyer concentration (buyerHhi, top-buyer share), sectors, countries, monthly timeline, contracts ending in the next 18 months and the 20 latest awards. Frameworks and implausible values are left out of the totals, and co-winners split a value equally.

curl

curl -s "https://api.arcadiumdata.io/v1/tenders/suppliers/siemens.com"

Who wins most in a country or CPV division, for the current month and the eleven before, from GET /v1/tenders/leaders (anonymous preview of five rows per list; it needs country, cpv or q). The free tender winners tool runs the same query in the browser.

Python

import requests

def awards_won(domain: str):
    r = requests.get("https://api.arcadiumdata.io/v1/tenders?winnerDomain=" + domain + "&limit=5", timeout=60)
    r.raise_for_status()
    for n in r.json()["data"]:
        value = (n.get("awardValue") or {}).get("eur")
        yield n["publishedOn"], n["buyer"]["name"], value, n["url"]

for row in awards_won("siemens.com"):
    print(row)

TypeScript

type Award = { publishedOn: string; buyer: { name: string | null }; awardValue: { eur: number | null } | null; url: string };

const res = await fetch('https://api.arcadiumdata.io/v1/tenders?winnerDomain=siemens.com&limit=5');
const { data } = (await res.json()) as { data: Award[] };
for (const a of data) console.log(a.publishedOn, a.buyer.name, a.awardValue?.eur, a.url);

Be clear about what you get. A winner domain is only set when the notice names one winner and one website (or our matching ties the company to a site), so the share of winners with a domain is far lower in the UK than in countries whose notices state websites. A profile is built from public notices and is not a statement about the company's total revenue. Search by name or buyer needs a free key.

Comparison

Read notices yourselfArcadium Data tenders API
SourcesOne integration per portalTED, Find a Tender, Contracts Finder and national boards
Winner to companyName matching by handwinnerDomains and supplier profile by domain
Duplicates across portalsYou find themListed once
Frameworks and valuesYou flag and sanity-checkFlagged; implausible values left out of totals
Private personsYour responsibilityWithheld, counted in winnersWithheld
Who wins most in a marketAggregate yourself/v1/tenders/leaders
CostEngineering timeAnonymous lookups free; free key for search

Get the data without the plumbing

Arcadium Data serves these datasets from one REST API and one MCP server, with sources, licences and dates on every answer. No key is needed to try the routes in this guide.

Sources

Last updated 2026-10-25. This guide is ours and independent of the organisations it describes. Found a mistake? Tell us and we correct it.

Questions

Where can I see who won a government contract?

In the contract award notice. For the EU that is TED, for the UK Find a Tender and Contracts Finder, and for smaller contracts the national portal of the country. All are free to read.

Why does an award notice show no winner?

A lot can close without an award, the buyer may defer some fields, or the winner name is not stated. In TED these notices exist in noticeable numbers, so count them separately.

Can I see how much a company earned from public contracts?

Only approximately. Notices state awarded values per contract; frameworks state a ceiling and call-offs are not always published. Treat totals as a lower bound and split joint awards.

Are sole traders shown as winners?

Not by us. Winner names that could be private persons are withheld and counted in winnersWithheld, because they are personal data.

How do I find all contracts won by one company?

Call GET /v1/tenders?winnerDomain=<domain> for the notices, or GET /v1/tenders/suppliers/<domain> for the profile. Both work without a key in small doses.

Cite as: Arcadium Data, "How to find who won a government contract: award data from TED, Find a Tender and national portals", https://arcadiumdata.io/guides/who-won-government-contracts-data/, data as of 2026-10-25.