Developer guides

Poland BZP and e-Zamowienia open data: the notice board API, paging and TED overlap

Poland publishes below-threshold procurement notices on the BZP board of the e-Zamowienia platform, and its notice list endpoint answers anonymous requests; EU-threshold procedures appear there too but are also in TED. Source: Arcadium Data, data as of .

The Biuletyn Zamowien Publicznych (BZP) is the Polish official bulletin for public procurement, now part of the state e-Zamowienia platform. Polish buyers are numerous and publish a large volume of notices every day.

The board page is a front end for a JSON API. The official documentation of that API is thin, so this guide describes what the endpoint does as we observe it (checked 2026-10-25) and marks what you should verify yourself.

The official route: the notice board endpoint

The request

GET https://ezamowienia.gov.pl/mo-board/api/v1/notice returns notices ordered by publication time. Parameters that work without a key: NoticeType (ContractNotice for a call for tenders, TenderResultNotice for an award), PublicationDateFrom, PublicationDateTo, PageSize (up to 500) and SearchAfter, the cursor.

Paging, the part that surprises people

Paging is by cursor, not by page number. Pass the objectId of the last row as SearchAfter to get the next batch. PageNumber is ignored, and PublicationDateTo is not strictly applied, so a page can run into the next day. Stop when rows leave your day instead of trusting the filter, and drop rows from other days.

curl

curl -s -G "https://ezamowienia.gov.pl/mo-board/api/v1/notice" \
  --data-urlencode "NoticeType=ContractNotice" \
  --data-urlencode "PublicationDateFrom=2026-10-23T00:00:00" \
  --data-urlencode "PublicationDateTo=2026-10-24T00:00:00" \
  --data-urlencode "PageSize=100"

Python

import requests

URL = "https://ezamowienia.gov.pl/mo-board/api/v1/notice"

def bzp_day(day: str, notice_type: str = "ContractNotice"):
    after = None
    while True:
        params = {"NoticeType": notice_type, "PublicationDateFrom": day + "T00:00:00",
                  "PublicationDateTo": day + "T23:59:59", "PageSize": 100}
        if after:
            params["SearchAfter"] = after
        rows = requests.get(URL, params=params, timeout=60).json()
        day_rows = [r for r in rows if str(r.get("publicationDate", ""))[:10] == day]
        yield from day_rows
        if not rows or len(day_rows) < len(rows):
            return
        after = rows[-1]["objectId"]

for n in bzp_day("2026-10-23"):
    print(n["bzpNumber"], n["organizationName"], n["cpvCode"], n["submittingOffersDate"])

Fields and traps

Reading result notices

A TenderResultNotice is the award. Its contractors list holds the winners' names and countries, and procedureResult says whether the procedure ended with a contract, was cancelled or had no offers; keep only the first for a winners list. A buyer often publishes several result notices for one procedure (one per part), so group by tenderId before you count procedures. Joining a result to its call is the same key. Polish company names carry forms such as "sp. z o.o.", "S.A." and "sp. j."; sole traders appear by personal name, so apply a company-form filter before you publish winners. The tax number (NIP) sometimes appears in the HTML next to the contractor, which is a company identifier and the right key for joining to the register.

What is covered

The BZP board holds procurement above a national minimum (about 130,000 PLN at the time of writing; below it the Public Procurement Law does not apply) and below the EU thresholds. Above the EU thresholds the notice goes to TED, and the Polish copy is flagged by isTenderAmountBelowEU. Buyers also publish voluntary notices, and some platforms outside e-Zamowienia hold quotations that never reach BZP, so the board is complete for regulated procedures, not for all public spending.

Rate and robustness

No quota is published. We read a day in a handful of calls of 500 rows with a pause between them. Since this is an undocumented front-end API, expect parameters to change and add a schema check that fails loudly when a field disappears. Licence terms are those of the platform (public information, re-use allowed with attribution); read the current terms before you republish.

A daily loop that survives the quirks

