# UN (UNGM) & World Bank Procurement Monitor - Tender Alerts (`datadeltas/ungm-worldbank-procurement`) Actor

Monitor UN Global Marketplace (UNGM) and World Bank procurement notices in one feed. Keyword, country, sector and notice-type filters over both portals' public endpoints. Emits only NEW notices on each run, with World Bank contract-award values parsed out - schedule it and feed your bid pipeline.

- **URL**: https://apify.com/datadeltas/ungm-worldbank-procurement.md
- **Developed by:** [Daniel Matthee](https://apify.com/datadeltas) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $15.00 / 1,000 procurement notice detecteds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## UN (UNGM) & World Bank Procurement Monitor — Tender Alerts

The **new-notice feed** for anyone who bids on UN and development-bank work.
Every scheduled run searches the **UN Global Marketplace (UNGM)** — the shared
tender board for ~90 UN agencies and funds — and **World Bank** project
procurement, keeps only the notices matching your keywords, and emits **just
the ones it has never seen before**.

Other actors dump a portal's catalogue and leave you to work out what changed.
This one remembers. Run it on a schedule and each run's dataset *is* your
circulation list: the tenders and awards published since you last looked, with
agency, country, deadline and — for World Bank awards — the **published
contract value**.

Both sources are the portals' own public endpoints. **No API key, no login, no
UNGM Pro subscription** required.

### What you get

| | UNGM (UN Global Marketplace) | World Bank |
|---|---|---|
| Coverage | Live tender notices from ~90 UN agencies: UNDP, UNICEF, WHO, WFP, FAO, IOM, UNOPS, UNHCR, UNIDO, ILO, IAEA, UNRWA and the rest | Procurement notices on Bank-financed projects: expressions of interest, invitations for bids, prequalifications, general procurement notices **and contract awards** |
| Stable id | UNGM notice id (`312822`) | World Bank operation id (`OP00465428`) |
| Notice types | Invitation to bid, request for quotation, request for proposal, call for individual consultants, request for EOI, request for information | Request for Expression of Interest, Invitation for Bids, Invitation for Prequalification, General Procurement Notice, Contract Award |
| Country | Beneficiary country or territory | Project country, including regional groupings ("Western Africa") |
| Sector | Not published in the listing — always `null` | Goods / Consulting services / Civil works / Non-consulting services |
| Contract value | Not published — always `null` | Parsed out of the award notice: **every one of 1,411 awards** in a live 14-day window carried a published amount |
| Typical volume | ~75 new notices per day | ~135 new notices per day |

### Example output record

A UNGM tender:

```json
{
  "source": "ungm",
  "noticeId": "312839",
  "title": "RFQ/EM/JOR/08/04 Rehabilitation Works at Al-Yarmouk Hospital in Irbid, Jordan (Kitchenware)",
  "agency": "WHO",
  "noticeType": "tender",
  "noticeCategory": "Request for quotation",
  "country": "Jordan",
  "sector": null,
  "reference": "EM/ACO/JOR/P/0009505",
  "projectId": null,
  "projectName": null,
  "description": null,
  "publishedDate": "2026-08-31",
  "deadlineDate": "2026-09-09T09:00:00Z",
  "valueUSD": null,
  "valueAmount": null,
  "valueCurrency": null,
  "url": "https://www.ungm.org/Public/Notice/312839",
  "detectedAt": "2026-08-31T10:56:36Z"
}
```

A World Bank contract award, same shape:

```json
{
  "source": "world-bank",
  "noticeId": "OP00465460",
  "title": "Accounting software 1C: Accounting (version 8.2)",
  "agency": "World Bank",
  "noticeType": "award",
  "noticeCategory": "Contract Award",
  "country": "Kyrgyz Republic",
  "sector": "Goods",
  "reference": "AQIP-RFQ-4.1",
  "projectId": "P177467",
  "projectName": "Kyrgyz Republic Air Quality Improvement Project",
  "description": "Contract Award Project: P177467-Kyrgyz Republic Air Quality Improvement Project…",
  "publishedDate": "2026-08-28",
  "deadlineDate": null,
  "valueUSD": null,
  "valueAmount": 2728000.0,
  "valueCurrency": "KGS",
  "url": "https://projects.worldbank.org/en/projects-operations/procurement-detail/OP00465460",
  "detectedAt": "2026-08-31T10:56:36Z"
}
```

`noticeId` is each portal's own public reference, so it pastes straight into
your bid pipeline or the portal's search box. Every field is `null` rather than
invented when a portal does not publish it — an award has no deadline, and a
UNGM listing carries no sector.

#### About contract values

World Bank awards publish the contract price in the **local currency of the
contract**. `valueAmount` + `valueCurrency` give you exactly what the notice
says; `valueUSD` is filled in **only** when the notice itself is denominated in
USD (225 of 1,411 awards in a live 14-day window). No exchange rate is applied
— an invented USD figure on a contract-value field is worse than a null.

### Input options

| Input | Default | What it does |
|---|---|---|
| `sources` | both | `ungm`, `world-bank`, or both. |
| `keywords` | *(empty)* | Case-insensitive substring match; a notice is emitted if **any** keyword matches. Empty = every new notice. See below. |
| `countries` | *(empty)* | ISO-2 codes (`KE`, `NG`, `UA`) or country names as the portals print them. Matched as a substring, so `Congo` also catches "Congo, Democratic Republic of". |
| `sectors` | *(empty)* | Goods / Consulting services / Civil works / Non-consulting services. **World Bank only** — UNGM publishes no sector in its listing, so UNGM notices are never dropped by it. |
| `noticeTypes` | tender + award | `tender` = live opportunities; `award` = published contract awards, for competitor and pricing intelligence. Awards come from the World Bank only. |
| `sinceDays` | 14 | How far back to look. Your backfill on run one. |
| `maxItems` | 500 | Cap on notices emitted (and billed) per run, **split evenly between the portals** so neither starves. |
| `stateStoreName` | `ungm-worldbank-procurement-state` | Change it to run two independent watchlists from one actor. |

#### Keyword matching, precisely

- **World Bank** notices are matched against the title, the bid reference and
  the **full notice text**.
- **UNGM** notices are matched against the title and the bid reference. UNGM
  does not publish a description in its search listing, and fetching ~1,300
  detail pages per run would cost you more than it is worth. This is also how
  UNGM's own search behaves by default.
- Give **exactly one keyword** and it is additionally pushed to UNGM's own
  server-side title search — the run gets dramatically cheaper and returns the
  portal's own result set (verified: UNGM reported 26 hits for `vehicle` over
  14 days; this actor fetched and emitted all 26).
