Official Python SDK for the Tenders-SA Developer API — 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 exposes enriched procurement data through a 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/v1
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": "v1",
"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", # 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 five resource classes, accessed as properties on the client.
# List with filters
result = await client.tenders.list({
"status": "OPEN",
"category": "Construction",
"province": "Gauteng",
"sort": "-closingDate",
})
# Get detail
detail = await client.tenders.get("tender_001")
# Sub-resources
docs = await client.tenders.documents("tender_001")
awards = await client.tenders.awards("tender_001")
timeline = await client.tenders.timeline("tender_001")
# AI analysis
analysis = await client.tenders.analysis("tender_001")
estimate = await client.tenders.value_estimate("tender_001")
# Search
results = await client.tenders.search({"q": "road construction"})result = await client.awards.list({
"province": "Western Cape",
"beeLevel": "Level 1",
"minAmount": 1_000_000,
})
award = await client.awards.get("award_001")
analytics = await client.awards.analytics({
"groupBy": "province",
"from": "2025-01-01",
"to": "2025-12-31",
})company = await client.companies.get("BuildCorp SA")
results = await client.companies.search({
"q": "Construction",
"beeLevel": "Level 1",
"province": "Gauteng",
})org = await client.organizations.get("org_001")
tenders = await client.organizations.tenders("org_001", {"status": "OPEN"})status = await client.meta.status()
provinces = await client.meta.provinces()
categories = await client.meta.categories()
usage = await client.meta.usage()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})")| Resource | Method | Endpoint |
|---|---|---|
client.tenders |
list(params?) |
GET /v1/tenders |
get(id) |
GET /v1/tenders/{id} |
|
search(params) |
GET /v1/tenders/search |
|
documents(id) |
GET /v1/tenders/{id}/documents |
|
awards(id) |
GET /v1/tenders/{id}/awards |
|
timeline(id) |
GET /v1/tenders/{id}/timeline |
|
analysis(id) |
GET /v1/tenders/{id}/analysis |
|
value_estimate(id) |
GET /v1/tenders/{id}/value-estimate |
|
paginated(params?) |
(async iterator) | |
client.awards |
list(params?) |
GET /v1/awards |
get(id) |
GET /v1/awards/{id} |
|
analytics(params?) |
GET /v1/awards/analytics |
|
client.companies |
get(name) |
GET /v1/companies/{name} |
search(params) |
GET /v1/companies/search |
|
client.organizations |
get(id) |
GET /v1/organizations/{id} |
tenders(id) |
GET /v1/organizations/{id}/tenders |
|
client.meta |
status() |
GET /v1/meta/status |
provinces() |
GET /v1/meta/provinces |
|
categories() |
GET /v1/meta/categories |
|
usage() |
GET /v1/meta/usage |
- 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