Architecture of the Tenders-SA Developer API: Cloudflare Workers, D1, and 80+ Endpoints
How we built the v2 Developer API on Cloudflare Workers with a dedicated D1 database: 80+ REST endpoints across 17 resource groups, cursor-based pagination, SHA-256 API key authentication, and a two-phase sync engine that backfills historical data before switching to incremental updates every 5 minutes.
Architecture of the Tenders-SA Developer API
The Tenders-SA Developer API v2 runs on a dedicated Cloudflare Worker at api.tenders-sa.org. It exposes 80+ endpoints across 17 resource groups — tenders, awards, organisations, suppliers, directors, categories, provinces, SEO content, industry benchmarks, services, OCDS parties, procurement intelligence, restricted supplier forensics, CIPC company data, newsletter editions, and document metadata. Every endpoint returns structured JSON with a consistent envelope, cursor-based pagination, and per-key rate limiting.
This is not a thin proxy over the main application database. The API runs against its own dedicated D1 database, synced from the main platform every five minutes. This article explains why we chose this architecture and how the sync engine works.
The people who reach for this API tend to fall into a handful of groups. There are South African SMMEs building their own internal tender-tracking tools who want raw data rather than a dashboard. There are industry associations that want to publish a members-only feed of opportunities in their sector. There are academic researchers studying procurement patterns who need structured award data rather than scraped PDFs. And there are software vendors — accounting packages, project management tools, compliance platforms — who want to surface relevant tenders inside their own product without becoming a procurement data company themselves. All of them share the same underlying need: predictable, versioned, machine-readable access to the same enriched dataset that powers the Tenders-SA website, without having to build their own scraping and enrichment pipeline from scratch.
Why a Separate Database?
The decision to run a separate database for the API was driven by three concerns: isolation (API traffic should never compete with the user-facing website for PostgreSQL connections), global performance (Cloudflare Workers serve from 300+ edge locations, and D1's edge replication means sub-50ms reads from anywhere), and cost (D1 is significantly cheaper than running reads against the primary PostgreSQL database under API traffic loads).
The tradeoff is data freshness. The API is never more than 5 minutes behind the main application — acceptable for almost all procurement use cases. For users who need real-time data, we expose direct webhook integrations.
This tradeoff is worth dwelling on because it shapes how integrators should think about the data they receive. If you are building a dashboard that refreshes once an hour, a 5-minute lag is invisible. If you are building an automated bidding tool that reacts the instant a new tender opens, you need to design around the fact that the API is a reflection of the platform, not the platform itself — which is precisely why webhook integrations exist as a separate, event-driven channel for time-sensitive use cases like new tender alerts or award notifications.
The Sync Engine: Two-Phase Architecture
The sync engine runs inside the Cloudflare Worker's scheduled handler (cron trigger) and manages a sync_meta table that tracks per-entity sync progress:
1const ENTITY_TABLE_MAP = { 2 'tenders': 'api_v2_tenders', 3 'awards': 'api_v2_awards', 4 'organizations': 'api_v2_organizations', 5 'tender-documents': 'api_v2_tender_documents', 6 'tender-seo': 'api_v2_tender_seo', 7 'tender-analyses': 'api_v2_tender_analyses', 8 'companies': 'api_v2_companies', 9 'directors': 'api_v2_directors', 10 'cipc-enrichments': 'api_v2_cipc_enrichments', 11 'intel-items': 'api_v2_intel_items', 12 // ... 36 entity types total 13}; 14 15export async function syncEntity(entityName, env, options = {}) { 16// ... (truncated)JAVASCRIPT
The sync engine distinguishes between backfill mode (sync everything from scratch, oldest first, one entity per cron run with batch size 1000) and incremental mode (sync only recent changes, round-robin across entities with batch size 100). During initial deployment, backfill ran for ~4 hours to transfer all 47,000+ tenders, 55,000+ awards, and 8,000+ tender documents. The system then automatically switched to incremental mode.
Route Architecture
The Worker uses a pure router pattern with no framework:
1export default { 2 async fetch(request, env, ctx) { 3 const url = new URL(request.url); 4 const method = request.method; 5 6 // Public meta endpoints (no auth required) 7 if (path === '/v2/meta/health') return handleHealth(env); 8 if (path === '/v2/meta/status') return handleStatus(env); 9 10 // Sync endpoints (shared-secret auth) 11 if (path.startsWith('/api/v2/sync/')) { 12 return requireSyncAuth(request, env, () => handleSync(request, env, ctx)); 13 } 14 15 // Authenticated resource endpoints 16// ... (truncated)JAVASCRIPT
Authentication and Rate Limiting
API keys are hashed with SHA-256 before storage — we never store plaintext keys. Keys are validated on every request against the api_v2_keys D1 table. Rate limiting is enforced per key using two windows: daily limits and monthly limits (configurable per key). We track calls_today, calls_this_month, and last_call_date on each key record. When a limit is exceeded, the response includes a Retry-After header with the reset time.
Cursor-Based Pagination
The API uses cursor-based pagination instead of page-based. Each list response includes a nextCursor field that clients pass as a query parameter to fetch the next page. The cursor is an opaque base64-encoded value containing the last seen record's sort key. This approach is more reliable than page-based pagination for datasets with high insertion rates (like the tenders table, where new records arrive every few minutes).
1// Example: GET /v2/tenders?cursor=eyJsYXN0SWQiOiAxMjM0fQ==&limit=50 2// Response: 3{ 4 "data": [ /* 50 tender records */ ], 5 "nextCursor": "eyJsYXN0SWQiOiAxMjg0fQ==", 6 "hasMore": true 7}JAVASCRIPT
The SDK Ecosystem
On top of the REST API, we publish SDKs for JavaScript and Python. Both SDKs handle authentication, pagination (automatically following cursors), rate-limit backoff, and error handling. The JavaScript SDK is distributed as an npm package; the Python SDK via PyPI. SDK documentation includes typed interfaces, usage examples, and integration patterns for common workflows like syncing tenders to a CRM or batch-analyzing awards.
D1 Schema Management
The D1 database has 35 tables and 660 columns, managed through SQL migration files. We have a verification script (scripts/verify-d1-schema.js) that validates the live schema matches the expected migration state. Schema changes are additive where possible — we add columns rather than altering them, to avoid downtime during the 5-minute sync window.
Practical Advice for Integrators
Building a reliable integration on top of any external API comes down to handling the failure modes gracefully rather than assuming the happy path. On the developer API, that means always checking the rate-limit headers before you hit the ceiling rather than after, so your application can slow down proactively instead of reacting to a wave of 429 responses. It means following the nextCursor field exactly as returned rather than trying to reconstruct pagination state yourself, since the cursor encodes the sort position of the last record you saw and is designed to remain stable even as new tenders are inserted ahead of it. And it means treating the API's 5-minute sync window as a design constraint from day one: if your integration needs to show "time since last update," surface that honestly rather than implying real-time freshness you cannot guarantee.
For teams just getting started, the fastest path is usually to prototype against a handful of endpoints using the JavaScript or Python SDK, confirm the shape of the data you need across the resource groups relevant to your use case, and only then decide whether you need the full breadth of the 80+ endpoints or just a narrow slice — most integrations, in our experience, end up using tenders, awards, and companies almost exclusively, with the remaining resource groups serving more specialised research and compliance use cases.
Why This Architecture Will Age Well
Separating the API's data plane from the main application was as much a bet on the future as a solution to today's traffic patterns. As more integrators build on top of the platform — CRM connectors, JV partner-matching tools, third-party dashboards — the load profile on the API becomes harder to predict. A dedicated D1 database means that a spike in API usage from one integrator cannot degrade page-load times for someone browsing tenders on the website, and a spike in website traffic cannot slow down a scheduled sync job pulling award data into a partner's CRM. Keeping these concerns physically separate, even at the cost of a small and well-understood freshness lag, is what makes it possible to keep expanding the API surface without re-architecting every time a new class of consumer shows up.
There is also a discipline benefit to running the API against its own schema rather than reusing the main application's tables directly. Because the D1 tables are populated by a controlled sync process rather than shared application code, we can evolve the internal data model of the main platform — renaming fields, restructuring relationships, changing how a value is derived — without that change silently breaking every API consumer overnight. The sync layer becomes the seam where internal refactors are translated into a stable, external-facing contract, which is ultimately what lets us promise developers that the shape of a tender or award record will not shift under them without notice.
Tags
Based on this article's topics, here are some current tenders that might interest you
PROCUREMENT OF AN E-RECRUITMENT DIGITAL SERVICES PLATFORM WITH INTERGRATED CR...
Digital Trade Corridor platform (as defined by WCO) and third-party pre-arriv...
APPOINTMENT OF A PANEL OF FIVE SERVICE PROVIDERS TO RENDER VARIOUS TRAINING SERVICES FOR A PERIOD OF 36 MONTHS Tenders are hereby invited from capable and experienced service providers for the above tender. 1. Only tenderers who have provided the following mandatory information and documents to be used to evaluate the bidder's responsiveness will be considered for further evaluation on functionality: 1.1 Only service providers that are registered on the Central Supplier Database will be considered for the awarding of this request for quotations and a copy of CSD report not later than three months should be attached. 1.2 Price quoted must be firm, VAT and other taxes inclusive, valid and fixed for duration of the contract. 1.3 No tenders shall be considered from a person/s who are in the service of the state. 1.4 Service providers are required to fully complete the attached MBD forms and submit together with their written tenders. 1.5 Attach a bank account confirmation letter with bank stamp not older than three months accompanied with an affidavit confirming the business bank account details - if the banking details are not verified on the CSD report. 1.6 Attach certified copy of identity documents (ID) of company directors. 1.7 Attach certified copy of the company registration certificate issued by the Companies and Intellectual Property Commission (CIPC). 1.8 Valid SARS pin certificate must be attached. 1.9 Attach verifiable Municipal Account/s not older than three months for both the tenderer and entity owner/s or director/s. In areas where the municipalities are not issuing municipal accounts, attach valid lease agreements or confirmation of residence or address for both the tenderer and entity owner/s or director/s issued by a relevant authority not older than three months. 1.10 Attach a certified copy for valid proof of registration with relevant SETA’s or Quality Council for Trades and Occupations (QCTO) accreditation. 1.11 Attach set of reviewed and signed Annual Financial Statements for the past three years or since the company came into existence if not older than three years or if so, required by law to prepare the Annual Financial Statements for auditing, attach set of audited annual financial statements for the past three years. 1.12 Bidders are advised not to commit any fraudulent activities, including forgery of documents. All abuses of the Supply Chain Management (SCM) systems including but not limited to forgery of returnable documents, may be reported to the South African Police Service (SAPS) and restricted from doing business with any public institution or organ of the state for a period not exceeding 10 years in line with the Prevention of Fraud and Corrupt Activities Act 12 of 2004. 1.13 Joint Venture or Consortium Agreement if applicable. 1.14 All tender documents must be duly signed and submitted on the PDF document that has been issued. All the certified documents as stated must not be older than three months. 2. The tender will be evaluated on the 80/20 preference point system in terms of the Preferential Procurement Policy of the Ehlanzeni District Municipality. The policy preference point system will be applied as follows: 2.1 The 80 points will be for price; and 2.2 The 20 points will be allocated for the specific goals on a proportional or pro rata basis as follows:- POINTS FOR CONTRACTING AN ENTERPRISE OWNED BY HISTORICALLY DISADVANTAGED PERSONS OR INDIVIDUALS HISTORICALLY DISADVANTAGED PERSONS OR INDIVIDUALS POINTS ALLOCATION SOURCE DOCUMENTS REQUIRED TO CLAIM POINTS 100% black person or people owned Enterprise 3,00 A copy of a Full CSD report not older than 3 months More than 30% woman or women shareholding or owned enterprise 2,00 More than 30% youth shareholding or owned enterprise 2,00 More than 30% people living with disability shareholding or owned Enterprise 2,00 A copy of a Medical Certificate to confirm disability or stated on the CSD report More than 30% military veteran’s shareholding or owned Enterprise 2,00 A copy of a Full CSD report not older than 3 months POINTS FOR IMPLEMENTING OF RDP PROGRAMMES Enterprises regarded as *EME’s located within the Ehlanzeni District Municipality area of jurisdiction 2,00 ? A copy of a Full CSD report not older than 3 months NB: Points will only be awarded if the CSD physical address is the same as the address for the proof of residence required in 1.7 above. Sub-contract minimum of 30% of the contract value to EME’s in the ward or local communities where the services to be rendered or works to be undertaken 2,00 ? Commitment letter of works or services to be sub-contracted. Corporate Social Investment (CSI) or Social Labour Plan proposition 3,00 ? Attach a CSI plan or Social Labour Plan B-BBEE level 1 contribution 2,00 ? Certified Valid SANAS Accredited BBBEE certificate; or ? Certified Valid EME and SME a sworn affidavit; or ? Certified Valid CIPC issued certificate confirming annual turnover and level of Black Ownership. TOTAL PREFERENCE POINTS TO CLAIMED 20,00 *EME’s are Exempted Micro Enterprise with an annual turnover of R10 million or less. Received bids will be evaluated for responsiveness based on mandatory requirements, functionality and bidders who obtain a minimum of 70 points out of a possible 100 points for further evaluation on the preference point scoring system. Bid documents can be viewed and downloaded at no cost on the Document Sharing and Collaboration Platform or Portal (NEPTUNE): http://edmservices.ehlanzeni.gov.za and National Treasury Portal from Monday, 03 August 2026. Further information regarding the downloading and uploading of documents will be explained at the compulsory briefing session. A compulsory briefing session will be held on Tuesday, 11 August 2026, 10h00 at Ehlanzeni District Municipality Office Complex, DMC, 8 Van Niekerk Street, Sonheuwel Central, Mbombela, 1201. Where bids should be submitted - Completed bid and other returnable documents must be submitted only in PDF format on the Document Sharing and Collaboration Platform or Portal: http://edmservices.ehlanzeni.gov.za on or before Monday, 31 August 2026 not later than 12h00. Enquiries: Contact Person - ADMINISTRATION: Mr. P Khumalo at 013 759 8-573 or [email protected] Contact Person – TECHNICAL: Mrs. C de Lange at 013 759 8500 or [email protected] Visit our website: www.ehlanzeni.gov.za for tender information. Employer: Acting Municipal Manager: Ms. S S Madlopha Ehlanzeni District Municipality P O Box 333,M MBOMBELA 1200
REQUEST FOR PRICE QUOTATIONS FOR PROCUREMENT AND DELIVERY OF ANNUAL VEEAM DATA PLATFORM ADVANCED UNIVERSAL LICENSES FOR THE NATIONAL LOTTERIES COMMISSION.
Want to see all available tenders?
Browse All Tenders →Share this article
Architecture of the Tenders-SA Developer API: Cloudflare Workers, D1, and 80+ Endpoints
How we built the v2 Developer API on Cloudflare Workers with a dedicated D1 database: 80+ REST endpoints across 17 resource groups, cursor-based pagination, SHA-256 API key authentication, and a two-phase sync engine that backfills historical data before switching to incremental updates every 5 minutes.