# South Africa Government Tender Awards (`mastazee_data_lab/south-africa-government-tender-awards`) Actor

Who won South African government tenders, for how much and from which buyer: award rows from the official eTenders API, linked to source.

- **URL**: https://apify.com/mastazee_data_lab/south-africa-government-tender-awards.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 $3.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 Tender Awards

Find out **who won South African government tenders, for how much, and from which buyer.** This Actor reads National Treasury's official eTenders open-data API and gives you one clean row per award: the winning supplier, the buyer, the contract value, the dates and a link back to the official record.

eTenders publishes open tenders well, but its award data is hard to use as it comes: a quarter to almost half of records make the API return a server error, award values are published with the decimal point missing, and supplier names come wrapped in stray spaces and brackets. This Actor skips and counts the broken records, reports the reliable signed-contract value instead, and cleans the names.

### What you can use it for

- **Track your competitors.** See which tenders a rival supplier has won, from which departments, and for how much. Filter by supplier name.
- **Size a market.** Pull every awarded security, cleaning or IT contract for a province or a department and add up what was spent.
- **Plan for re-tenders.** See who is buying what you sell, and when the contract ends (`contract_end_date`), so you are ready when it comes up again.

> **Tested on 26 October 2026** against the live eTenders API: the newest 800 tenders gave 141 award rows in about 1.5 minutes (215 tenders skipped by the eTenders server error described below), and the default input returns award rows from May 2026. On the Apify platform the default input returned 39 award rows in about 1.7 minutes.
>
> This is a data tool, not a lead guarantee. Coverage is what buyers publish on eTenders (see *Coverage and limits*).

### What you get

One row per award per supplier. A sample row from a real run:

| Field | Example |
|---|---|
| `supplier_name` | REAL SEC |
| `supplier_id` | eTenders' own supplier number, the same across all of that supplier's awards |
| `supplier_enterprise_size` | EME (exempted micro enterprise), QSE (qualifying small enterprise) or GEN, as eTenders records it |
| `buyer_name` | Kwazulu Natal - Education |
| `buyer_province` | KwaZulu-Natal |
| `tender_title` | APPOINTMENT OF A SERVICE PROVIDER/S TO RENDER SECURITY SERVICES … |
| `tender_reference` | the buyer's bid number |
| `category` | goods, services or works |
| `procurement_method` | e.g. Request for Quotation, Request for Bid (Open-Tender), Single source bidding |
| `contract_value_zar` | 8365627 |
| `contract_signed_date` | 2026-06-10 |
| `contract_start_date`, `contract_end_date` | contract period, when published |
| `tender_published_date`, `tender_closing_date` | when the tender opened and closed |
| `award_value_as_published` | the award amount exactly as eTenders shows it (see below) |
| `ocid`, `source_url`, `licence` | the official record and its open-data licence |

#### Read this before you add up values

- **`contract_value_zar` is the value to use.** It comes from the signed contract, in whole rand, with any cents dropped.
- **`award_value_as_published` is usually wrong by a factor of 10 or 100.** eTenders drops the decimal point from award amounts: a contract worth R8,365,627 is listed as an award of 836562779. We checked all 11 awards in our sample that also have a contract, and every one was exactly 10× or 100× too high. We keep the published figure for completeness but never "fix" it, because the number of missing decimal places can't be known without the contract.
- **Not every award has a contract yet.** In our samples about a third did. Without one, `contract_value_zar` and the contract dates are empty.
- **`buyer_province` comes from the buyer's name**, because eTenders has no province field. It is filled for provincial departments ("Gauteng Health", "Kwazulu Natal - Education") and the big metros (Johannesburg, Tshwane, Ekurhuleni, Cape Town, eThekwini, Nelson Mandela Bay, Buffalo City, Mangaung). National departments and most local municipalities have no province.
- **Personal contact details are removed.** eTenders attaches an official's name, phone number and email to each award. This Actor never outputs them.

### How to run it

eTenders numbers every tender in order (`ocds-9t57fa-1`, `-2`, … about 175,000 so far, roughly 110 more each day). The Actor checks a run of those numbers, counting back from a starting point, and saves the awards it finds.

| Input | What it does |
|---|---|
| **Start from tender number** | Where to start counting back. `0` means the newest tender. The default, 157000, is late May 2026 and returns awards straight away. |
| **Tenders to scan** | How many tender numbers to check. About one readable tender in seven has an award. 500 takes under a minute; 15,000 covers four to five months. |
| **Maximum awards** | Stop after this many rows. |
| **Maximum scan time** | Stop scanning after this many seconds (default 3,300) and keep what was found. |
| **Buyer / supplier name contains** | Keep only matching names. Not case-sensitive. |
| **Categories, Buyer province, Minimum contract value** | Narrow the results further. |

