If your business supplies the South African public sector, finding the right tender in time is half the work. A cleaning company in the Western Cape, a civil contractor in KwaZulu-Natal or a bid writer working for ten clients all have the same morning routine: open the eTenders portal, scroll, search, open notices one by one and copy the details into a spreadsheet.
Missing one notice can mean missing a compulsory briefing session, and that usually rules you out of the bid.
This tutorial shows how to replace that routine with a scheduled feed that lands every morning with only the new tenders that match your province, category and keywords. It uses the South Africa Tenders Scraper on the Apify Store.
Where the data comes from
National Treasury runs the eTender Publication Portal at etenders.gov.za, the central place where public bodies advertise tenders. It carries notices from national and provincial departments, municipalities and state-owned companies such as Eskom, Transnet and SANRAL.
Treasury also publishes this data in machine-readable form through an API that follows the Open Contracting Data Standard (OCDS), an international format for public procurement data. OCDS gives each tender a stable ID (ocid), the buyer, the description, documents and dates. Some details that bidders care about, such as province, the detailed eTenders category, briefing session details and delivery location, are on the portal itself. The Actor reads the OCDS API, adds those details from the same portal and returns one record per tender with the same field names every time.
Treasury's publication policy allows reuse, including commercial use, as long as a link to the original source stays intact. Every record includes a sourceUrl and a licence note so you can keep that attribution when you share the data.
The Actor is independent and is not affiliated with or endorsed by National Treasury or any organ of state.
Step 1: Run it once in the Apify Console
Open the Actor's Store page (link at the end) and try it with the default input. You get up to 50 open tenders published in the last 3 days.
Then narrow it down with these fields:
-
Published in the last N days (
publishedWithinDays): default 3. -
Status:
openby default. You can also pickclosed,cancelled,awardedorall. -
Provinces: any of the nine provinces, or
Nationalfor national departments and state-owned companies. -
Organ of state / department contains: words in the buyer name, for example
Transnet,City of Cape TownorHealth. -
Category contains: words in the eTenders category, for example
Construction,Security,Services: Professional, or the OCDS classworks. -
Keywords: words in the title, description or tender number, for example
solarorcleaning services. -
Closing on or after (
closingDateFrom): set this to7 daysto skip tenders closing too soon for you to respond. -
Maximum tenders (
maxItems): default 50, newest first.
Click Start. The Output tab has three views: Tenders (key columns), Briefing sessions, and Documents with one row per document and its download link.
Step 2: Schedule it every weekday morning with onlyNew
- Switch on Only new since last run (
onlyNew). The Actor remembers the OCDS id of every tender it delivers, in a named key-value store in your Apify account, and later runs return only tenders it has not delivered before. You pay only for the new ones. - Keep Published in the last N days at 3. A Monday run then still catches Friday's notices, and the overlap is never delivered twice.
- Set Maximum tenders high enough for a full day, for example 500.
- Save the input as a task. In Schedules, create a schedule with cron
0 7 * * 1-5and timezoneAfrica/Johannesburg, and add the task.
The memory is tied to your filters (status, provinces, organs of state, categories, keywords). Changing one starts a fresh memory. If you track tenders for several clients, give each one its own task and set State store name to something like client-abc-tenders so their histories stay separate.
Step 3: Pull the data with Python
The official apify-client package can run the Actor and read the results. Install it with pip install apify-client and copy your API token from the Integrations section of the Console.
This script fetches new Western Cape and Gauteng tenders for security and cleaning work, then prints them in closing date order with a warning for compulsory briefings.
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run_input = {
"publishedWithinDays": 3,
"status": "open",
"provinces": ["Western Cape", "Gauteng"],
"keywords": ["security", "cleaning"],
"closingDateFrom": "7 days",
"maxItems": 500,
"onlyNew": True,
}
run = client.actor("agents001/sa-tenders-feed").call(
run_input=run_input,
max_total_charge_usd=2,
)
if run is None:
raise SystemExit("Run failed to start or finish")
tenders = list(client.dataset(run.default_dataset_id).iterate_items())
tenders.sort(key=lambda t: t["closingDateTime"] or "")
for t in tenders:
print(f"{t['tenderNumber']} | {t['buyerName']} | {t['province']}")
print(f" {t['title']}")
print(f" Closes {t['closingDateTime']} ({t['daysUntilClosing']} days)")
briefing = t.get("briefingSession") or {}
if briefing.get("isCompulsory"):
print(f" COMPULSORY briefing: {briefing.get('dateTime')} at {briefing.get('venue')}")
print(f" Source: {t['sourceUrl']}\n")
call() waits for the run to finish and returns None if it fails. iterate_items() reads the whole dataset page by page.
What the output looks like
One record from a sample run, trimmed (the full record lists each document with its title, format and download link):
{
"ocid": "ocds-9t57fa-174759",
"tenderNumber": "CCMA/2026/05/DR (B)",
"title": "THE APPOINTMENT OF A SERVICE PROVIDER FOR THE PROVISION OF PRINTING, SUPPLY AND DELIVERY OF A4 CASE FILES WITH LABELS AND CLIPS TO VARIOUS CCMA OFFICES...",
"tenderType": "Request for Proposal",
"buyerName": "Commission for Conciliation Mediation & Arbitration",
"province": "National",
"category": "Other service activities",
"mainProcurementCategory": "services",
"status": "open",
"publishedDate": "2026-10-19T00:00:00+02:00",
"closingDateTime": "2026-11-09T11:00:00+02:00",
"daysUntilClosing": 21,
"briefingSession": {
"hasBriefing": true,
"isCompulsory": true,
"dateTime": "2026-10-27T11:00:00+02:00",
"venue": "Link"
},
"eSubmission": false,
"documentCount": 1,
"sourceUrl": "https://ocds-api.etenders.gov.za/api/OCDSReleases/release/ocds-9t57fa-174759",
"contactEmail": null,
"enriched": true
}
Things to notice:
- Dates are in South African time (
+02:00). -
daysUntilClosingsaves you doing date maths in a spreadsheet. -
briefingSession.isCompulsoryis the field to watch. Missing a compulsory briefing normally disqualifies a bid. -
contactEmailis empty by default. A switch adds the official contact email from the notice. Contact names, phone numbers and free webmail addresses are never included, in line with POPIA data minimisation. -
enriched: falsemeans the portal could not be reached during that run, so province and category may be missing for that record.
What it costs
The Actor uses pay-per-event pricing: $3 per 1,000 tenders returned, so $0.003 per tender. Tenders your filters drop are not charged.
Examples based on that price:
- A tight feed for one province and two keywords that returns 10 new tenders a day costs $0.03 a day, roughly $0.63 over 21 working days.
- A broad feed of every new open tender nationally that returns 200 a day would cost $0.60 a day.
You can set a maximum cost per run and the Actor stops cleanly when it is reached. Check the Store page for current pricing.
What to do with the data
- Google Sheets: send each run's dataset to a shared sheet with the Apify Google Sheets integration. Add your own columns for "Bid / No bid", owner and status.
- Email or Slack alerts: use an Apify integration or a webhook to post new tenders to a channel or inbox each morning, with the compulsory briefing date in the message.
-
Calendar: create calendar entries for briefing sessions and closing dates from
briefingSession.dateTimeandclosingDateTime. - Client digests: if you are a bid writer or B-BBEE consultant, run one task per client with their provinces and categories, and send each client their own short list.
-
Research: set
statustoallwith a wider date window to study what different departments buy.
Before you bid, remember that most organs of state require suppliers to be registered on National Treasury's Central Supplier Database. The feed helps you find the tender. The official documents tell you what you need to submit.
Limitations
- Only the national eTenders portal is covered today. Some entities also advertise on their own procurement portals, and those notices may not appear.
-
titleis the first 160 or so characters of the description, because eTenders has no separate short title. The full text is indescription. - If the portal is down during a run, records still come through from OCDS but may lack province, category and briefing details.
- Award details (winning bidder and value) are not included yet.
- With
onlyNew, a tender is delivered once. Later changes such as an extended closing date are not sent again, so check the notice before you submit. - Treasury does not guarantee that the data is complete or current. Always confirm against the official notice and documents.
Top comments (0)