- Matching is a plain substring, not the portals' stemmed, multilingual
  relevance ranking. Over the same 14-day window, `vehicle` matched 20 World
  Bank notices here versus 27 from the Bank's own search box: the Bank's extra
  hits are Spanish-language *"VEHICULOS"* rows, while 10 of ours are genuine
  English "vehicle" hits deep in a notice body that its search box missed.
  Predictable beats clever — if you want the wide net, give several narrow
  keywords rather than one broad one.

### The first run backfills, then it goes quiet

Run one emits everything in your `sinceDays` window that matches — a real
backfill you can circulate immediately. From run two on, an empty dataset means
nothing new was published, which is the answer you wanted. (Verified live: a
1-day window emitted 129 notices, and the immediate re-run emitted **0**.)

Both portals are busy — around 210 notices a day between them — so a 14-day
no-keyword backfill is thousands of records. `maxItems` is your meter: anything
over the cap is **not** marked as seen and is emitted on the following run
rather than lost, and the cap is split evenly between the portals so you always
get both.

Recommended schedule: **daily**. UNGM publishes continuously; the World Bank
posts on business days.

### Use cases

- **"Tell me every new UN and World Bank tender in our sectors."** Set
  `keywords` to your practice areas — `["water supply", "borehole",
  "monitoring and evaluation"]` — schedule it daily, and pipe the dataset into
  Slack or a shared inbox. That is the Monday-morning portal scan, done.
- **"We only work in East Africa."** Set `countries: ["KE", "TZ", "UG", "RW",
  "ET"]` with `noticeTypes: ["tender"]`, and the feed is the pipeline your
  country leads actually cover — no more filtering 200 notices a day by hand.
- **"Who is winning the civil-works contracts, and at what price?"** Set
  `sources: ["world-bank"]`, `noticeTypes: ["award"]` and
  `sectors: ["Civil works"]`. Every award arrives with the project, the country
  and the published contract price — competitor and pricing intelligence you
  currently pay an aggregator for.

### Pricing (pay per event)

| Event | Price | Charged |
|---|---|---|
| Actor start | $0.005 | Once per run, flat |
| Procurement notice detected | $0.015 | Per new notice pushed to the dataset |

You pay for new notices, not for pages. A quiet run costs half a cent.

### Maintenance promise

