How to pull German tender data with the Vergabe API
What is oeffentlichevergabe.de?
oeffentlichevergabe.de is Germany’s national notice service, run as the Datenservice Öffentliche Vergabe. Federal, state, and municipal buyers publish here, which makes it the single place where German contract notices, prior information notices, and award notices land in one machine-readable format.
Two things make it worth pulling directly. It carries below-threshold notices that never reach TED, and it publishes everything as eForms and OCDS under an open data licence. Roughly 1,000 to 1,500 notices arrive on a normal working day.

Do you need an API key?
No. There is no registration, no key, and no quota to apply for. The endpoints below are open to anyone, and the only thing expected of you is that you do not hammer them.
The two official endpoints
| Endpoint | Returns |
|---|---|
GET /api/notice-exports?pubDay=YYYY-MM-DD&format=ocds.zip | Every notice published on one day, zipped |
GET /api/notices/{id}?format=domain | One notice in full detail |
There is no query or filter parameter on either. You ask for a day or for an identifier, and any filtering by sector, region, or value happens on your side after download. Every notice also has a human-readable page on the portal at /ui/de/notices/{id}.
Download a day of notices in Python
Ask for a day, unzip it in memory, and read each release:
import io
import json
import zipfile
import requests
URL = "https://oeffentlichevergabe.de/api/notice-exports"
resp = requests.get(URL, params={"pubDay": "2026-08-07", "format": "ocds.zip"})
archive = zipfile.ZipFile(io.BytesIO(resp.content))
for name in archive.namelist(): # "<noticeId>-<version>.json"
release = json.loads(archive.read(name))["releases"][0]
tender = release["tender"]
buyer = next((p["name"] for p in release["parties"] if "buyer" in p["roles"]), "")
cpv = [i["classification"]["id"] for i in tender.get("items", [])]
value = tender.get("value", {})
print(
release["id"],
release["tag"][0], # planning | tender | award
tender["title"][:60],
buyer[:40],
value.get("amount", "n/a"), value.get("currency", ""),
",".join(cpv),
)
Each file holds one release under releases[0], and the winner of an award is the party carrying the supplier role. Read tender.value defensively, because most notices do not carry one.
Fetch one notice in full
The OCDS export is deliberately slim. Deadlines, procedure type, award criteria, and the submission platform are not in it, so anything time-sensitive needs a second call to the detail endpoint. The domain format is the one to use, because it flattens the six eForms flavours the portal has accumulated into one shape. ocds and xml are also available.

Note the Procedure and Lot tabs on that page. Most detail hangs off the lots, not the notice, so a deadline is per lot:
notice = requests.get(
f"https://oeffentlichevergabe.de/api/notices/{notice_id}",
params={"format": "domain"},
).json()
for lot in notice["lots"]:
terms = lot["submissionTerms"]
deadline = terms.get("deadlineReceiptTenders") or terms.get("deadlineReceiptRequests")
print(lot["id"], deadline, terms.get("submissionUrl"))
Two-stage procedures use deadlineReceiptRequests for the participation deadline, single-stage ones use deadlineReceiptTenders, and plenty of notices carry neither. If you do not know which CPV code covers your sector, look it up with our CPV code search.
Limits and good practice
- Export window. Days are served only once fully elapsed, and anything before 1 December 2022 is rejected. Request yesterday, not today.
- No filtering. Neither endpoint takes a CPV, region, or value parameter. Narrowing to your sector means pulling every day in full and discarding most of it.
- No deadlines in the bulk file. The export carries no submission deadline, so anything time-sensitive needs the detail endpoint. Filter the day down to the notices in your sector first and call detail only for those; award notices never need it.
- Versions. Notices get corrected and republished, and
noticeVersionincrements each time. Key your storage on the identifier, keep the highest version, and skip what you already hold. - Missing buyer addresses. Older eForms flavours leave the buyer address out on many notices. Default the country to Germany rather than dropping the record.
- German only. Titles and descriptions are published in German, with no translation layer. Comparing across countries means translating on your side.
- Be polite. One request every few seconds is plenty, and a daily sync keeps you current.
How this compares with TED
German notices above the EU thresholds are forwarded to TED, so those you can get from either source. The detail record even tells you when, under tedPublication. Everything below threshold exists only on oeffentlichevergabe.de, and that is the larger share of German procurement by notice count.
The trade-off is coverage against depth. TED answers questions across all 27 member states in one query, in every EU language. This portal answers questions about Germany in more detail, and it does so a step earlier in the publication chain. If Germany is your market, you want both, which means reconciling two identifier schemes, two vocabularies, and two update rhythms.
That is the whole loop: request the day, unzip it, parse the releases, call back for the deadlines you need, and keep the highest version of each identifier. The data is free and the endpoints are stable. The parsing is the part that takes time.