Skip to main content
Technology

Accessing SA Tender Data with the Python SDK

A practical guide to the tendersa-sdk Python package — async/await client, resource methods, paginated iteration, error handling, and rate limit tracking. For data analysts, researchers, and Python developers.

SA Procurement Data in Python: Async, Typed, and Open Source

The tendersa-sdk Python package provides an idiomatic async client for the Tenders-SA Developer API. It uses httpx for asynchronous HTTP, maps every API response to typed Python objects, and supports async iteration through paginated results. It is designed for data analysts, researchers, and Python backend developers who need to integrate SA procurement data into their workflows.

The SDK is open source under the MIT license and published on PyPI. The source code is available at github.com/Tenders-SA/python

.

Why Reach for an SDK Instead of Calling the REST API Directly

Everything this package does could, in principle, be replicated with a few dozen lines wrapped around httpx or requests. The value of the SDK isn't in doing something the REST API can't already do on its own — it's in absorbing the repetitive plumbing that every consumer of that API would otherwise write independently. Without the SDK, you'd be responsible for serialising filter dictionaries into query parameters correctly, deserialising JSON into your own data classes, mapping HTTP status codes to meaningful exceptions, tracking rate limit headers, and writing retry-with-backoff logic for transient failures. For a data analyst or backend developer who just wants to answer a question about procurement data, none of that is work you actually want to be doing.

The typed response objects also make exploratory work considerably faster. In a Jupyter notebook or an interactive Python shell, being able to tab-complete tender.estimated_value or tender.requirements on a typed object is a meaningfully better experience than working with raw nested dictionaries and remembering exact key names and casing across dozens of fields.

Installation

1pip install tendersa-sdk
BASH

The SDK requires Python 3.9+ and httpx 0.27+ (installed automatically as a dependency).

Quick Start

The SDK uses async/await throughout. Here is a minimal example to verify connectivity and pull live data:

1import asyncio
2from tendersa import TendersaClient
3
4async def main():
5    client = TendersaClient(api_key="tsa_prod_your_key")
6
7    # List open tenders
8    tenders = await client.tenders.list({
9        "status": "OPEN",
10        "province": "Western Cape",
11    })
12
13    for t in tenders.data:
14        print(t.title, t.status)
15
16// ... (truncated)
PYTHON

The client also supports async context manager usage, which handles cleanup automatically:

1async with TendersaClient(api_key="tsa_prod_your_key") as client:
2    result = await client.tenders.list({"status": "OPEN"})
PYTHON

Client Configuration

1from tendersa import TendersaClient
2
3client = TendersaClient(
4    api_key="tsa_prod_your_key",
5    base_url="https://api.tenders-sa.org",  # default
6    timeout=30.0,                            # 30 seconds (default)
7    max_retries=3,                           # exponential backoff (default)
8)
PYTHON

Resource Methods

The SDK is organised into five resource classes, mirroring the API structure. Each method maps directly to a REST endpoint.

This grouping — client.tenders, client.awards, client.companies, client.organizations, and client.meta — reflects how integrations are actually built in practice. A researcher studying award patterns across provinces will spend almost all their time in awards and companies, while a script that just needs to confirm the API is healthy before a scheduled job runs only touches meta. Keeping each resource's methods scoped to its own namespace also keeps autocomplete suggestions relevant: typing client.tenders. in a notebook only surfaces tender-related operations, not the full surface area of the entire SDK.

Tenders

1# List with filters
2result = await client.tenders.list({
3    "status": "OPEN",
4    "category": "Construction",
5    "province": "Gauteng",
6    "sort": "-closingDate",
7})
8
9# Get detail
10# Access AI-enriched fields: .summary, .requirements, .estimated_value
11detail = await client.tenders.get("tender_001")
12
13# Sub-resources
14docs = await client.tenders.documents("tender_001")
15awards = await client.tenders.awards("tender_001")
16// ... (truncated)
PYTHON

Awards

1result = await client.awards.list({
2    "province": "Western Cape",
3    "beeLevel": "Level 1",
4    "minAmount": 1_000_000,
5})
6
7award = await client.awards.get("award_001")
8
9analytics = await client.awards.analytics({
10    "groupBy": "province",
11    "from": "2025-01-01",
12    "to": "2025-12-31",
13})
PYTHON

Companies

1# Full company intelligence profile
2company = await client.companies.get("BuildCorp SA")
3
4# Search by name, BEE level, or province
5results = await client.companies.search({
6    "q": "Construction",
7    "beeLevel": "Level 1",
8    "province": "Gauteng",
9})
PYTHON

