Using the Tenders-SA JavaScript SDK to Access SA Procurement Data
A practical guide to the @tenders-sa-org/sdk-js npm package — installation, client setup, resource methods, pagination iterators, error handling, retry configuration, and sparse fields.
Building with SA Procurement Data in JavaScript and TypeScript
The @tenders-sa-org/sdk-js package provides a typed, idiomatic TypeScript client for the Tenders-SA Developer API. It handles authentication, pagination, error mapping, rate limit tracking, and retry with exponential backoff — so you can focus on integrating procurement data into your application rather than building HTTP client infrastructure.
The SDK is open source under the MIT license and published on npm. The source code is available at github.com/Tenders-SA/js.
Why Reach for an SDK Instead of Calling the REST API Directly
Every capability in this SDK is ultimately a thin wrapper around the same public REST endpoints you could call yourself with fetch. So why bother with a package at all? In practice, the value isn't in doing anything the REST API can't already do — it's in removing the repetitive plumbing that every consumer of that API would otherwise have to write themselves. Without the SDK, you'd be responsible for constructing query strings correctly, parsing response envelopes, mapping HTTP status codes to meaningful error handling, tracking rate limit headers, and writing your own retry-with-backoff logic for transient failures. Multiply that across every project or script that talks to the API, and you end up reimplementing the same boilerplate again and again, with subtly different bugs each time.
The SDK also buys you compile-time safety that a raw HTTP client cannot offer. Because every resource method, filter object, and response shape is fully typed, your editor can autocomplete field names on a tender or award object, and TypeScript will flag a typo in a filter key (like passing provnce instead of province) before your code ever runs, rather than after a confusing empty response in production. For a small team building an internal dashboard or a solo developer scripting a weekend analysis, that immediate feedback loop is often worth more than the marginal convenience of skipping an npm install.
Installation
1npm install @tenders-sa-org/sdk-jsBASH
The SDK requires Node.js 18+ (for native fetch support) and works with TypeScript 5+ for full type safety. No additional HTTP client libraries are required.
Quick Start
Create a client with your API key and start querying data within seconds:
1import { TendersaClient } from '@tenders-sa-org/sdk-js' 2 3const client = new TendersaClient({ apiKey: 'tsa_prod_your_key' }) 4 5// List open tenders in the Western Cape 6const tenders = await client.tenders.list({ 7 status: 'OPEN', 8 province: 'Western Cape', 9}) 10 11for (const tender of tenders.data) { 12 console.log(`${tender.title} — ${tender.closingDate}`) 13} 14 15// Get a single tender's AI analysis 16// ... (truncated)TYPESCRIPT
Client Configuration
The TendersaClient accepts several configuration options beyond the API key:
1const client = new TendersaClient({ 2 apiKey: 'tsa_prod_your_key', 3 baseUrl: 'https://api.tenders-sa.org', // default 4 timeout: 30_000, // 30 seconds (default) 5 retry: { maxRetries: 3 }, // exponential backoff 6})TYPESCRIPT
Resource Methods
The SDK is organised into five resource classes, each mapped to a section of the API.
Splitting the client into client.tenders, client.awards, client.companies, client.organizations, and client.meta mirrors the way most integrations actually get built: a developer building a bidder-facing dashboard leans heavily on tenders, someone building a competitive-intelligence tool for procurement analysts leans on awards and companies, and a lightweight status widget only ever needs meta. Rather than exposing one flat, sprawling client object, grouping methods by resource keeps each class's surface area small enough to hold in your head while you're writing an integration, and makes it easier to guess the right method name from the resource you're already thinking about.
Tenders
1// List with filters 2const { data, meta } = await client.tenders.list({ 3 status: 'OPEN', 4 category: 'Construction', 5 province: 'Gauteng', 6 sort: '-closingDate', 7}) 8 9// Get detail with AI analysis 10const tender = await client.tenders.get('tender_001') 11 12// Related resources 13const docs = await client.tenders.documents('tender_001') 14const awards = await client.tenders.awards('tender_001') 15const timeline = await client.tenders.timeline('tender_001') 16// ... (truncated)TYPESCRIPT
Awards
1// List awards with filters 2const { data } = await client.awards.list({ 3 province: 'Western Cape', 4 beeLevel: 'Level 1', 5 minAmount: 1_000_000, 6}) 7 8// Get a single award 9const award = await client.awards.get('award_001') 10 11// Aggregated analytics 12const analytics = await client.awards.analytics({ 13 groupBy: 'province', 14 from: '2025-01-01', 15 to: '2025-12-31', 16// ... (truncated)TYPESCRIPT
Companies
1// Company intelligence profile (by exact name) 2const company = await client.companies.get('BuildCorp SA') 3 4// Search companies 5const results = await client.companies.search({ 6 q: 'Construction', 7 beeLevel: 'Level 1', 8 province: 'Gauteng', 9})TYPESCRIPT
Organisations (Procurement Bodies)
1const org = await client.organizations.get('org_001') 2const tenders = await client.organizations.tenders('org_001', { 3 status: 'OPEN', 4})TYPESCRIPT
Meta
1const status = await client.meta.status() 2const provinces = await client.meta.provinces() 3const categories = await client.meta.categories() 4const usage = await client.meta.usage()TYPESCRIPT
Common Integration Patterns
The resource methods above are simple enough individually, but the SDK becomes genuinely useful once you start composing them into small workflows. Two patterns come up constantly among developers building on top of Tenders-SA data: a live alert dashboard for bidders, and a scheduled export feed into a CRM or spreadsheet.
Building a Tender Alert Dashboard
A common use case is a small internal dashboard that shows a sales or bids team the open tenders closing soonest in the categories they care about. This combines tenders.list with a simple sort and a client-side filter — no new SDK surface required, just recombining what's already there:
1import { TendersaClient } from '@tenders-sa-org/sdk-js' 2 3const client = new TendersaClient({ apiKey: process.env.TENDERSA_API_KEY! }) 4 5async function getUpcomingDeadlines(category: string) { 6 const { data } = await client.tenders.list({ 7 status: 'OPEN', 8 category, 9 sort: 'closingDate', 10 }) 11 12 // Highlight anything closing within the next 7 days 13 const now = Date.now() 14 const soon = data.filter((t) => { 15 const closing = new Date(t.closingDate).getTime() 16// ... (truncated)TYPESCRIPT
A dashboard built this way can run this function on a timer (say, every few minutes on the client, or on a cron job server-side) and re-render the 'closing soon' list, giving a bids team an always-current view without anyone needing to manually refresh a government portal.
Feeding Tender and Award Data Into a CRM
Sales and business development teams often want procurement data inside the tools they already live in — a CRM, a spreadsheet, or a data warehouse — rather than a separate dashboard. Because the SDK's pagination iterator already yields plain arrays of typed objects, exporting a full result set to CSV or forwarding it into a CRM's import API is mostly a matter of shaping the fields you already have:
1import { TendersaClient } from '@tenders-sa-org/sdk-js' 2 3const client = new TendersaClient({ apiKey: process.env.TENDERSA_API_KEY! }) 4 5async function exportAwardsToRows(province: string) { 6 const rows: string[][] = [] 7 const paginator = client.awards.listPages({ province }) 8 9 for await (const page of paginator.pages()) { 10 for (const award of page) { 11 rows.push([award.id, award.province, String(award.amount)]) 12 } 13 } 14 15 return rows // hand these rows to your CSV writer or CRM import client 16// ... (truncated)TYPESCRIPT
Because the pagination iterator already handles page-by-page traversal, this kind of export script stays short and readable even when it needs to walk through thousands of award records — you never have to write your own 'fetch the next page until there isn't one' loop.
Pagination
List endpoints return a single page by default. For traversing multiple pages, the SDK provides a PaginatedAsyncIterator that yields arrays of items per page:
1const paginator = client.tenders.listPages({ 2 status: 'OPEN', 3 category: 'Construction', 4}) 5 6for await (const page of paginator.pages()) { 7 for (const tender of page) { 8 console.log(tender.title) 9 } 10} 11 12// Control page size and max pages 13const awards = client.awards.listPages( 14 { province: 'Gauteng' }, 15 { pageSize: 50, maxPages: 10 } 16// ... (truncated)TYPESCRIPT
The pageSize and maxPages options matter more than they might first appear. Without a maxPages cap, a broad filter on a large dataset like awards could walk through hundreds of pages before your script finishes, which is rarely what you want in an ad-hoc script or a request handler with its own timeout. Setting a sensible cap up front — and choosing a larger pageSize when you know you want the full dataset — is usually simpler than adding your own early-exit logic around an unbounded loop.
Error Handling
The SDK throws typed errors for every API response status. You can catch specific error types to handle different failure modes:
1import { 2 TendersaError, 3 AuthError, 4 NotFoundError, 5 RateLimitError, 6} from '@tenders-sa-org/sdk-js' 7 8try { 9 const result = await client.tenders.get('nonexistent') 10} catch (err) { 11 if (err instanceof AuthError) { 12 console.error('Invalid API key') 13 } else if (err instanceof NotFoundError) { 14 console.error('Tender not found') 15 } else if (err instanceof RateLimitError) { 16// ... (truncated)TYPESCRIPT
Every error exposes status (HTTP status code), code (machine-readable, e.g. NOT_FOUND), message (human-readable), requestId (for tracing with support), and docs (link to error documentation).
Designing your error handling around these typed classes rather than raw HTTP status codes tends to produce more readable application code. A catch block that checks instanceof AuthError reads, at a glance, as 'the API key is wrong' — you don't have to remember that 401 means auth and 404 means not found. This becomes especially valuable in larger codebases where several developers are consuming the same client: everyone reads the same vocabulary of error types instead of re-deriving what each status code means from the API documentation each time.
Rate Limit Tracking
The client stores the most recent rate limit snapshot, accessible via client.lastRateLimit:
1console.log(client.lastRateLimit) 2// { limit: 500, remaining: 498, reset: '2026-01-02T00:00:00Z', policy: 'daily' }TYPESCRIPT
Checking client.lastRateLimit after a batch of requests is a cheap habit that pays off in any script that fires off many calls in a loop, such as the CRM export pattern shown earlier. Logging remaining before and after a large export lets you catch a script that's approaching its limit long before it starts throwing RateLimitError, and lets you decide whether to slow the loop down or spread the work across a longer time window.
Retry Configuration
The SDK retries on transient failures (network errors, 500-series responses) with exponential backoff. Configure retry behaviour through the client constructor:
1const client = new TendersaClient({ 2 apiKey: 'tsa_prod_your_key', 3 retry: { 4 maxRetries: 5, // default: 3 5 baseDelayMs: 200, // default: 1000ms 6 maxDelayMs: 10_000, // default: 30000ms 7 }, 8})TYPESCRIPT
In practice, most integrations never need to touch these defaults — three retries with a capped exponential backoff is a reasonable balance between resilience and not hammering the API during an outage. The exception is background jobs that run unattended overnight, where it can be worth raising maxRetries slightly so a brief network blip in your infrastructure doesn't fail an entire batch job that nobody is watching in real time.
Sparse Fields
Reduce response payload size by specifying only the fields you need. Supported on all list methods:
1const { data } = await client.tenders.list({ 2 fields: 'tenderId,title,status,closingDate', 3})TYPESCRIPT
Sparse fields are most valuable in two scenarios: when you're rendering a compact list view (like the alert dashboard above) that only needs a handful of columns, and when you're pulling large paginated result sets where every unused field in the response adds up across thousands of records. Requesting only tenderId,title,status,closingDate for a dashboard table, for instance, keeps the payload small and the rendering fast, even though the full tender object also carries a lengthy AI-generated summary and extracted requirements you don't need for that view.
SDK vs. Calling the REST API Directly
If you're deciding whether to reach for the SDK or write your own thin wrapper around fetch, it helps to see the trade-offs side by side:
| Concern | Raw REST calls | @tenders-sa-org/sdk-js |
|---|---|---|
| Type safety | None — you parse JSON and hope the shape matches | Full TypeScript types for every request and response |
| Pagination | Manual loop, tracking cursors or page numbers yourself | Built-in async iterator via listPages() |
| Retries | You write your own backoff logic | Configurable exponential backoff out of the box |
| Error handling | Inspect status codes and response bodies manually | Typed error classes (AuthError, NotFoundError, RateLimitError) |
| Rate limit visibility | Read headers manually on every response | client.lastRateLimit updated automatically |
None of this means the REST API itself is somehow inferior — the SDK calls the exact same endpoints under the hood. The choice is really about how much of that supporting infrastructure you want to write and maintain yourself versus getting for free by installing a package.
Links and Resources
The full SDK source code is on GitHub at github.com/Tenders-SA/js. The package is published to npm as @tenders-sa-org/sdk-js. The complete API reference is available at tenders-sa.org/developers/docs.
The repository includes the full TypeScript source with type definitions exported, unit tests, and a comprehensive README with examples for every endpoint. Issues and pull requests are welcome.
Tags
Based on this article's topics, here are some current tenders that might interest you
REPLACEMENT OF EXISTING PIPE SYSTEMS EMPLOYING SPECIALISED TRENCHLESS CONSTRUCTION TECHNOLOGY IN THE DRAKENSTEIN MUNICIPAL AREA FOR A PERIOD UP TO 30 JUNE 2029
Request For Information (RFI) CSIR is requesting information from interested service providers, product suppliers, research organisations and technology developers on: Radio Frequency Electronic Warfare (EW) payloads for unmanned airborne and spaceborne platforms
Request for supply, delivery, commissioning and maintenance of a Dicom dry imaging printer with dry laser printing technology (digital x-ray printers) for Eastern Cape facilities for 36 months.
REQUEST FOR TOWN PLANNING & ENVIRONMENTAL SERVICES AT SABIE EXTENSION 19 (FORESTRY INDUSTRIAL TECHNOLOGY PARK) FOR THE APPROVAL OF THE LAYOUT PLAN AND REQUIRED ENVIRONMENTAL AUTHORISATION IN SABIE, THABA CHWEU LOCAL MUNICIPALITY, MPUMALANGA PROVINCE FOR A PERIOD OF NINE (09) MONTHS
ENTERPRISE CONTENT MANAGEMENT (ECM) STRATEGY, GOVERNANCE FRAMEWORK, TARGET ARCHITECTURE, TECHNOLOGY ROADMAP AND NARSSA-COMPLIANT ORGANISATIONAL FILE PLAN FOR A DURATION OF SIX(6) MONTHS
Appointment of a service provider to supply, installation and commissioning of an acoustic sound insulation system for executive offices in the Department of Science, Technology and Innovation
Want to see all available tenders?
Browse All Tenders →Share this article
Using the Tenders-SA JavaScript SDK to Access SA Procurement Data
A practical guide to the @tenders-sa-org/sdk-js npm package — installation, client setup, resource methods, pagination iterators, error handling, retry configuration, and sparse fields.