Skip to main content
Technology

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-js
BASH

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:

ConcernRaw REST calls@tenders-sa-org/sdk-js
Type safetyNone — you parse JSON and hope the shape matchesFull TypeScript types for every request and response
PaginationManual loop, tracking cursors or page numbers yourselfBuilt-in async iterator via listPages()
RetriesYou write your own backoff logicConfigurable exponential backoff out of the box
Error handlingInspect status codes and response bodies manuallyTyped error classes (AuthError, NotFoundError, RateLimitError)
Rate limit visibilityRead headers manually on every responseclient.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.

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

JavaScriptTypeScriptSDKnpmAPI ClientDeveloper ToolsOpen Source
Relevant Tender Opportunities

Based on this article's topics, here are some current tenders that might interest you

Civil Engineering

REPLACEMENT OF EXISTING PIPE SYSTEMS EMPLOYING SPECIALISED TRENCHLESS CONSTRUCTION TECHNOLOGY IN THE DRAKENSTEIN MUNICIPAL AREA FOR A PERIOD UP TO 30 JUNE 2029

Drakenstein Municipality
Western Cape
29 Oct 2026
36d left
Other Professional, Scientific and Technical Activities

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

Council for Scientific and Industrial Research (CSIR)
Gauteng
30 Sept 2026
8d left
Supplies: Medical

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.

Eastern Cape - Health
Eastern Cape
06 Oct 2026
13d left
Services: Professional

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

Mpumalanga - Economic Development Environment and Tourism
Mpumalanga
02 Oct 2026
10d left
Administrative and Support Activities

ENTERPRISE CONTENT MANAGEMENT (ECM) STRATEGY, GOVERNANCE FRAMEWORK, TARGET ARCHITECTURE, TECHNOLOGY ROADMAP AND NARSSA-COMPLIANT ORGANISATIONAL FILE PLAN FOR A DURATION OF SIX(6) MONTHS

Passenger Rail Agency of South Africa (PRASA)
Gauteng
30 Sept 2026
7d left
Services: Professional

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

Science & Technology
Gauteng
25 Sept 2026
2d left

Want to see all available tenders?

Browse All Tenders →
AI-Powered Matching
Never Miss a Perfect Tender Again
Our AI analyzes thousands of tenders and finds the ones YOUR company can actually win
AI Match Scoring for every tender
Instant alerts for 85%+ matches
B-BBEE level optimization
Document readiness checks

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.

https://www.tenders-sa.org/blog/tendersa-javascript-sdk-guide