Organisations (Procurement Bodies)

1org = await client.organizations.get("org_001")
2tenders = await client.organizations.tenders("org_001", {"status": "OPEN"})
PYTHON

Meta

1status = await client.meta.status()
2provinces = await client.meta.provinces()
3categories = await client.meta.categories()
4usage = await client.meta.usage()
PYTHON

Common Integration Patterns

Two patterns come up again and again among Python developers integrating with Tenders-SA: building a pandas DataFrame for analysis, and running a scheduled script that checks for new awards and raises an alert. Both are simple compositions of the resource methods already shown above.

Loading Award Data Into pandas

Because client.awards.list already returns typed objects, converting a page of results into a pandas DataFrame is a matter of extracting the fields you care about into plain dictionaries:

1import asyncio
2import pandas as pd
3from tendersa import TendersaClient
4
5async def awards_dataframe(province: str) -> pd.DataFrame:
6    async with TendersaClient(api_key="tsa_prod_your_key") as client:
7        rows = []
8        async for page in client.awards.paginated({"province": province}):
9            for award in page:
10                rows.append({
11                    "id": award.id,
12                    "province": award.province,
13                    "amount": award.amount,
14                })
15        return pd.DataFrame(rows)
16// ... (truncated)
PYTHON

This pattern scales naturally to sector-wide analysis: swap the province filter for a category filter, or drop the filter entirely and page through the full award history, and the same handful of lines becomes the foundation for a spending trend report.

A Scheduled Award Alert Script

For competitive monitoring, a simple cron-scheduled script can check for newly published awards involving a specific supplier or category and log or notify on a match, reusing the same list method with a narrower filter:

1import asyncio
2from tendersa import TendersaClient
3
4async def check_new_awards(min_amount: int):
5    async with TendersaClient(api_key="tsa_prod_your_key") as client:
6        result = await client.awards.list({
7            "province": "Western Cape",
8            "minAmount": min_amount,
9        })
10        for award in result.data:
11            print(f"New award over R{min_amount:,}: {award.id}")
12
13asyncio.run(check_new_awards(5_000_000))
PYTHON

Wired up to a scheduler such as cron or a serverless function trigger, a script like this turns the SDK into the backbone of a lightweight competitive-monitoring tool, without any custom infrastructure beyond whatever notification channel you choose to log or forward matches to.

Pagination

The Python SDK supports idiomatic async iteration through paginated results. Use the paginated() method on list resources:

1# Iterate through all pages of open tenders
2async for page in client.tenders.paginated({
3    "status": "OPEN",
4    "category": "Construction",
5}):
6    for tender in page:
7        print(tender.title, tender.closing_date)
8
9# Control max pages
10async for page in client.awards.paginated(
11    {"province": "Gauteng"},
12    max_pages=5,
13):
14    print(f"Page {page.page}: {len(page)} items")
15    print(f"  Total: {page.total_count}, Has next: {page.has_next}")
PYTHON

Each page is a PaginatedResponse object with convenience properties: .page, .page_size, .total_count, .total_pages, .has_next, and .has_prev.

These properties are particularly useful for progress reporting in long-running scripts. A batch job exporting several thousand award records can print f"page.page of {page.total_pages}" after each page completes, giving whoever is watching the job's logs a concrete sense of how much work remains, rather than a script that appears to hang silently until it finishes.

Error Handling

The SDK raises typed exceptions for every HTTP status code the API can return. Catch specific exceptions for targeted handling:

1from tendersa.errors import (
2    TendersaError,
3    AuthError,
4    NotFoundError,
5    RateLimitError,
6    BadRequestError,
7    ForbiddenError,
8    ConflictError,
9)
10
11try:
12    tender = await client.tenders.get("nonexistent")
13except AuthError:
14    print("Invalid API key. Get one at https://tenders-sa.org/developers/api-keys")
15except NotFoundError:
16// ... (truncated)
PYTHON

Catching specific exception classes rather than inspecting a raw status code produces more maintainable scripts, especially ones that run unattended. A scheduled job can be written to retry on RateLimitError, alert a human on AuthError (since that usually means a key has been revoked or misconfigured), and simply log and skip on NotFoundError — three very different responses that would otherwise require manually branching on numeric status codes.

Rate Limit Tracking

1rl = client.last_rate_limit
2if rl:
3    print(f"{rl.remaining}/{rl.limit} requests remaining ({rl.policy})")
PYTHON

