Search Ukraine's ProZorro public procurement — tenders, buyers, awarded suppliers, values, items (CPV) and status from the official OpenProcurement API. Filter by keyword, category, value and date; ideal for reconstruction-contract monitoring. Open data, no key.
0.2.12 — 2026-10-09 — status and region decided from the feed, date ranges in Kyiv days, new "accepting bids" status, masked tenders left out; README rewritten
A status or region filter no longer reads every tender in full. ProZorro's feed can carry the status and the buyer next to each tender id; the run now asks for them and reads in full only the tenders that can still match. In the newest 5,000 changes on 2026-10-08, 58 of 100 were not active…, and 87 of 100 lay outside the largest region (Київська область), 94 or more outside any other — tenders the run used to download one by one and throw away. The same maxScan therefore reaches further back.
maxScan counts the tenders read in full and has no fixed value in the form any more. Left empty, the limit is 20,000, as before. A keyword typed in Latin letters is the exception: tender text is Ukrainian, so such a run stops once it has found nothing in 3,000 tenders read in full, and its status message says so; a maxScan you set yourself is read to its end. A region typed in Latin letters ends the run after 20,000 changes without a match, with or without a maxScan: none of the 2,625 buyer regions in the rows measured on 2026-10-08 carried a Latin letter, and a region is decided from the listing without reading a tender, so maxScan never fills.
New status tendering: tenders in active.enquiries or active.tendering whose bidding deadline is not over — the tenders a supplier can still ask about or bid on. (Of 100 rows in those two stages measured on 2026-10-08, 3 had a deadline one to six days in the past; such tenders are left out.) active keeps its meaning — every active… status, including evaluation and awarded.
Date ranges are Kyiv calendar days, the calendar of ProZorro's own timestamps: a range for 15 September returns rows dated 15 September. Ranges were cut at UTC midnight — of the 50 rows of a one-day range measured on 2026-10-08, 25 carried the next day's date. The presets start at Kyiv midnight too.
A custom date range starts where it ends. The feed's position is a moment in time, so a custom range is entered at its dateTo instead of being reached from today, tender by tender; a range that ended a working day or more earlier was out of reach at the default maxScan. Custom dates are validated: a date that does not exist stops the run with a message that names the field (a malformed dateTo used to be ignored, and the run delivered tenders newer than the range). So do custom with neither date (it used to mean every date) and a dateFrom after the dateTo.
Masked tenders are not delivered. ProZorro masks some records: the title is a placeholder and buyer, value and items are zeros. Two of the 807 rows of a run measured on 2026-10-08 were such records, delivered and charged. They are now left out and counted in the status message.
A run that finishes ends with a status message: tenders delivered, changes looked at, the change the run stopped at and why it stopped — date range covered, maxResults, maxScan, maximum charge, nothing found, 3,000,000 changes or the end of the feed. While the run walks, its status line says how far it is; a run that is aborted or cut off by its time limit keeps that line and has no closing message (the Apify SDK ends an aborted run at once). Tenders ProZorro did not return are counted in the message instead of being skipped in silence. Dates given next to a preset are named as not used. Only a custom range whose dateTo is over is called "the whole date range"; a preset, a range without dateTo and a dateTo that is not over are said to end at the start of the run. The run stops at its maximum charge, at once when the charge is used up; a charge that covers no tender stops it before the first request. The OUTPUT record in the key-value store keeps scanned and pushed and gains looked, skippedByListing, unreadable, masked, reachedBackTo and stopped.
Interruptions at ProZorro are ridden out. Every request used to have three tries within one second: a listing page that did not come ended the run with fetch failed (HTTP 502) and nothing else, and a tender that did not come was left out without a word. Now a listing page is asked for seven times, with pauses that add up to 97 seconds. A tender is asked for three times; when tenders do not come, the run asks the feed whether ProZorro is there at all — if it is, the missing tenders are asked for once more and what still does not come is counted as unreadable; if it is not, the run waits for it for up to a minute and a half and then reads the rest. When ProZorro does not come back — or its feed answers and a whole batch of tenders does not — the run fails with a message that says how far it got and how many tenders it had delivered, and writes its OUTPUT record first. Checked against a simulated feed only (interruptions of 3, 30 and 60 seconds cost no tender there; one of five minutes ended the run with that message): no measured run met an interruption.
A keyword with an apostrophe finds every spelling. Tender text writes the Ukrainian apostrophe with several signs — in the 2,878 tenders of the measured rows ’ in 369, ' in 237, a backtick in 20 — and the keyword was compared as written: комп'ютер typed with the plain sign found 18 of the 63 tenders that carry the word, typed with ’ 43. The signs are now read as one, in the keyword and in the tender text (counted on the saved rows; no platform run of this build yet).
The README described another Actor. Its examples used input keys this Actor does not have (queries, maxResultsPerQuery, with a literal undefined value), its input table listed phantom fields (maxConcurrency, includeRaw) and its text described a company registry ("relevance-ranked name search or exact registry-ID lookup", "legal form, formation date"). Every example input is now a real one and was run on the platform (the curl line itself was not run as written); the output table says how often each field was filled in a measured run.
The Python example works with the current Apify client. It read the results with run['defaultDatasetId']; with apify-client 3 for Python call() returns a run object and that line raised TypeError: 'Run' object is not subscriptable after the run had finished. It now reads run.default_dataset_id; with client version 1 or 2, keep run['defaultDatasetId'].
No output field or pricing change. Behaviour changes for inputs that already exist: a run with a status or region filter reaches further back for the same maxScan, so it can deliver — and charge for — more tenders than before; the edges of a custom range move by two or three hours (Kyiv days instead of UTC days), and a preset counts its days back from the Kyiv date — a run started between 00:00 and 03:00 Kyiv time begins its range 21 to 22 hours later than before; masked tenders are no longer delivered; a keyword with an apostrophe also finds the tenders that write it with another apostrophe sign (more rows than before); a keyword in Latin letters with maxScan left empty, and a region in Latin letters with or without one, can stop early; maxScan is no longer rounded up to the next hundred (250 used to read 300); custom with neither date, a dateFrom after the dateTo and a minValue above the maxValue stop the run; a run fails when ProZorro stops answering tender requests — it used to end as a shorter or an empty result.
0.2 — 2026-09-07
The README was regenerated from a template shared by many Actors; the entry of that day read "Enabled AI-agent payments (x402) + rebuilt the README to the full standard". The label is the generator's: the builds of that day were numbered like the others of this Actor. That README is the one the entry above corrects — its examples and parts of its text did not describe this Actor.
0.2 — 2026-08-01
Buyer contacts: buyerContactName, buyerContactEmail, buyerContactPhone — already present in the fetched API response, so no extra requests or runtime.
Keyword search now also matches item descriptions and CPV codes, not just the tender title.
Counts that are not in the record stay null instead of reporting a misleading 0.
New "Buyer contacts" dataset view.
0.0
Initial release, 2026-07-09 (the Actor's first changelog called it 0.1): Prozorro tenders with buyers, award entries (supplier, registry number, award value), expected values, CPV-coded items and status; filters by keyword, status, category, region, value and date; newest first with a maxScan cap.