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

Services: Professional

This Request for Information (RFI) invites qualified vendors, advisors and technology providers to submit information on tools, models, workflows and enablement services required to close these identified gaps and integrate with the existing Enerweb ETRM, phased across Day-1 go-live (Phase 1), controlled expansion (Phase 2, Year 1–2) and a mature state (Phase 3, Year 3+).

Eskom
Gauteng
17 Aug 2026
10d left
Professional, Scientific and Technical Activities

Feasibility Study on Hydrogen-Powered Mining Equipment Conversion using Hydrogen Combustion Technology for Surface and Underground Mining.

South African National Energy Development Institute
Gauteng
02 Sept 2026
26d left
Administrative and Support Activities

VIRTUALIZATION TECHNOLOGY REQUIREMENTS AND CAPABILITIES FOR SABC

South African Broadcasting Corporation Limited
National
28 Aug 2026
21d left
Computer Programming, Consultancy and Related Activities

REQUEST FOR INFORMATION: CLOUD-HOSTED OBSERVABILITY SOLUTION FOR THE ROAD ACCIDENT FUND (RAF) TO SUPPORT ENTERPRISE-WIDE MONITORING, ANALYTICS AND OPERATIONAL INTELLIGENCE ACROSS ITS HYBRID TECHNOLOGY ENVIRONMENT

Road Accident Fund (RAF)
Gauteng
26 Aug 2026
19d left
Services: Professional

APPOINTMENT OF A SERVICE PROVIDER TO ROLL OUT AND IMPLEMENT A FOUNDRY TRAINING AND FURNACE REPAIRS PROJECT AT THE DOWNSTREAM ALUMINIUM CENTRE FOR TECHNOLOGY (DACT) TO IMPROVE METAL BENEFICIATION SKILLS FOR A PERIOD OF SIX MONTHS.

Kwazulu Natal - Economic Development, Tourism and Environ Affairs
KwaZulu-Natal
25 Aug 2026
18d left
Services: Professional

Job grading, remuneration benchmark, and employee engagement services Tender for Technology Innovation Agency

Technology Innovation Agency
National
24 Aug 2026
17d 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