This is especially worth checking inside a long export or analysis loop such as the pandas example above. Logging client.last_rate_limit every few hundred iterations lets you catch a job that's approaching its limit and pause or throttle it deliberately, rather than discovering the problem only when a RateLimitError interrupts an otherwise-complete overnight export.

SDK vs. Calling the REST API Directly

If you're weighing whether to install the SDK or write your own thin wrapper around httpx, the trade-offs generally come down to this:

ConcernRaw REST callstendersa-sdk
Typed responsesNone — you parse JSON dicts and hope the shape matchesTyped Python objects with attribute access
PaginationManual loop, tracking page numbers yourselfIdiomatic async iteration via paginated()
RetriesYou write your own backoff logicConfigurable exponential backoff built in
Error handlingInspect status codes and response bodies manuallyTyped exceptions (AuthError, NotFoundError, RateLimitError, and more)
Rate limit visibilityRead headers manually on every responseclient.last_rate_limit updated automatically

As with the REST API itself, none of this is a limitation of the underlying service — it's the same endpoints either way. The SDK simply takes on the maintenance burden of the client-side plumbing so your analysis or integration code can stay focused on the actual question you're trying to answer.

Walkthrough: From Notebook Exploration to a Scheduled Job

A common trajectory for teams adopting the SDK starts in a Jupyter notebook. An analyst installs tendersa-sdk, instantiates a client with an API key, and runs a handful of exploratory calls against client.tenders.list and client.awards.list with different filter combinations to get a feel for what data is available for a given province or category. Because the response objects are typed, this exploration phase is fast — there is no need to first print a raw JSON blob and manually inspect its keys before knowing what attribute to reference next; editor and notebook autocomplete surfaces the available fields directly.

Once the analyst has confirmed the shape of the data they need, the natural next step is usually to wrap the exploratory calls into a small function, as in the awards_dataframe example above, and run it against a broader set of filters to produce a full dataset for a report. From there, teams that want the analysis to stay current typically promote the same function into a scheduled script — the award alert pattern shown earlier is the same idea applied to monitoring rather than one-off reporting. The point is that nothing about the underlying code changes meaningfully between these three stages: the same typed client, the same resource methods, and the same pagination and error-handling patterns carry through from a one-off notebook cell to a production cron job, which is a large part of why the SDK is worth adopting even for what starts out as a quick, informal analysis.

Choosing Between the Python SDK and Direct REST Calls in Practice

Not every integration needs the SDK. If you are calling the API once from a shell script with curl to check a single tender's status, reaching for a full Python dependency is unnecessary overhead. The SDK earns its keep specifically in scenarios involving repeated calls, pagination, or any amount of unattended operation — anywhere that typed error handling, automatic retry, and rate limit visibility save you from writing and maintaining that logic yourself. As a rule of thumb: a single ad-hoc lookup is fine as a raw REST call; anything that runs more than once, runs unattended, or iterates over more than a page of results is a good candidate for the SDK.

It is also worth noting that adopting the SDK does not lock you out of the REST API. Because the SDK is a thin, open-source wrapper around the same endpoints documented in the API reference, you can always drop down to inspecting the underlying HTTP request and response — useful when debugging an unexpected result — without abandoning the SDK for the rest of your codebase.

Use Cases in Python

The Python SDK is particularly useful for data analysis and automation workflows:

  • Market research: Pull tender data into pandas DataFrames for sector analysis. Iterate through all pages of awards and export to CSV for offline analysis.
  • Competitive monitoring: Script periodic scans that check for new awards in your sector and send alerts when specific suppliers win contracts.
  • Compliance tracking: Monitor procurement opportunities in specific categories or regions. Use the analytics endpoint to track spending trends over time.
  • Data enrichment: Combine Tenders-SA data with other datasets (e.g. company registries, geographic data) for enriched analysis.

The full SDK source code is on GitHub at github.com/Tenders-SA/python

. The package is published to PyPI as tendersa-sdk
. The complete API reference is available at tenders-sa.org/developers/docs
.

The repository includes the full Python source code with type annotations, comprehensive test coverage, async context manager support, and a README with examples for every endpoint.

Tags

PythonSDKPyPIAsyncData AnalysisAPI ClientOpen SourceDeveloper Tools
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
37d 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
9d 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
15d 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
11d 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
8d 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
4d 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

Accessing SA Tender Data with the Python SDK

A practical guide to the tendersa-sdk Python package — async/await client, resource methods, paginated iteration, error handling, and rate limit tracking. For data analysts, researchers, and Python developers.

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