**Tip:** awards appear weeks or months after a tender is published. If you start from the newest tender (`0`), scan at least 15,000 tenders, or you will mostly see tenders that haven't been awarded yet.

Each run also saves a `RUN_SUMMARY` record in its key-value store, showing how many tenders were read, skipped or missing.

### Coverage and limits

- Source: [eTenders OCDS API](https://ocds-api.etenders.gov.za/), published by National Treasury under the [Open Data Commons PDDL](https://opendatacommons.org/licenses/pddl/1-0/) (public domain). Every row links to its source record, as the [eTenders publication policy](https://data.etenders.gov.za/Home/LearnMore) asks.
- eTenders covers national and provincial departments plus the municipalities and state-owned entities that choose to publish there. It is not every public contract in South Africa.
- About 25–45% of eTenders records cannot be read at all: the official API returns a server error for them (215 of the newest 800 on 26 October 2026). The Actor skips them and counts them in `RUN_SUMMARY`. When Treasury fixes those records, re-running the same range picks them up.
- The Actor sends at most 8 requests at a time (4 by default), to be polite to a government server.
- Up to 50,000 tenders per run (over a year of tenders). A run that reaches its maximum scan time keeps what it found; its `RUN_SUMMARY` says `status: partial` and gives the lowest tender number reached.
- No "new since last run" mode: each run returns every award in the range it scans.

### Running time

About 8–10 tenders a second at the default 4 parallel requests. Measured on 26 October 2026:

| Scan | Time | Award rows |
|---|---|---|
| 500 tenders (default, May 2026) | ~1 min | ~39 |
| Newest 800 tenders (about a week) | ~1.5 min | 141 |
| 15,000 tenders (~4–5 months) | ~25–30 min (estimated) | roughly 1,200–2,600 (estimated) |

### Pricing

Pay per event: **$3.00 per 1,000 award rows** ($0.003 per row), plus Apify's standard tiny start fee. You are charged only for rows written to your dataset.

| How you use it | Award rows | You pay (approx.) |
|---|---|---|
| Default run (500 tenders) | ~39 | ~$0.12 |
| Weekly run over the newest ~800 tenders, for a month | ~600 | ~$1.80 a month |
| One 15,000-tender export (4–5 months) | ~1,200–2,600 | ~$3.60–7.80 |

- **Spending limits:** if you set a maximum cost per run, the Actor stops scanning as soon as it's reached, so you don't pay for work it can't deliver. The run summary says where it stopped.
- **Failed runs:** a run that can't reach eTenders outputs nothing and costs only the start fee.

### Feedback

Found a wrong value or need another field? Open an issue on the Actor's **Issues** tab.

# Actor input Schema

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

The scan starts at this eTenders tender number (the digits after ocds-9t57fa-) and works backwards. Set to 0 to start from the newest tender. Awards are published weeks or months after a tender opens, so the newest few thousand tenders hold few awards. The default, 157000, is May 2026.

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

How many tender numbers to check, counting back from the start. About 110 tenders are published per day and roughly one in seven readable tenders has an award. 500 takes under a minute; 15000 covers about four to five months (roughly 25 minutes). Maximum 50000 per run.

## `maxAwards` (type: `integer`):

Stop once this many award rows have been saved. Leave empty for no limit.

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

Only keep awards whose buyer name contains this text, for example "Revenue Service" or "Municipality". Not case-sensitive.

## `supplierContains` (type: `string`):

Only keep awards whose winning supplier name contains this text. Not case-sensitive.

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

Only keep these procurement categories. Leave empty for all.

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

Only keep buyers whose name places them in these provinces (e.g. "Gauteng Department of Health", "City of Johannesburg"). eTenders has no province field, so national departments and most municipalities have no province and are dropped when this filter is set.

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

Only keep awards with a signed contract worth at least this many rand. Awards without a published contract are dropped when this is set.

## `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 keep the awards already saved. The run summary says how far the scan got.

## Actor input object example

```json
{
  "toOcid": 157000,
  "recordsToScan": 500,
  "concurrency": 4,
  "maxRuntimeSeconds": 3300
}
```

# Actor output Schema

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

South African government tender award rows from eTenders: supplier, buyer, signed contract value, dates and the official record link.

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

Tender range scanned, tenders read, skipped (eTenders server error) or missing, awards output 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 = {
    "toOcid": 157000,
    "recordsToScan": 500
};

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

# Run the Actor and wait for it to finish
run = client.actor("mastazee_data_lab/south-africa-government-tender-awards").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 '{
  "toOcid": 157000,
  "recordsToScan": 500
}' |
apify call mastazee_data_lab/south-africa-government-tender-awards --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-tender-awards"
        }
    }
}
```

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/HwZPj0tEDfCkdlezo/builds/5Ioosh2cPM7ba68A5/openapi.json
