Official TypeScript/JavaScript 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
- Document Management — Centralised document vault with CIPC, tax clearance, and BBBEE certificates
- Tender Toolkit — BBBEE Calculator, Readiness Assessment, Market Heatmap, AI Proposal Generator
The Tenders-SA Developer API exposes this 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"
}
}
}Error:
{
"success": false,
"error": "Not found",
"code": "NOT_FOUND",
"message": "The requested resource was not found",
"requestId": "req_xxx",
"docs": "https://tenders-sa.org/developers/docs/errors#NOT_FOUND",
"timestamp": "2026-01-01T00:00:00Z"
}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 |
Reduce response payload size by specifying only the fields you need:
GET /v1/tenders?fields=tenderId,title,status,closingDate
npm install @tenders-sa-org/sdk-js- Node.js 18+ (native
fetch) - TypeScript 5+ (optional, for type safety)
import { TendersaClient } from '@tenders-sa-org/sdk-js'
const client = new TendersaClient({ apiKey: 'tsa_prod_your_key' })
// List open tenders in Western Cape
const tenders = await client.tenders.list({
status: 'OPEN',
province: 'Western Cape',
})
for (const tender of tenders.data) {
console.log(`${tender.title} — ${tender.closingDate}`)
}
// Get tender detail
const detail = await client.tenders.get('tender_001')
console.log(detail.data.aiSummary)
// AI-powered search
const results = await client.tenders.search({ q: 'road construction' })import { TendersaClient } from '@tenders-sa-org/sdk-js'
const client = new TendersaClient({
apiKey: 'tsa_prod_your_key',
// Optional:
baseUrl: 'https://api.tenders-sa.org', // default
timeout: 30_000, // 30s (default)
retry: { maxRetries: 3 }, // exponential backoff
})The SDK is organised into five resource classes, each mirroring a section of the API.
const { data } = await client.tenders.list({
status: 'OPEN',
category: 'Construction',
province: 'Gauteng',
sort: '-closingDate',
})
const detail = await client.tenders.get('tender_001')
const docs = await client.tenders.documents('tender_001')
const awards = await client.tenders.awards('tender_001')
const timeline = await client.tenders.timeline('tender_001')
const analysis = await client.tenders.analysis('tender_001')
const estimate = await client.tenders.valueEstimate('tender_001')
const search = await client.tenders.search({ q: 'road construction' })const { data } = await client.awards.list({
province: 'Western Cape',
beeLevel: 'Level 1',
minAmount: 1_000_000,
})
const award = await client.awards.get('award_001')
const analytics = await client.awards.analytics({
groupBy: 'province',
from: '2025-01-01',
to: '2025-12-31',
})const company = await client.companies.get('BuildCorp SA')
const results = await client.companies.search({
q: 'Construction',
beeLevel: 'Level 1',
province: 'Gauteng',
})const org = await client.organizations.get('org_001')
const tenders = await client.organizations.tenders('org_001', {
status: 'OPEN',
})const status = await client.meta.status()
const provinces = await client.meta.provinces()
const categories = await client.meta.categories()
const usage = await client.meta.usage()List endpoints support cursor-like pagination via PaginatedAsyncIterator:
const paginator = client.tenders.listPages({
status: 'OPEN',
category: 'Construction',
})
for await (const page of paginator.pages()) {
for (const tender of page) {
console.log(tender.title)
}
}The paginator yields arrays of items per page. You can control page size and max pages:
const paginator = client.awards.listPages({ ... }, {
pageSize: 50, // items per page (default: 100)
maxPages: 10, // stop after 10 pages
})The SDK throws typed errors for every API response status:
import {
TendersaError,
AuthError,
NotFoundError,
RateLimitError,
BadRequestError,
ForbiddenError,
ConflictError,
InternalError,
ServiceUnavailableError,
} from '@tenders-sa-org/sdk-js'
try {
const result = await client.tenders.get('nonexistent')
} catch (err) {
if (err instanceof AuthError) {
console.error('Invalid API key. Get one at https://tenders-sa.org/developers/api-keys')
} else if (err instanceof NotFoundError) {
console.error('Tender not found')
} else if (err instanceof RateLimitError) {
console.error(`Rate limited: ${err.used}/${err.limit}. Resets at ${err.resetsAt}`)
} else if (err instanceof TendersaError) {
console.error(`API error [${err.code}]: ${err.message}`)
}
}Every error exposes:
status— HTTP status codecode— Machine-readable error codemessage— Human-readable descriptionrequestId— For tracing with supportdocs— Link to error documentation
The client stores the most recent rate limit snapshot:
console.log(client.lastRateLimit)
// { limit: 500, remaining: 498, reset: '2026-01-02T00:00:00Z', policy: 'daily' }Pass the fields parameter to any list method to reduce payload:
const { data } = await client.tenders.list({
fields: 'tenderId,title,status,closingDate',
})The SDK retries on transient failures with exponential backoff:
const client = new TendersaClient({
apiKey: 'tsa_prod_your_key',
retry: {
maxRetries: 5, // default: 3
baseDelayMs: 200, // default: 1000
maxDelayMs: 10_000, // default: 30_000
},
})| 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 |
|
valueEstimate(id) |
GET /v1/tenders/{id}/value-estimate |
|
listPages(params?) |
(paginated iterator) | |
client.awards |
list(params?) |
GET /v1/awards |
get(id) |
GET /v1/awards/{id} |
|
analytics(params?) |
GET /v1/awards/analytics |
|
listPages(params?) |
(paginated iterator) | |
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