# South Africa Government Supplier Tracker (`mastazee_data_lab/south-africa-government-supplier-tracker`) Actor

Which suppliers win South African government tenders: eTenders awards grouped by supplier, with buyers, signed contract values and official links.

- **URL**: https://apify.com/mastazee_data_lab/south-africa-government-supplier-tracker.md
- **Developed by:** [Mastazee Data Lab](https://apify.com/mastazee_data_lab) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 results

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## South Africa Government Supplier Tracker

**See which companies win South African government tenders, how often, from which buyers, and for how much.** This Actor reads National Treasury's official eTenders open-data API and groups the awards by winning supplier. You get one row per supplier with their award count, the total value of their signed contracts, their main buyers and categories, and a link to every official award record.

Use it to:

- **Watch competitors.** Follow named rivals (by name or eTenders supplier id) and get a row whenever they win something new.
- **Find partners and subcontractors.** See who keeps winning security, cleaning, IT or construction work in your province.
- **Size a market.** Count awards and known contract values per supplier for a buyer or category.

> **Tested on 26 October 2026** against the live eTenders API:
>
> - **Default input:** 500 tenders scanned in about a minute, 38 suppliers from 39 awards (217 tenders skipped by the eTenders server error described below).
> - **Monitor mode:** a second run of the same monitor straight after the first returned 0 suppliers, repeating nothing.
> - **On the Apify platform:** the same default run took about 1.5 minutes, and a repeat run of a monitor again returned 0 suppliers.
>
> This is a data tool, not a lead guarantee. Coverage is what buyers publish on eTenders (see *Data limitations*).

### Example input

```json
{
  "supplierNames": ["security"],
  "provinces": ["KwaZulu-Natal"],
  "toOcid": 0,
  "recordsToScan": 15000,
  "onlyNewAwards": true,
  "stateKey": "kzn-security"
}
```

Leave every filter empty to see all suppliers in the scanned range. Schedule this weekly with `onlyNewAwards` on, and each run returns only suppliers with awards you haven't received yet. Monitors with different filters keep separate memories, even under the same `stateKey`.

### Example output (one supplier, trimmed)

```json
{
  "supplier_key": "id:1442842",
  "supplier_name": "QEDANATION",
  "supplier_id": "1442842",
  "name_variants": ["QEDANATION"],
  "other_ids_with_same_name": [],
  "award_count": 2,
  "awards_with_known_value": 0,
  "awards_with_unknown_value": 2,
  "total_contract_value_zar": null,
  "first_tender_published_date": "2026-03-06",
  "buyers": [{"name": "Ingquza Hill Local Municipality", "awards": 2}],
  "categories": [{"name": "goods", "awards": 2}],
  "awards": [
    {"ocid": "ocds-9t57fa-150000", "award_id": "66010", "buyer_name": "Ingquza Hill Local Municipality",
     "tender_title": "PROCUREMENT OF STATIONERY", "contract_value_zar": null,
     "source_url": "https://ocds-api.etenders.gov.za/api/OCDSReleases/release/ocds-9t57fa-150000"}
  ]
}
```

### Fields

| Field | Meaning |
|---|---|
| `supplier_key` | `id:<eTenders supplier id>`, or `name:<normalised name>` when eTenders gives no id |
| `supplier_name`, `name_variants` | The most common spelling and every spelling seen |
| `other_ids_with_same_name` | Other eTenders supplier ids using the same name. These are **not merged** (see below) |
| `award_count` | Awards in the scanned range. Separate awards in one tender count separately |
| `awards_with_known_value`, `awards_with_unknown_value` | How many awards have a signed contract value |
| `total_contract_value_zar`, `largest_contract_value_zar` | Sum and maximum of **known** contract values only. `null` when none is known |
| `first_/last_contract_signed_date` | Earliest and latest contract signing date (eTenders has no award date) |
| `first_/last_tender_published_date` | When the related tenders opened |
| `buyers`, `categories`, `buyer_provinces` | Counts of awards per buyer, category and province |
| `enterprise_sizes` | EME / QSE / GEN as recorded by eTenders |
| `awards` | Each award with its official `source_url` (turn off with `includeAwardList`) |

### How suppliers are matched (conservatively)

- Awards are grouped by eTenders' own supplier id, which stays the same across a company's awards.
- Different ids are **never merged**, even if the names match. You'll see them listed in `other_ids_with_same_name` and can decide for yourself.
- Awards without a supplier id are grouped by name, separately from any id-based group.
- Only case, spaces and punctuation are ignored when comparing names. "ACME" and "ACME (PTY) LTD" stay separate suppliers.

### Data source and attribution

[eTenders OCDS API](https://ocds-api.etenders.gov.za/), published by National Treasury (South Africa) under the [Open Data Commons PDDL](https://opendatacommons.org/licenses/pddl/1-0/). Every award links to its official record, as the [eTenders publication policy](https://data.etenders.gov.za/Home/LearnMore) asks. No API key is needed.

### Data limitations (please read)

- **About 25–45% of eTenders records can't be read.** The official API returns a server error for them (217 of 500 in the default range, 50 of the newest 200 on 26 October 2026). They're skipped and counted in the run's `RUN_SUMMARY`; re-running the range picks them up if Treasury fixes them.
- **Values come only from signed contracts.** eTenders' own award amounts are published with the decimal point dropped (10× or 100× too high), so they're never used. About a third of awards have a contract value. The rest are counted as unknown.
- **There's no award date.** Contract signing date and tender publication date are given instead.
- **Province comes from the buyer's name** (provincial departments and the big metros). National departments and most municipalities have none.
- **Coverage:** eTenders covers national and provincial government plus the municipalities and state entities that choose to publish there. It isn't every public contract in South Africa.
- **Personal contact details are never output.** eTenders attaches officials' names, phones and emails to awards.

### Running time

The Actor checks tender numbers one by one (about 110 new tenders a day), roughly 8–10 a second at the default 4 parallel requests. Measured on 26 October 2026:

| Scan | Time | Suppliers |
|---|---|---|
| 500 tenders (default, May 2026) | ~1 min | 38 |
| Newest ~800 tenders (about a week) | ~1.5 min | ~125 |
| 15,000 tenders (~4–5 months) | ~25–30 min (estimated) | roughly 800–2,000 (estimated) |

### Pricing

Pay per event: **$5.00 per 1,000 supplier rows** ($0.005 per supplier), plus Apify's standard tiny start fee. Each row is one supplier with all its awards in the scanned range, and you are charged only for rows written to your dataset.

| How you use it | Supplier rows | You pay (approx.) |
|---|---|---|
| Default run (500 tenders) | ~38 | ~$0.19 |
| Weekly run over the newest ~800 tenders, no filter, for a month | ~550 | ~$2.75 a month |
| Weekly watch list of a few named competitors | 0–a few a week | cents a month |

- **No double billing in monitor mode:** with `onlyNewAwards`, an award is delivered once. A supplier comes back only when it has an award you haven't received.
- **Spending limits:** if you set a maximum cost per run, the Actor stops cleanly when it's reached. Suppliers it didn't deliver come back on the next monitor run.
- **Overlapping runs:** if a scheduled run starts while the previous run of the same monitor is still going, it stops at once without output or charges.

### Troubleshooting

- **Few rows?** Many awards are added weeks or months after a tender opens. To catch them, scan more tenders (15,000 is about 4–5 months) or start from an older number.
- **Many "skipped" tenders in the summary:** that's the eTenders server error described above, not a fault in the Actor.
- **Run failed with "could not be reached":** the eTenders API is down. Try again later.
- **"Only new awards" returned nothing:** everything in the range was already delivered to this monitor. Use a new `stateKey` to start over.
- **Summary says `status: skipped`:** another run of the same monitor was still in progress, so this one stopped without output or charges.

### Known restrictions

- Up to 50,000 tenders per run, and up to 8 parallel requests, to stay polite to a government server.
- `onlyNewAwards` remembers up to 200,000 awards per monitor.
- Up to 36,000 seconds of scanning per run (`maxRuntimeSeconds`, default 3,300).
- A run that hits `maxRuntimeSeconds` outputs what it found. Its summary says `status: partial` and gives the lowest tender number reached.

# Actor input Schema

## `supplierNames` (type: `array`):

Only keep suppliers whose name contains one of these (not case-sensitive), e.g. "security" or "Microsoft". Leave empty to see every supplier in the scanned range.

## `supplierIds` (type: `array`):

Only keep these eTenders supplier ids (the supplier_id field of an earlier run). The most precise way to follow a known company.

## `toOcid` (type: `integer`):

The scan starts at this eTenders tender number (the digits after ocds-9t57fa-) and counts back. 0 means the newest tender. Awards appear weeks or months after a tender opens, so the default (157000, May 2026) returns awards straight away.

## `recordsToScan` (type: `integer`):

How many tender numbers to check. About 110 tenders are published a day; 500 takes about a minute, 15000 (about 4-5 months) roughly half an hour. Maximum 50000 per run.

## `buyerContains` (type: `string`):

Only count awards from buyers whose name contains this text.

## `categories` (type: `array`):

Only count awards in these procurement categories.

## `provinces` (type: `array`):

Only count awards from buyers whose name places them in these provinces. eTenders has no province field; national departments and most municipalities have none and are dropped when this is set.

## `minContractValue` (type: `integer`):

Only count awards with a signed contract worth at least this much. Awards without a published contract are dropped when this is set.

## `onlyNewAwards` (type: `boolean`):

For scheduled monitoring: leave out awards already delivered by an earlier run of the same monitor (same state key and filters), so you are charged once per supplier delivery. A run started while the same monitor is still running stops without output.

## `stateKey` (type: `string`):

Name for this monitor's memory when 'Only awards not seen in earlier runs' is on. Monitors with different filters keep separate memories automatically; use a different key to start a watch list over.

## `includeAwardList` (type: `boolean`):

Adds the individual awards (with links to the official records) to each supplier row.

## `maxResults` (type: `integer`):

Stop after this many supplier rows. Leave empty for no limit.

## `concurrency` (type: `integer`):

Requests sent to the eTenders API at once. Keep this low; it is a government server.

## `maxRuntimeSeconds` (type: `integer`):

Stop scanning after this long and output what was found. The summary says how far the scan got.

## Actor input object example

```json
{
  "supplierNames": [],
  "toOcid": 157000,
  "recordsToScan": 500,
  "onlyNewAwards": false,
  "stateKey": "default",
  "includeAwardList": true,
  "concurrency": 4,
  "maxRuntimeSeconds": 3300
}
```

# Actor output Schema

## `results` (type: `string`):

One row per winning supplier: award count, known signed contract values, buyers, categories and each award's official eTenders link.

## `runSummary` (type: `string`):

Tender range scanned, tenders read, skipped (eTenders server error) or missing, suppliers output, awards already delivered in monitor mode and stop reason.

# 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 = {
    "supplierNames": [],
    "toOcid": 157000,
    "recordsToScan": 500
};

// Run the Actor and wait for it to finish
const run = await client.actor("mastazee_data_lab/south-africa-government-supplier-tracker").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 = {
    "supplierNames": [],
    "toOcid": 157000,
    "recordsToScan": 500,
}

# Run the Actor and wait for it to finish
run = client.actor("mastazee_data_lab/south-africa-government-supplier-tracker").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 '{
  "supplierNames": [],
  "toOcid": 157000,
  "recordsToScan": 500
}' |
apify call mastazee_data_lab/south-africa-government-supplier-tracker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,mastazee_data_lab/south-africa-government-supplier-tracker"
        }
    }
}
```

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/1yL47pYLU0pMgDe17/builds/UlnYEJhpJtaJnupml/openapi.json