Both endpoints are checked against a live acceptance run and a fixture-based
test suite (52 tests) on each release; parsing degrades to a logged warning
naming the missing field rather than a failed run when a portal changes its
markup. If one portal is down for a run, the other still reports and the run
succeeds with a warning — only a total outage fails the run, deliberately, so a
dead run never looks like a quiet day.

Notes on coverage, stated plainly:

- UNGM publishes **contract awards on a separate board** that this actor does
  not cover, so `noticeTypes: ["award"]` returns World Bank rows only.
- UNGM re-serves a small number of rows across pages while you page a busy
  window (about 8% of a 14-day fetch); those are deduplicated per run, and you
  are never billed twice for the same notice.
- Contact names, emails and phone numbers appear in the World Bank's raw feed
  and are **deliberately not collected** — only the implementing organisation.

### Changelog

- **0.1** — First release. UNGM public notice search + World Bank
  `procnotices` API, seen-id delta monitoring per portal, keyword / country /
  sector / notice-type filters, World Bank award-value extraction, fair
  `maxItems` split across portals.
  - Internal, pre-release: the fair `maxItems` split now comes from the shared
    monitor library (v0.2.0) instead of a local copy. Fixes an edge case at
    `maxItems=1`, where one portal's zero share was treated as "no cap" and
    could emit — and bill — more notices than the cap allowed. Output, filters,
    state and pricing are unchanged; anything over the cap still carries over.

# Actor input Schema

## `sources` (type: `array`):

Which procurement portals to watch. Default: both. If one portal is down for a run, the other still reports and the run succeeds with a warning.

## `keywords` (type: `array`):

Case-insensitive substring match, emitted when ANY keyword matches. World Bank notices are matched on title + reference + the full notice text. UNGM listings publish no description, so UNGM notices are matched on title + reference (this is also how UNGM's own search behaves by default). Give exactly ONE keyword and it is additionally pushed to UNGM's server-side title search, which makes the run much cheaper. Leave empty to monitor every new notice.

## `countries` (type: `array`):

Optional. ISO-2 codes (KE, NG, UA) or full country names as the portals print them. Matched as a substring against the notice's own country string, so "Congo" also catches "Congo, Democratic Republic of". A notice with no country published is excluded when this filter is set. Note the World Bank also uses regional groupings such as "Western Africa" or "Southwest Indian Ocean" - add those by name if you want them.

## `sectors` (type: `array`):

Optional World Bank procurement categories. Has NO effect on UNGM notices - UNGM classifies by UNSPSC code on the notice detail page only, not in its listing - so UNGM results are never dropped by this filter.

## `noticeTypes` (type: `array`):

"Tender" = live opportunities (invitations to bid, RFQs, RFPs, expressions of interest, consultant calls). "Award" = published contract awards, useful for competitor and pricing intelligence. Awards come from the World Bank only: UNGM publishes contract awards on a separate board that this actor does not cover. Default: both.

## `sinceDays` (type: `integer`):

How far back to look for notices. On the first run this is your backfill; afterwards it just bounds the search, because only never-seen notices are emitted. Keep it modest on the first run and raise Max items instead - both portals publish hundreds of notices a day.

## `maxItems` (type: `integer`):

Safety cap on notices emitted (and billed) in one run. Anything over the cap is NOT marked as seen, so it is emitted on the next run instead of being lost.

## `stateStoreName` (type: `string`):

Named key-value store holding the seen-notice ids, kept separately per portal. Change it to run two independent watchlists (e.g. one per bid team) from the same actor.

## Actor input object example

```json
{
  "sources": [
    "ungm",
    "world-bank"
  ],
  "keywords": [],
  "countries": [],
  "sectors": [],
  "noticeTypes": [
    "tender",
    "award"
  ],
  "sinceDays": 14,
  "maxItems": 500,
  "stateStoreName": "ungm-worldbank-procurement-state"
}
```

# Actor output Schema

## `records` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "keywords": [],
    "countries": [],
    "sectors": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("datadeltas/ungm-worldbank-procurement").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "keywords": [],
    "countries": [],
    "sectors": [],
}

# Run the Actor and wait for it to finish
run = client.actor("datadeltas/ungm-worldbank-procurement").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "keywords": [],
  "countries": [],
  "sectors": []
}' |
apify call datadeltas/ungm-worldbank-procurement --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,datadeltas/ungm-worldbank-procurement"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/VUJ3aX0MgbwhEPkGc/builds/MNlDgWbBo6Fj1mqLx/openapi.json
