Official Python SDK for the Tenders-SA Developer API v2 — enriched South African public procurement data.
Tenders-SA.org is an AI-powered tender matching and application platform for South African businesses. It aggregates tenders from national, provincial, and municipal government departments, SOEs (Eskom, Transnet, SANRAL), and public entities — sourced directly from official OCDS (Open Contracting Data Standard) feeds.
The platform goes beyond simple aggregation: AI enrichment extracts key requirements, generates summaries, estimates tender values, classifies categories, and calculates compatibility scores between your company profile and each opportunity. The result is a unified intelligence layer over South Africa's fragmented public procurement landscape.
- Tender Discovery — Search and filter thousands of active, closed, and awarded tenders across all provinces and categories
- AI Enrichment — Every tender is processed through AI pipelines for summarisation, requirement extraction, value estimation, and classification
- Company Intelligence — Research award histories, track supplier performance, and perform due diligence
- Organisation Profiles — Procurement body profiles enriched with Google and Wikidata data
- Award Analytics — Analyse award patterns by enterprise type, BEE level, province, and category
- Tender Toolkit — BBBEE Calculator, Readiness Assessment, Market Heatmap, AI Proposal Generator
- Market Research — Analyse tender volumes, award patterns, and spending trends across provinces and sectors
- Competitive Intelligence — Track which companies are winning contracts in your industry
- Supplier Discovery — Find subcontractors and partners by analysing award histories
- Compliance Monitoring — Monitor procurement opportunities in specific categories or regions
The Tenders-SA Developer API v2 exposes enriched procurement data through a comprehensive set of RESTful endpoints. It serves from a dedicated infrastructure layer with its own database, synced from the main platform, ensuring the API remains fast and available independently of the main web application.
https://api.tenders-sa.org/v2
All API requests require a Bearer token passed via the Authorization header:
Authorization: Bearer tsa_prod_YOUR_API_KEY
API keys are generated through the Developer Portal. Keys use the format tsa_prod_ followed by a unique generated string.
Access Requirements: API access requires a Professional or Enterprise subscription.
| Plan | Max Keys | Daily Limit | Monthly Limit |
|---|---|---|---|
| Professional | 3 | 500 | 15,000 |
| Enterprise | 25 | 10,000 | 300,000 |
All API responses follow a consistent envelope:
Success:
{
"success": true,
"data": { ... },
"meta": {
"requestId": "req_uuid",
"timestamp": "2026-01-01T00:00:00Z",
"apiVersion": "v2",
"page": 1,
"pageSize": 20,
"totalCount": 142,
"totalPages": 8,
"hasNext": true,
"hasPrev": false,
"rateLimit": {
"limit": 500,
"remaining": 498,
"reset": "2026-01-02T00:00:00Z",
"policy": "daily"
}
}
}Rate limit status is returned in both response headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-RateLimit-Policy) and the response body's meta.rateLimit object. When exceeded, a 429 status is returned.
| Status | Code | Description |
|---|---|---|
| 400 | BAD_REQUEST |
Invalid request parameters |
| 401 | UNAUTHORIZED |
Missing or invalid API key |
| 403 | FORBIDDEN / KEY_NOT_ACTIVE / KEY_EXPIRED |
Key not active or expired |
| 404 | NOT_FOUND |
Resource not found |
| 409 | CONFLICT / KEY_LIMIT_REACHED |
Key limit reached for plan |
| 429 | RATE_LIMIT_DAILY_EXCEEDED / RATE_LIMIT_MONTHLY_EXCEEDED |
Rate limit exceeded |
| 500 | INTERNAL_ERROR |
Server error |
| 502 | SERVICE_UNAVAILABLE |
Service temporarily unavailable |
pip install tendersa-sdk- Python 3.9+
- httpx 0.27+
import asyncio
from tendersa import TendersaClient
async def main():
client = TendersaClient(api_key="tsa_prod_your_key")
# List open tenders
tenders = await client.tenders.list({"status": "OPEN", "province": "Western Cape"})
for t in tenders.data:
print(t.title, t.status)
# Get tender detail
detail = await client.tenders.get("tender_001")
print(detail.title, detail.estimated_value)
# AI-powered search
results = await client.tenders.search({"q": "road construction"})
await client.close()
asyncio.run(main())from tendersa import TendersaClient
client = TendersaClient(
api_key="tsa_prod_your_key",
base_url="https://api.tenders-sa.org/v2", # default
timeout=30.0, # 30s (default)
max_retries=3, # exponential backoff (default)
)The client also supports async context manager usage:
async with TendersaClient(api_key="tsa_prod_your_key") as client:
result = await client.tenders.list({"status": "OPEN"})The SDK is organised into 16 resource classes, accessed as properties on the client.
Full tender discovery, filtering, search, and detail.
# List with filters
result = await client.tenders.list({
"status": "OPEN",
"category": "Construction",
"province": "Gauteng",
"sort": "-closingDate",
})
# Search
results = await client.tenders.search({"q": "road construction"})
# Filtered lists
closing = await client.tenders.closing_soon()
new = await client.tenders.new_tenders()
bbbee = await client.tenders.bbbee_required()
by_province = await client.tenders.by_province("Gauteng", {"status": "OPEN"})
by_org = await client.tenders.by_organization("org_001")
# Value range
range_results = await client.tenders.value_range(1_000_000, 10_000_000)
# Counts
prov_counts = await client.tenders.counts_by_province()
cat_counts = await client.tenders.counts_by_category()
org_counts = await client.tenders.counts_by_organization()
status_counts = await client.tenders.counts_by_status()
# Sub-resources
detail = await client.tenders.get("tender_001")
docs = await client.tenders.documents("tender_001")
awards = await client.tenders.awards("tender_001")
timeline = await client.tenders.timeline("tender_001")
contracts = await client.tenders.contracts("tender_001")
milestones = await client.tenders.milestones("tender_001")
bidders = await client.tenders.bidders("tender_001")
sub_req = await client.tenders.submission_requirements("tender_001")
related = await client.tenders.related("tender_001")
# AI analysis
analysis = await client.tenders.analysis("tender_001")
estimate = await client.tenders.value_estimate("tender_001")
# SEO and slug
seo_data = await client.tenders.seo("tender_001")
slug_data = await client.tenders.slug("tender_001")Award data and market analytics.
result = await client.awards.list({"province": "Western Cape", "beeLevel": "Level 1"})
award = await client.awards.get("award_001")
# Filtered lists
by_tender = await client.awards.by_tender("tender_001")
by_supplier = await client.awards.by_supplier("BuildCorp SA")
by_date = await client.awards.by_date_range({"from": "2025-01-01", "to": "2025-12-31"})
# Analytics
analytics = await client.awards.analytics({"groupBy": "province"})
by_prov = await client.awards.analytics_by_province({"from": "2025-01-01"})
by_cat = await client.awards.analytics_by_category()
by_bee = await client.awards.analytics_by_bee_level()
by_ent = await client.awards.analytics_by_enterprise_type()
# Subcontractors
subs = await client.awards.subcontractors("award_001")Supplier/contractor intelligence.
# Profile (includes awards + directors)
company = await client.companies.get("BuildCorp SA")
print(company.profile.name, company.profile.total_awards)
print(company.awards[0].supplier_name)
# Search
results = await client.companies.search({"q": "Construction", "beeLevel": "Level 1"})
# List all companies
all_companies = await client.companies.list({"province": "Gauteng"})
# Top companies
top = await client.companies.top()
# Lookup by registration number
by_reg = await client.companies.by_registration("2020/123456/07")
# Sub-resources
awards = await client.companies.awards("BuildCorp SA")
contracts = await client.companies.contracts("BuildCorp SA")
tenders = await client.companies.tenders("BuildCorp SA")
directors = await client.companies.directors("BuildCorp SA")Government departments and procurement bodies.
org = await client.organizations.get("org_001")
tenders = await client.organizations.tenders("org_001", {"status": "OPEN"})
# List, search, and lookups
orgs = await client.organizations.list()
results = await client.organizations.search({"q": "Health"})
by_slug = await client.organizations.by_slug("dept-health")
by_reg = await client.organizations.by_registration("123456")
counts = await client.organizations.counts_by_type()
directors = await client.organizations.directors("org_001")Company director information from CIPC sources.
directors = await client.directors.list({"fullName": "John"})
director = await client.directors.get("dir_001")
results = await client.directors.search({"q": "Smith"})
by_org = await client.directors.by_organization("org_001")Tender category reference data.
categories = await client.categories.list()
category = await client.categories.get("cat_001")
by_slug = await client.categories.by_slug("construction")Province data with health scores.
provinces = await client.provinces.list()
province = await client.provinces.get("gauteng")
scores = await client.provinces.health_scores("gauteng")SEO metadata and content.
cat_seo = await client.seo.category("construction")
prov_seo = await client.seo.province("gauteng")
articles = await client.seo.list_articles({"limit": 10})
article = await client.seo.get_article("article_001")
author = await client.seo.get_author("author_001")Industry value benchmarks.
benchmarks = await client.industry.list()
benchmark = await client.industry.get("bench_001")Service type classifications.
services = await client.services.list()
service = await client.services.get("consulting")Open Contracting Data Standard parties.
parties = await client.ocds.list_parties({"role": "buyer"})
party = await client.ocds.get_party("party_001")Market alerts and sector insights.
sources = await client.intel.list_sources()
source = await client.intel.get_source("src_001")
items = await client.intel.list_items({"category": "energy"})
item = await client.intel.get_item("item_001")Restricted supplier screening.
suppliers = await client.forensic.list_restricted_suppliers()
supplier = await client.forensic.get_restricted_supplier("rs_001")
matches = await client.forensic.match_restricted_supplier({"name": "Acme Corp"})
result = await client.forensic.check_restricted_supplier({"name": "Acme Corp"})Companies and Intellectual Property Commission data.
enrichments = await client.cipc.list_enrichments({"supplierName": "Build"})
enrichment = await client.cipc.get_enrichment("enr_001")
directors = await client.cipc.list_directors({"companyName": "BuildCorp"})
director = await client.cipc.get_director("dir_001")Newsletter editions.
editions = await client.newsletters.list()
edition = await client.newsletters.get("nl_001")Tender document metadata and download URLs.
doc = await client.documents.get("doc_001")
url = await client.documents.download_url("doc_001", {"requireR2": "1"})API status, usage, and reference data.
status = await client.meta.status()
provinces = await client.meta.provinces()
categories = await client.meta.categories()
usage = await client.meta.usage()
industries = await client.meta.industries()List endpoints support async iteration through pages:
async for page in client.tenders.paginated({
"status": "OPEN",
"category": "Construction",
}):
for tender in page:
print(tender.title, tender.closing_date)Each page is a PaginatedResponse object with convenience properties:
async for page in client.awards.paginated({"province": "Gauteng"}, max_pages=5):
print(f"Page {page.page}: {len(page)} items")
print(f" Total: {page.total_count}, Has next: {page.has_next}")The SDK throws typed exceptions for every API response status:
from tendersa.errors import (
TendersaError,
AuthError,
NotFoundError,
RateLimitError,
BadRequestError,
ForbiddenError,
ConflictError,
InternalError,
ServiceUnavailableError,
)
try:
tender = await client.tenders.get("nonexistent")
except AuthError:
print("Invalid API key. Get one at https://tenders-sa.org/developers/api-keys")
except NotFoundError:
print("Tender not found")
except RateLimitError as e:
print(f"Rate limited: {e.used}/{e.limit}. Resets at {e.resets_at}")
except TendersaError as e:
print(f"API error [{e.code}]: {e.message} (request: {e.request_id})")Every exception exposes:
status— HTTP status codecode— Machine-readable error codemessage— Human-readable descriptionrequest_id— For tracing with supportdocs— Link to error documentation
rl = client.last_rate_limit
if rl:
print(f"{rl.remaining}/{rl.limit} requests remaining ({rl.policy})")| Method | Endpoint |
|---|---|
list(params?) |
GET /v2/tenders |
get(id) |
GET /v2/tenders/{id} |
search(params) |
GET /v2/tenders/search |
closing_soon(params?) |
GET /v2/tenders/closing-soon |
new_tenders(params?) |
GET /v2/tenders/new |
bbbee_required(params?) |
GET /v2/tenders/bbbee-required |
value_range(min, max, params?) |
GET /v2/tenders/value-range |
by_province(province, params?) |
GET /v2/tenders/by-province/{province} |
by_organization(orgId, params?) |
GET /v2/tenders/by-organization/{orgId} |
by_publication_type(type, params?) |
GET /v2/tenders/by-publication-type/{type} |
by_category(category, params?) |
GET /v2/tenders/by-category/{category} |
counts_by_province() |
GET /v2/tenders/counts/province |
counts_by_category() |
GET /v2/tenders/counts/category |
counts_by_organization() |
GET /v2/tenders/counts/organization |
counts_by_status() |
GET /v2/tenders/counts/status |
contracts(id) |
GET /v2/tenders/{id}/contracts |
milestones(id) |
GET /v2/tenders/{id}/milestones |
bidders(id) |
GET /v2/tenders/{id}/bidders |
submission_requirements(id) |
GET /v2/tenders/{id}/submission-requirements |
documents(id) |
GET /v2/tenders/{id}/documents |
awards(id) |
GET /v2/tenders/{id}/awards |
timeline(id) |
GET /v2/tenders/{id}/timeline |
analysis(id) |
GET /v2/tenders/{id}/analysis |
value_estimate(id) |
GET /v2/tenders/{id}/value-estimate |
seo(id) |
GET /v2/tenders/{id}/seo |
slug(id) |
GET /v2/tenders/{id}/slug |
related(id, params?) |
GET /v2/tenders/{id}/related |
paginated(params?) |
(async iterator) |
| Method | Endpoint |
|---|---|
list(params?) |
GET /v2/awards |
get(id) |
GET /v2/awards/{id} |
by_tender(tenderId, params?) |
GET /v2/awards/by-tender/{tenderId} |
by_supplier(name, params?) |
GET /v2/awards/by-supplier/{name} |
by_supplier_party(partyId, params?) |
GET /v2/awards/by-supplier-party/{partyId} |
by_date_range(params) |
GET /v2/awards/by-date-range |
analytics(params) |
GET /v2/awards/analytics |
analytics_by_province(params?) |
GET /v2/awards/analytics/province |
analytics_by_category(params?) |
GET /v2/awards/analytics/category |
analytics_by_bee_level(params?) |
GET /v2/awards/analytics/bee-level |
analytics_by_enterprise_type(params?) |
GET /v2/awards/analytics/enterprise-type |
subcontractors(id, params?) |
GET /v2/awards/{id}/subcontractors |
| Method | Endpoint |
|---|---|
list(params?) |
GET /v2/companies |
get(name) |
GET /v2/companies/{name} |
search(params) |
GET /v2/companies/search |
top(params?) |
GET /v2/companies/top |
by_registration(reg) |
GET /v2/companies/by-registration/{reg} |
awards(name, params?) |
GET /v2/companies/{name}/awards |
contracts(name, params?) |
GET /v2/companies/{name}/contracts |
tenders(name, params?) |
GET /v2/companies/{name}/tenders |
directors(name) |
GET /v2/companies/{name}/directors |
| Method | Endpoint |
|---|---|
list(params?) |
GET /v2/organizations |
get(id) |
GET /v2/organizations/{id} |
search(params) |
GET /v2/organizations/search |
by_slug(slug) |
GET /v2/organizations/by-slug/{slug} |
by_registration(reg) |
GET /v2/organizations/by-registration/{reg} |
counts_by_type() |
GET /v2/organizations/counts-by-type |
tenders(id, params?) |
GET /v2/organizations/{id}/tenders |
directors(id, params?) |
GET /v2/organizations/{id}/directors |
| Method | Endpoint |
|---|---|
list(params?) |
GET /v2/directors |
get(id) |
GET /v2/directors/{id} |
search(params) |
GET /v2/directors/search |
by_organization(orgId, params?) |
GET /v2/directors/by-organization/{orgId} |
| Method | Endpoint |
|---|---|
list() |
GET /v2/categories |
get(id) |
GET /v2/categories/{id} |
by_slug(slug) |
GET /v2/categories/by-slug/{slug} |
| Method | Endpoint |
|---|---|
list() |
GET /v2/provinces |
get(slug) |
GET /v2/provinces/{slug} |
health_scores(slug) |
GET /v2/provinces/{slug}/health-scores |
| Method | Endpoint |
|---|---|
category(slug) |
GET /v2/seo/category/{slug} |
province(slug) |
GET /v2/seo/province/{slug} |
list_articles(params?) |
GET /v2/articles |
get_article(id) |
GET /v2/articles/{id} |
get_author(id) |
GET /v2/authors/{id} |
| Method | Endpoint |
|---|---|
list() |
GET /v2/industry/benchmarks |
get(id) |
GET /v2/industry/benchmarks/{id} |
| Method | Endpoint |
|---|---|
list() |
GET /v2/services |
get(slug) |
GET /v2/services/{slug} |
| Method | Endpoint |
|---|---|
list_parties(params?) |
GET /v2/ocds/parties |
get_party(id) |
GET /v2/ocds/parties/{id} |
| Method | Endpoint |
|---|---|
list_sources() |
GET /v2/intel/sources |
get_source(id) |
GET /v2/intel/sources/{id} |
list_items(params?) |
GET /v2/intel/items |
get_item(id) |
GET /v2/intel/items/{id} |
| Method | Endpoint |
|---|---|
list_restricted_suppliers(params?) |
GET /v2/forensic/restricted-suppliers |
get_restricted_supplier(id) |
GET /v2/forensic/restricted-suppliers/{id} |
match_restricted_supplier(params) |
GET /v2/forensic/restricted-suppliers/match |
check_restricted_supplier(params) |
GET /v2/forensic/restricted-suppliers/check |
| Method | Endpoint |
|---|---|
list_enrichments(params?) |
GET /v2/cipc/enrichments |
get_enrichment(id) |
GET /v2/cipc/enrichments/{id} |
list_directors(params?) |
GET /v2/cipc/directors |
get_director(id) |
GET /v2/cipc/directors/{id} |
| Method | Endpoint |
|---|---|
list(params?) |
GET /v2/newsletters |
get(id) |
GET /v2/newsletters/{id} |
| Method | Endpoint |
|---|---|
get(id) |
GET /v2/documents/{id} |
download_url(id, params?) |
GET /v2/documents/{id}/download-url |
| Method | Endpoint |
|---|---|
status() |
GET /v2/meta/status |
provinces() |
GET /v2/meta/provinces |
categories() |
GET /v2/meta/categories |
usage() |
GET /v2/meta/usage |
industries(params?) |
GET /v2/meta/industries |
- Tenders-SA Platform — Main website
- Developer Portal — API keys, docs, and pricing
- API Documentation — Full API reference
- API Key Management — Create and manage keys
- Pricing — Subscription plans
- GitHub — Source code & issues
- Support — Email support
- Developer Contact — API & SDK feedback
MIT