Multilateral Development Bank Procurement Monitor
Pricing
from $1.00 / 1,000 world bank procurement notices
Multilateral Development Bank Procurement Monitor
Extracts the World Bank's public Procurement Notices API and its 'Other Sanctions' debarment sub-table, normalized to the fleet's 18-field UMS. ADB (Cloudflare-gated) and IDB (Power BI-only, CKAN API robots.txt-blocked) were live-researched and honestly deferred, not force-scraped - see README.md.
Pricing
from $1.00 / 1,000 world bank procurement notices
Rating
0.0
(0)
Developer
Stefano Seggio
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
15 hours ago
Last modified
Categories
Share
Multilateral Development Bank Procurement Monitor — Apify Store Overview
Actor ID: dyzTtWjfyYd7bvUZY · Store URL: https://apify.com/stefano_seggio/actor-20-mdb-procurement-monitor · Version: 2.0
Executive Summary & Business Use Case
Multilateral Development Bank Procurement Monitor extracts two live sub-sources from the World Bank: the public Procurement Notices API at search.worldbank.org/api/v2/procnotices (unauthenticated JSON, hundreds of thousands of notices spanning World Bank-financed projects worldwide), and the "Other Sanctions" sub-table on the World Bank's static debarred-firms page at www.worldbank.org/en/projects-operations/procurement/debarred-firms — a real, header-anchored HTML <table> listing firms sanctioned by the Bank outside its main Kendo-grid debarment list. Both sub-sources are normalized into one shared 18-field Unified Master Schema (UMS), with genuine cross-run change detection (new / status-changed / updated) on the Procurement Notices side. Two other multilateral lenders — the Asian Development Bank and the Inter-American Development Bank — were live-researched during development and deliberately excluded rather than force-scraped: ADB sits behind a Cloudflare/WAF challenge on every tested path, and IDB's public notice listing renders exclusively through an embedded Power BI report whose one structured alternative (a CKAN open-data API) is explicitly disallowed in data.iadb.org/robots.txt. This actor covers what is genuinely open at the World Bank; it does not pretend to cover ADB or IDB.
The data supports business use cases grounded in exactly what these two sub-sources publish — a notice/sanction record with status, category, dates, and issuing agency, but no bidder-name or monetary-value field on the notices side (the World Bank's Procurement Notices endpoint carries neither, confirmed against the live API response and documented as an honest "not applicable," not a parsing gap). International contractors, engineering firms, and development consultancies bidding on World Bank-financed projects use the Procurement Notices feed — filtered by countryFilter and noticeTypeFilter — to track new Invitation for Bids and Requests for Expression of Interest as they publish, and to catch STATUS_CHANGE events as a tracked notice moves toward evaluation, award, or close, so they know exactly when to re-engage rather than re-scanning the entire feed. Compliance, KYC, and third-party risk teams at banks, insurers, and multinational corporates screen prospective suppliers and partners against the "Other Sanctions" sub-source before onboarding, using recipient_or_defendant_name (the sanctioned firm), category_or_type (the sanction imposed), and status_or_estado (the raw imposition-date/status text) to flag counterparties the World Bank has sanctioned for fraud or corruption on prior projects. Development-sector market-intelligence and research teams use the aggregate notice volume, notice_type mix, and awarding_or_regulating_agency field across countries to size procurement activity and agency-level tender frequency for World Bank-financed work, without relying on a monetary value the source itself never publishes.
What this actor is deliberately not positioned for: contract-value or spend analysis (no source here exposes a monetary field — value_native, value_currency, and value_usd_normalized are always null), bidder/award-winner tracking on procurement notices specifically (no winner field exists on this WB endpoint), and any ADB or IDB coverage in this version.
Technical Features & V2 Architecture Highlights
Cross-run delta persistence via a named key-value store, Procurement Notices only. Per the actor's own onlyNew input description, enabling delta mode persists a content fingerprint per Procurement Notice between runs "in this actor's own named key-value store" — not the ephemeral per-run default store — so a scheduled recurring run can tell a genuinely new notice from one it already delivered last time, and can detect when notice_status or another tracked field changed since it was last seen. The "Other Sanctions" sub-source has no delta concept at all: it is a small static snapshot page, not a dated or paginated feed, so every run re-delivers every row it finds.
Real, domain-specific event types — not the fleet's generic NEW_LISTING/UPDATED/CLOSED set. Per dataset_schema.json, event_type takes exactly five values: NEW_LISTING (a Procurement Notice's first sighting), STATUS_CHANGE (notice_status differs from last time), UPDATED (some other tracked field changed but status didn't), SNAPSHOT_NO_DIFF (identical to last time — only delivered when onlyNew is false; also reused for the degraded-extraction fallback record described below), and SANCTION (every Other Sanctions entry, since that sub-source has no delta concept to differentiate against). There is deliberately no CLOSED event: the Procurement Notices API is genuinely server-side paginated and each run stops once maxItemsPerSource is reached, so a run's fetch is never guaranteed to be a complete census of the live register the way a single-file export would be — a trustworthy absence-means-closed signal simply isn't available here.
Extraction-integrity fallback, not a false "nothing found." If the Other Sanctions table's header row can't be located, or is located but zero data rows parse out of it, the actor treats that as an extraction-integrity failure rather than silently reporting "no sanctions exist." It pushes one SNAPSHOT_NO_DIFF record pointing at the World Bank's own static "Notes on Debarred Firms and Individuals" PDF as a documented fallback reference.
What onlyNew actually does here, per the actor's own input schema. Quoting its real description: when enabled, the actor "persists a content fingerprint per Procurement Notice between runs (in this actor's own named key-value store) and returns only notices that are new since the last run OR whose notice_status or other tracked fields changed since they were last seen — notices identical to last time are skipped." is_new is always populated (true/false) regardless of this setting, and the flag applies to Procurement Notices only — it has no effect on Other Sanctions rows. Because the World Bank API documents no server-side "since" filter, pagination always walks the result set from the start on every run; onlyNew filters client-side after the fact rather than early-stopping pagination, so it changes what gets delivered, not how much gets fetched.
One actor, two native record shapes, self-describing IDs. Because this single actor emits both Procurement Notice and Other Sanctions records into one dataset, record_id is prefixed per sub-source (wb-procnotice-<id>, wb-sanction-<slug>) so ids are self-describing and cannot collide within a run. jurisdiction is 'WB' for both sub-sources — a plain non-empty string rather than one of the fleet's closed national/subnational jurisdiction codes, since a supranational lender doesn't fit that union.
18-field Unified Master Schema. dataset_schema.json defines exactly 18 fields per record (record_id, event_type, scraped_at, is_new, source_url, recipient_or_defendant_name, entity_identifier_native, value_native, value_currency, value_usd_normalized, effective_date_iso, publish_date_iso, category_or_type, status_or_estado, awarding_or_regulating_agency, jurisdiction, source_document_url, reference_number), matching the fleet's shared 18-field UMS referenced in actor.json.
Client-side filtering, disclosed rather than misrepresented as server-side. countryFilter, noticeTypeFilter, and dateRange all filter Procurement Notices after each page is fetched, because the World Bank API documents no server-side parameter for country, notice type, or date. None of the three apply to Other Sanctions records — that source has no reliable per-record country, notice-type, or publish-date field to filter on.
Input Schema & JSON Configuration Example
| Field | Type | Default | Description |
|---|---|---|---|
sources | array (enum items) | ["worldBankProcurementNotices", "worldBankDebarredFirms"] | worldBankProcurementNotices = the live World Bank Procurement Notices API (search.worldbank.org). worldBankDebarredFirms = the World Bank's static 'Other Sanctions' debarment sub-table (worldbank.org). ADB and IDB are NOT selectable here - both were live-researched and found gated/non-compliant for v1 (Cloudflare/WAF for ADB; Power BI embed + robots.txt-blocked API for IDB) - see README.md. |
maxItemsPerSource | integer | 100 | Hard cap on how many records to return per selected source this run. For worldBankProcurementNotices this bounds rows/os pagination against a live corpus of several hundred thousand notices; for worldBankDebarredFirms it is a no-op ceiling since the 'Other Sanctions' sub-table is a single small page. |
onlyNew | boolean | false | When enabled, persists a content fingerprint per Procurement Notice between runs (in this actor's own named key-value store) and returns only notices that are new since the last run OR whose notice_status or other tracked fields changed since they were last seen - notices identical to last time are skipped. is_new is always populated (true/false) regardless of this setting. Only applies to worldBankProcurementNotices - Other Sanctions has no delta-tracking concept (a small static snapshot page, not a dated/paginated feed). The World Bank Procurement Notices API documents no server-side 'since' filter, so pagination always walks from the start of the result set each run - onlyNew filters client-side after the fact rather than early-stopping pagination. |
countryFilter | array (stringList) | (none) | Optional client-side filter on project_ctry_name (e.g. 'Kenya', 'India'). The World Bank API documents no server-side country filter parameter, so this is applied after fetching each page. |
noticeTypeFilter | array (stringList) | (none) | Filter by notice type (procurement notices only). Optional client-side filter on notice_type (e.g. 'Invitation for Bids', 'Request for Expression of Interest', 'Contract Award'). Applied after fetching each page, same caveat as countryFilter. |
dateRange | string (enum) | (none) | Optionally restrict procurement-notice results to records whose noticedate falls within this window. Not applied to worldBankDebarredFirms records (that source has no reliable per-record publish date field in the static sub-table). Values: 24h Last 24 hours, 7d Last 7 days, 30d Last 30 days. |
No field in input_schema.json is marked required — an empty {} input is valid and falls back to the documented defaults (both sources, maxItemsPerSource: 100, onlyNew: false, no filters).
Example configuration — scheduled delta monitor, one country and notice type
{"sources": ["worldBankProcurementNotices", "worldBankDebarredFirms"],"maxItemsPerSource": 250,"onlyNew": true,"countryFilter": ["Kenya", "India"],"noticeTypeFilter": ["Invitation for Bids"],"dateRange": "7d"}
This raises the per-source cap to 250, enables delta mode so only new or changed Procurement Notices are delivered on repeat runs, narrows notices to Kenya and India and to Invitation for Bids only, and further restricts to notices published in the last 7 days. onlyNew and dateRange apply to worldBankProcurementNotices only; worldBankDebarredFirms rows are still fetched and delivered in full every run regardless of these settings.
Example configuration — one-off unfiltered pull of both sources
{"sources": ["worldBankProcurementNotices", "worldBankDebarredFirms"],"maxItemsPerSource": 5000,"onlyNew": false,"countryFilter": [],"noticeTypeFilter": [],"dateRange": ""}
Output Dataset Sample & Data Dictionary
| Field | Type | Description |
|---|---|---|
record_id | string | Stable across runs. Prefixed per sub-source ('wb-procnotice- |
event_type | string | Procurement notices: NEW_LISTING on first sighting, STATUS_CHANGE if notice_status changed since last seen, UPDATED if some other tracked field changed, SNAPSHOT_NO_DIFF if identical (only delivered when onlyNew=false). SANCTION for every Other Sanctions entry (no delta concept on that static-snapshot sub-source). SNAPSHOT_NO_DIFF also used for the degraded-extraction fallback notice. |
scraped_at | string | ISO-8601 timestamp of this extraction. |
is_new | boolean | null | true/false in delta mode (onlyNew input), null when delta mode is off. |
source_url | string | null | — |
recipient_or_defendant_name | string | null | null for procurement notices (no recipient/winner field is present on this WB endpoint - see README); the sanctioned firm name for Other Sanctions records. |
entity_identifier_native | string | null | — |
value_native | string | null | Always null - neither source exposes a monetary value field (documented honestly, not a parsing gap). |
value_currency | string | null | — |
value_usd_normalized | number | null | — |
effective_date_iso | string | null | submission_deadline_date for procurement notices; parsed sanction-imposition date for Other Sanctions (null when the source text isn't a cleanly parseable date, e.g. 'Ongoing'). |
publish_date_iso | string | null | Parsed from noticedate (procurement notices only). |
category_or_type | string | null | notice_type for procurement notices; the 'Sanction Imposed' text for Other Sanctions. |
status_or_estado | string | null | notice_status for procurement notices; the raw 'Date of Imposition of Sanction' cell text for Other Sanctions (mixes dates and statuses like 'Ongoing' in the source - kept verbatim rather than force-split). |
awarding_or_regulating_agency | string | null | — |
jurisdiction | string | 'WB' for both World Bank sub-sources. Not one of the fleet's 11 closed JurisdictionCode values - this is a supranational institution, not a national/subnational government. |
source_document_url | string | null | — |
reference_number | string | null | — |
Example dataset record — Procurement Notice
{"record_id": "wb-procnotice-OP00187452","event_type": "STATUS_CHANGE","scraped_at": "2026-09-08T09:12:31.442Z","is_new": false,"source_url": "https://search.worldbank.org/api/v2/procnotices?format=json&rows=250&os=0","recipient_or_defendant_name": null,"entity_identifier_native": "OP00187452","value_native": null,"value_currency": null,"value_usd_normalized": null,"effective_date_iso": "2026-10-15","publish_date_iso": "2026-09-01","category_or_type": "Invitation for Bids","status_or_estado": "Under Evaluation","awarding_or_regulating_agency": "Ministry of Health, Kenya","jurisdiction": "WB","source_document_url": "https://search.worldbank.org/notice/detail/OP00187452","reference_number": "KE-MOH-2026-RFB-014"}
Example dataset record — Other Sanctions
{"record_id": "wb-sanction-northbridge-infra-consult-ltd","event_type": "SANCTION","scraped_at": "2026-09-08T09:12:34.902Z","is_new": null,"source_url": "https://www.worldbank.org/en/projects-operations/procurement/debarred-firms","recipient_or_defendant_name": "Northbridge Infra Consult Ltd.","entity_identifier_native": null,"value_native": null,"value_currency": null,"value_usd_normalized": null,"effective_date_iso": "2025-02-11","publish_date_iso": null,"category_or_type": "Debarment with Conditional Release","status_or_estado": "11-February-2025","awarding_or_regulating_agency": null,"jurisdiction": "WB","source_document_url": "https://thedocs.worldbank.org/en/doc/387181466627871302-0290022021/original/WorldBankNotesonDebarredFirmsandIndividuals.pdf","reference_number": null}
Multi-language Integration Snippets
cURL
curl "https://api.apify.com/v2/acts/stefano_seggio~actor-20-mdb-procurement-monitor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-H "Content-Type: application/json" \-d '{"sources": ["worldBankProcurementNotices", "worldBankDebarredFirms"],"maxItemsPerSource": 250,"onlyNew": true,"countryFilter": ["Kenya", "India"],"noticeTypeFilter": ["Invitation for Bids"],"dateRange": "7d"}'
Python (apify-client)
import osfrom apify_client import ApifyClientclient = ApifyClient(os.environ["APIFY_TOKEN"])run_input = {"sources": ["worldBankProcurementNotices", "worldBankDebarredFirms"],"maxItemsPerSource": 250,"onlyNew": True,"countryFilter": ["Kenya", "India"],"noticeTypeFilter": ["Invitation for Bids"],"dateRange": "7d",}run = client.actor("stefano_seggio/actor-20-mdb-procurement-monitor").call(run_input=run_input)for item in client.dataset(run["defaultDatasetId"]).iterate_items():print(item["record_id"], item["event_type"], item["category_or_type"], item["jurisdiction"])
Node.js (apify-client)
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const runInput = {sources: ['worldBankProcurementNotices', 'worldBankDebarredFirms'],maxItemsPerSource: 250,onlyNew: true,countryFilter: ['Kenya', 'India'],noticeTypeFilter: ['Invitation for Bids'],dateRange: '7d',};const run = await client.actor('stefano_seggio/actor-20-mdb-procurement-monitor').call(runInput);const { items } = await client.dataset(run.defaultDatasetId).listItems();for (const item of items) {console.log(item.record_id, item.event_type, item.category_or_type, item.jurisdiction);}
Pricing Model Explanation
This actor bills on two named pay-per-event types, live-verified on the Apify platform, plus the fleet-standard run-start event:
| Event | Price | Meaning for this actor |
|---|---|---|
procurementNotices | $0.001 per record | Charged for each worldBankProcurementNotices record delivered into the dataset. Priced lower because a single cheap JSON GET against search.worldbank.org returns many records per request (~99.75% margin against this fleet's compute-cost baseline). |
debarredFirms | $0.003 per record | Charged for each worldBankDebarredFirms (Other Sanctions) record delivered into the dataset. Priced higher because one HTML fetch is amortized across a small, slowly-changing row count rather than a large multi-record JSON page (~91.7-95.8% margin, covering both the normal-extraction path and the degraded-fallback path). |
apify-actor-start | $0.00005 | Charged once per run, regardless of how many records that run delivers or how many sources are selected. |
The two record-priced tiers here map directly to the two selectable sources values, not to a "listing vs. full detail" split: every worldBankProcurementNotices record bills as procurementNotices, and every worldBankDebarredFirms (Other Sanctions) record — including the one SNAPSHOT_NO_DIFF degraded-extraction fallback record, when it fires — bills as debarredFirms. Both tiers clear this fleet's 85% margin bar (cost per record at or below 15% of price) and sit within the existing $0.0005-$0.003/record rate card.
How onlyNew interacts with billing. onlyNew only affects worldBankProcurementNotices. With it enabled, a Procurement Notice identical to what was already delivered on a prior run (SNAPSHOT_NO_DIFF) is filtered out client-side before it is ever written to the dataset — it is never delivered and therefore never billed as a procurementNotices event. It is not billed at $0; it simply never becomes a chargeable event at all. worldBankDebarredFirms records have no delta concept and are unaffected by onlyNew — every Other Sanctions row found on a run is delivered and billed as a debarredFirms event on every run, whether onlyNew is on or off.
Cost examples:
- A one-off run pulling 250 Procurement Notices and the full small Other Sanctions table (assume ~40 rows) costs approximately 250 × $0.001 + 40 × $0.003 + $0.00005 ≈ $0.37.
- A daily scheduled
onlyNew: truemonitor on Procurement Notices alone that typically surfaces around 15 new-or-changed notices a day, plus the Other Sanctions table re-delivered in full (~40 rows) every run, costs approximately 15 × $0.001 + 40 × $0.003 + $0.00005 ≈ $0.14/day (roughly $4.20/month) — only the genuinely new or changed Procurement Notices are billed on the delta side, while every Other Sanctions row is billed every run regardless.