# South Africa Tenders Scraper: eTenders Feed (`agents001/sa-tenders-feed`) Actor

Returns open South African government tenders from eTenders: tender number, buyer, province, category, closing date and time, briefing sessions and document links. Filter by province, buyer, category or keyword. Daily mode returns only new tenders.

- **URL**: https://apify.com/agents001/sa-tenders-feed.md
- **Developed by:** [Mike Ross](https://apify.com/agents001) (community)
- **Categories:** Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 tender records

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Tenders Scraper: eTenders Feed

Get a clean, filterable feed of open South African government tenders, straight from National Treasury's official eTenders portal. Every record comes with the tender number, buyer, province, category, closing date and time, briefing session details and direct links to the tender documents.

No login, no copying from web pages, no PDFs to read just to find out what is on offer. Run it once, schedule it daily, or plug it into your own alerts.

### What it does

- Reads new tender notices from the **national eTenders portal** (etenders.gov.za) through its official Open Contracting (OCDS) API.
- Adds the details OCDS leaves out (province, detailed category, briefing session, delivery location and live status) from the same portal.
- Filters by publication date, status, province, organ of state, category, keywords and closing date.
- Returns one tidy record per tender, with the same field names every time.

The eTenders portal already carries notices from national and provincial departments, municipalities and state-owned companies, including Eskom, Transnet, SANRAL, the City of Cape Town and the City of Tshwane.

### Who it is for

- **Suppliers and contractors** who want a daily list of tenders that fit their trade and province.
- **B-BBEE consultants and bid writers** who track opportunities for several clients.
- **Tender alert services and newsletters** that need a reliable, structured feed to build on.
- **Researchers and journalists** looking at public procurement activity.

### Quick start

1. Click **Start** with the default input. You get up to 50 open tenders published in the last 3 days.
2. Narrow it down: pick provinces, add keywords like `security` or `solar`, or set a closing date window.
3. Schedule it to run every morning and connect the dataset to email, Google Sheets, Slack or your own system with an Apify integration or webhook.

### Input

| Field | What it does | Default |
|---|---|---|
| `publishedWithinDays` | Tenders published from this many days ago up to today | 3 |
| `publishedFrom` / `publishedTo` | Exact publication window instead of "last N days". Accepts `2026-10-01` or `2 weeks` | not set |
| `status` | `open`, `closed`, `cancelled`, `awarded` or `all` | `open` |
| `provinces` | One or more of the 9 provinces, or `National` | all |
| `organsOfState` | Buyer name contains any of these words, for example `Transnet`, `City of Cape Town`, `Health` | all |
| `categories` | Category contains any of these words, for example `Construction`, `Security`, `works` | all |
| `keywords` | Title, description or tender number contains any of these words | none |
| `closingDateFrom` / `closingDateTo` | Only tenders closing in this window. `7 days` means seven days from today | not set |
| `maxItems` | Stop after this many tenders, newest first. `0` means no limit | 50 |
| `includeContactPersons` | Add the official contact email from the notice | false |
| `onlyNew` | Return only tenders not delivered by an earlier run with the same filters. See Daily monitoring below | false |
| `stateStoreName` | Optional custom name for the store that remembers delivered tenders | automatic |

Example input:

```json
{
  "publishedWithinDays": 7,
  "status": "open",
  "provinces": ["Western Cape", "Gauteng"],
  "keywords": ["cleaning", "security"],
  "closingDateFrom": "7 days",
  "maxItems": 200
}
```

### Daily monitoring

Turn on `onlyNew` (Only new since last run) and schedule the Actor. Each run then returns only tenders it has not delivered before, so your email digest, sheet or Slack channel gets each tender once, and you pay only for the new ones.

How it works:

- The Actor remembers the OCDS id (`ocid`) of every tender it delivers, in a named key-value store in your Apify account. The name is built from a hash of your filters, for example `sa-tenders-seen-8b41d0c7e2f95a13`. The exact name is shown in the log and in `RUN_SUMMARY`.
- The filters in the hash are `status`, `provinces`, `organsOfState`, `categories`, `keywords` and `sources`. The publication and closing date windows, `maxItems` and `includeContactPersons` are not, so a rolling window keeps the same memory. Change a filter and you get a fresh, separate memory.
- Set `stateStoreName` to choose the name yourself, for example one store per client, or to start over with a new name. Deleting the store in Storage also starts over.
- The last 50,000 tender IDs are kept; older ones are dropped.
- A tender is delivered once per memory, even if its status or closing date changes later.

Example schedule: every weekday at 07:00 South African time (cron `0 7 * * 1-5`, timezone Africa/Johannesburg) with this input:

```json
{
  "publishedWithinDays": 3,
  "status": "open",
  "provinces": ["Western Cape"],
  "keywords": ["security", "cleaning"],
  "maxItems": 500,
  "onlyNew": true
}
```

The 3 day window overlaps the previous runs on purpose. Notices published late or over a weekend are still caught, and the overlap is never delivered twice. Keep `maxItems` high enough for a full day of new tenders, because it counts new tenders only.

### Output

Each tender is one item in the dataset. Dates are in South African time (`+02:00`). You can download the data as JSON, CSV, Excel or XML, or read it through the Apify API.

```json
{
  "source": "etenders.gov.za",
  "ocid": "ocds-9t57fa-174760",
  "sourceTenderId": "174760",
  "tenderNumber": "TNPA/2026/10/0006/114898/RFP",
  "title": "LEASING OF PREMISES DESCRIBED AS LAND ASSET NUMBER ERF EBX2466L, AND EBX2467L LOCATED ON A PORTION OF ERF 19711 AND ERF 19720, WEST BANK, IN THE PORT OF EAST...",
  "description": "LEASING OF PREMISES DESCRIBED AS LAND ASSET NUMBER ERF EBX2466L, AND EBX2467L LOCATED ON A PORTION OF ERF 19711 AND ERF 19720, WEST BANK, IN THE PORT OF EAST LONDON FOR A PERIOD OF UP TO TEN (10) YEARS",
  "tenderType": "Request for Proposal",
  "procurementMethod": "open",
  "buyerName": "Transnet SOC Ltd",
  "buyerId": "667",
  "province": "Eastern Cape",
  "category": "Rental and leasing activities",
  "mainProcurementCategory": "services",
  "status": "open",
  "sourceStatus": "Published",
  "publishedDate": "2026-10-19T00:00:00+02:00",
  "closingDateTime": "2026-11-27T15:00:00+02:00",
  "daysUntilClosing": 39,
  "briefingSession": {
    "hasBriefing": true,
    "isCompulsory": false,
    "dateTime": "2026-10-30T12:00:00+02:00",
    "venue": "MS TEAMS Meeting - Refer to tender documents for the link"
  },
  "deliveryLocation": "1 Hely Hutchinson Drive, Quigney, East London, 5201",
  "specialConditions": null,
  "eSubmission": false,
  "documents": [
    {
      "title": "TNPA2026100006114898RFP- Request for Proposal (4).pdf",
      "url": "https://www.etenders.gov.za/home/Download?blobName=9944e9e9-d0ac-4c27-a494-b1c590e1bec7.pdf&downloadedFileName=...",
      "format": "pdf",
      "datePublished": "2026-10-19T22:04:17+02:00"
    }
  ],
  "documentCount": 7,
  "sourceUrl": "https://ocds-api.etenders.gov.za/api/OCDSReleases/release/ocds-9t57fa-174760",
  "portalUrl": "https://www.etenders.gov.za/Home/opportunities?id=1",
  "contactEmail": null,
  "enriched": true,
  "dataOrigin": "ocds+portal",
  "licence": "Source: National Treasury eTenders portal (etenders.gov.za) via the OCDS API. ...",
  "scrapedAt": "2026-10-19T23:28:57+02:00",
  "schemaVersion": 1
}
```

The Output tab has three ready-made views: **Tenders** (the key columns), **Briefing sessions** and **Documents** (one row per document with a download link).

A short run report is saved in the key-value store as `RUN_SUMMARY` (items returned, records skipped, any source problems).

### Pricing

**$3 per 1,000 tenders** returned (pay per event, charged once per tender saved). You only pay for tenders that land in your dataset, so tight filters keep costs low. Set a maximum cost per run in the run options and the Actor stops cleanly when it is reached.

### Data licence and attribution

The data is published by the Office of the Chief Procurement Officer, National Treasury, on the eTenders and data.etenders.gov.za portals. Treasury's publication policy says you may use the data for any purpose, including commercial use, and may republish it as long as **a link to the original source stays intact**. The OCDS API points to the Open Data Commons PDDL, while the data.etenders.gov.za homepage states CC BY 4.0. To be safe this Actor follows the stricter of the two: credit National Treasury eTenders and keep the link to the source. Each record keeps a `sourceUrl` and a `licence` note so you can meet the attribution condition when you pass the data on.

Treasury does not guarantee that the data is complete or up to date. Always check the official notice and documents before you submit a bid.

### Privacy (POPIA)

By default the Actor returns **no personal information**. If you switch on `includeContactPersons`, it adds only the official contact email published on the notice. It never returns contact names, phone or fax numbers, and it drops free webmail addresses (Gmail, Yahoo and similar) because they are likely to be personal. If you store or use these emails, you are responsible for doing so lawfully under POPIA, for example only to ask questions about the tender.

### FAQ

**How fresh is the data?**
Treasury publishes new notices to the OCDS API through the day. A daily run each morning catches everything from the previous working day. The default 3 day window means a Monday run still catches Friday's notices.

**Why do some tenders have no province or category?**
These come from the eTenders portal rather than the OCDS API. If the portal is down during a run, you still get the OCDS data, and `enriched` is set to `false` on those records.

**Why is the title cut short?**
eTenders has no separate short title, so the `title` is the first 160 or so characters of the notice description. The full text is in `description`.

**Does it include Eskom, Transnet and municipal tenders?**
Yes, when they are advertised on the national eTenders portal, which is required for most public bodies. Some entities also run their own portals; separate sources for those are on the roadmap.

**Can I get closed, cancelled or awarded tenders?**
Yes. Set `status` to `closed`, `cancelled`, `awarded` or `all`. Award details (winning bidder and value) are not included yet.

**How do I get alerts?**
Schedule the Actor daily with your filters and `onlyNew` turned on (see Daily monitoring), then use an Apify integration (email, Slack, Google Sheets, Zapier, Make) or a webhook to send new results wherever you need them.

**Something looks wrong. What should I do?**
Open an issue on the Actor's Issues tab with the run link. Include the tender number if a specific record is affected.

### Disclaimer

This Actor is independent and is not affiliated with or endorsed by National Treasury or any organ of state. It reads public data published for reuse.

# Actor input Schema

## `publishedWithinDays` (type: `integer`):

Return tenders published from this many days ago up to today (South African time). Ignored when 'Published from' is set.

## `publishedFrom` (type: `string`):

Start of the publication window. Use a date (2026-10-01) or a relative value ("2 weeks" means two weeks ago). Overrides 'Published in the last N days'.

## `publishedTo` (type: `string`):

End of the publication window (inclusive). Defaults to today.

## `closingDateFrom` (type: `string`):

Only tenders closing on or after this date. A relative value like "7 days" means seven days from today. Useful to skip tenders you cannot respond to in time.

## `closingDateTo` (type: `string`):

Only tenders closing on or before this date. A relative value like "30 days" means thirty days from today.

## `status` (type: `string`):

Tender status. 'Open' means published and not yet closed. 'All' also includes draft notices that appear in the OCDS data before going live.

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

Only tenders for these provinces. Leave empty for all. 'National' covers national departments and state-owned companies.

## `organsOfState` (type: `array`):

Only tenders whose buyer name contains any of these words, for example "Transnet", "Eskom", "City of Cape Town" or "Health". Case-insensitive.

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

Only tenders whose eTenders category (or OCDS goods/services/works class) contains any of these words, for example "Construction", "Security", "Supplies: Computer Equipment" or "works".

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

Only tenders whose title, description or tender number contains any of these words or phrases, for example "solar", "cleaning services". Case-insensitive.

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

Stop after this many tenders (newest first). Set 0 for no limit. You pay per tender returned.

## `includeContactPersons` (type: `boolean`):

Add the official contact email published on the notice. For POPIA data minimisation, contact names, phone numbers and free webmail addresses are never included.

## `onlyNew` (type: `boolean`):

For scheduled monitoring. When on, the Actor remembers which tenders (by OCDS id) it has already delivered for this combination of filters and returns only tenders it has not delivered before. You pay only for the new tenders that are saved. State is kept in a named key-value store in your account, named from a hash of your filters (status, provinces, organs of state, categories, keywords, sources). Publication and closing date windows and Maximum tenders are not part of it, so a rolling window keeps the same state. The last 50,000 tender IDs are remembered.

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

Only used with 'Only new since last run'. Override the automatic name of the key-value store that remembers delivered tenders, for example to share state between schedules or to start fresh with a new name. Lowercase letters, numbers and hyphens, up to 63 characters. Leave empty for the automatic name.

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

Which tender sources to read. Currently only the national eTenders portal.

## `requestDelaySecs` (type: `number`):

Minimum seconds between requests to the same server. Keep this polite: the portal is a public government service.

## Actor input object example

```json
{
  "publishedWithinDays": 3,
  "status": "open",
  "maxItems": 50,
  "includeContactPersons": false,
  "onlyNew": false,
  "sources": [
    "etenders"
  ],
  "requestDelaySecs": 0.5
}
```

# Actor output Schema

## `tenders` (type: `string`):

No description

## `runSummary` (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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("agents001/sa-tenders-feed").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("agents001/sa-tenders-feed").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 '{}' |
apify call agents001/sa-tenders-feed --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,agents001/sa-tenders-feed"
        }
    }
}
```

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/zaczDWIcH79lnkaLA/builds/XVyxpycYWfIL3w5hf/openapi.json