Start from the last cursor you stored, not from page one. For each notice type, request a day, sort by publication time, and keep going with SearchAfter until a row from the next day shows up or a page is empty. Re-read the previous two days on every run: buyers publish corrections, and a late notice carries its original publication date. Upsert by objectId, store the raw JSON and the HTML you parsed values from, and log the count per day. A sudden drop to zero is a signal that the parameters changed, and should stop the job instead of writing an empty day.

Two parsing habits help. First, take values from the labelled sections of the HTML (the estimated value, the awarded value, the currency) and fail loudly when the label text changes. Second, normalise the buyer name and the city: the same ministry appears with different spellings across years, and the region you derive from the city or NUTS code should be checked against a lookup table. Region views for Poland (16 voivodeships) are an obvious product once the NUTS codes are clean, and they let a regional supplier see only the buyers within reach.

Finally, expect volume: the board carries several thousand notices on a normal weekday across both notice types, and a year of history is a few hundred thousand rows. That is small for a database but large enough that you should page politely and cache the raw responses.

The fast route: Polish awards and buyers in one schema

Arcadium Data runs the day loop for BZP, parses the HTML labels for NUTS and values, removes contact data, keeps the below-threshold notices and merges TED for the rest. The twelve-month IT services market in Poland (CPV 72) is open without a 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/markets?country=PL&cpv=72"

Python

import requests

data = requests.get("https://api.arcadiumdata.io/v1/tenders/markets?country=PL&cpv=72", timeout=30).json()
print(data["awards"], data["valueEur"])
for w in data["topWinners"]:
    print(w["name"], w["awards"], w.get("domain"))

TypeScript

const res = await fetch('https://api.arcadiumdata.io/v1/tenders/markets?country=PL&cpv=72');
const data = (await res.json()) as { awards: number; valueEur: number; topWinners: { name: string; awards: number; domain?: string }[] };
console.log(data.awards, data.valueEur);
for (const w of data.topWinners) console.log(w.name, w.awards, w.domain);

Response (abridged)

{
  "awards": 1499, "awardsWithWinner": 1116, "valueEur": 3810721872, "medianValueEur": 381737,
  "awardToEstimate": { "median": 0.969 },
  "topWinners": [{ "name": "Asseco Poland S.A.", "domain": "asseco.com", "awards": 48 }],
  "topBuyers": [{ "name": "Narodowy Bank Polski", "awards": 27 }]
}

Also see Polish tenders. Notice search (GET /v1/tenders with country=PL) needs a free key.

Comparison

BZP board endpointArcadium Data
KeyNoneNone for summaries; free key for notice search
PagingSearchAfter cursor; date filter looseCursor
Values and NUTSParse the notice HTMLParsed
TED overlapFilter on isTenderAmountBelowEUFiltered
Contact dataIn the HTMLNever read
StabilityUndocumented, can changeMonitored by daily checks

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

Is there an official API for Polish public procurement?

The BZP board of e-Zamowienia is backed by a JSON endpoint that answers anonymous requests, but its public documentation is thin. Treat it as a stable-enough front-end API and validate the schema on each run.

How does paging work on the BZP endpoint?

Use the objectId of the last row as SearchAfter. PageNumber is ignored and the end date filter is not strictly applied, so stop when rows leave your day.

Are BZP and TED notices duplicates?

For EU-threshold procedures, yes. Rows with isTenderAmountBelowEU set to false are also in TED. Below the thresholds, BZP is the only source.

Where are values and regions in a BZP notice?

In the notice HTML under numbered section labels. The list endpoint gives buyer, CPV, title, deadline and contractors, but not values.

Can I store the contact details of buyers?

No. The notice HTML includes an email and a phone number of a contact person, which is personal data. Parse around it.

Cite as: Arcadium Data, "Poland BZP and e-Zamowienia open data: the notice board API, paging and TED overlap", https://arcadiumdata.io/guides/poland-bzp-ezamowienia-open-data/, data as of 2026-10-25.