Access live tenders from across Africa. Display, filter, and integrate tender data seamlessly into any application.
Access live tender data updated continuously from multiple African countries
Filter tenders by country, category, sector, county, and closing date
Download tender documents securely through our API gateway
Enterprise-grade API with rate limiting and 99.9% uptime guarantee
Use HTTP headers for API authentication instead of query parameters.
// ✅ SECURE: Use headers
headers: {
'Authorization': 'Bearer YOUR_API_KEY'
}
Select from our pre-built solutions or build your own integration.
Deploy your integration and start displaying live tenders to your users.
Enter your API key to test connectivity and explore the API:
These endpoints trigger file downloads directly. Use the iframe method for reliable downloads.
// Response will appear here...
Get all available countries with their codes
Get all active tenders with optional filtering
?country=KE&category=5§or=12
Get detailed information about a specific tender
/api/v1/tenders/1
Tender descriptions are automatically formatted and sanitized
Get tenders published today
Get tenders closing soon (default: 7 days)
?days=3&country=KE
Get all documents for a specific tender
/api/v1/tenders/1/documents
Returns: { "status": true, "documents": [...] }
Download tender advertisement as PDF (Direct Download)
/api/v1/advert_doc/1
Auto-downloads: tender-title-advert.pdf
Note: This is a binary download, not a JSON response
Download specific document (Direct Download)
/api/v1/documents/1/download
Auto-downloads: document-title.extension
Note: This is a binary download, not a JSON response
Get all tender categories
Get all tender sectors
Get all tender statuses
Get all counties (optionally filter by country_id)
?country_id=1
Get company logo information by company ID
/api/v1/company_logo/1
Returns logo URL and whether it's the default placeholder
{
"status": true,
"logo_url": "https://www.tendersoko.com/images/companies/logo.png",
"is_default": false
}
// Tendersoko API - Document Download Solution
const API_KEY = 'YOUR_API_KEY_HERE';
const API_BASE = 'https://api.tendersoko.africa/api/v1';
/**
* Download tender advert - uses hidden iframe method
* This method avoids console errors and works reliably
*/
function downloadAdvert(tenderId) {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = `${API_BASE}/advert_doc/${tenderId}`;
document.body.appendChild(iframe);
setTimeout(() => {
if (iframe.parentNode) {
document.body.removeChild(iframe);
}
}, 5000);
return true;
}
/**
* Download document - works for all document types (PDF, DOC, etc.)
*/
function downloadDocument(docId) {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = `${API_BASE}/documents/${docId}/download`;
document.body.appendChild(iframe);
setTimeout(() => {
if (iframe.parentNode) {
document.body.removeChild(iframe);
}
}, 5000);
return true;
}
/**
* Alternative method using fetch (may show console errors but works)
*/
async function downloadDocumentWithFetch(docId, docTitle, docExtension) {
try {
const response = await fetch(`${API_BASE}/documents/${docId}/download`, {
headers: {
'Authorization': 'Bearer ' + API_KEY
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
// Create safe filename
const safeTitle = docTitle
.replace(/[^a-zA-Z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.toLowerCase()
.substring(0, 100);
a.download = `${safeTitle}.${docExtension.toLowerCase()}`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
return { success: true };
} catch (error) {
console.warn('Fetch method failed, falling back to iframe');
downloadDocument(docId);
return { success: true, method: 'fallback' };
}
}
// Example usage:
// downloadAdvert(1); // Downloads PDF advert
// downloadDocument(1); // Downloads specific document
downloadAdvert(1)
Downloads: tender-advert.pdf
/api/v1/tenders/1/documents
Returns JSON list of documents for tender 1
Copy and paste this code to display tenders on your website:
<!DOCTYPE html>
<html>
<head>
<title>Tenders Display</title>
<style>
.tenders-container { max-width: 1200px; margin: 0 auto; padding: 20px; }
.tender-card {
background: white; border-radius: 12px; padding: 20px; margin: 15px 0;
border: 1px solid #e2e8f0; box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: all 0.3s;
}
.tender-card:hover {
transform: translateY(-2px); box-shadow: 0 8px 25px rgba(0,0,0,0.15);
}
.tender-title {
font-size: 1.1rem; font-weight: 600; margin-bottom: 12px;
color: #334155; cursor: pointer;
}
.tender-meta {
display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px; margin: 10px 0;
}
.meta-item {
display: flex; align-items: center; gap: 8px;
color: #64748b; font-size: 0.9rem;
}
.tender-actions {
display: flex; gap: 10px; margin-top: 15px;
padding-top: 15px; border-top: 1px solid #e2e8f0;
}
.action-btn {
background: #f8fafc; color: #334155; padding: 8px 16px;
border: none; border-radius: 6px; font-size: 0.8rem; cursor: pointer;
transition: all 0.2s;
}
.action-btn:hover { background: #3b82f6; color: white; }
/* Document download notice */
.download-notice {
background: #fef3c7; border: 1px solid #f59e0b;
padding: 10px; border-radius: 6px; margin: 10px 0;
font-size: 0.85rem; color: #92400e;
}
</style>
</head>
<body>
<div class="tenders-container" id="tenders-container">
<div style="text-align: center; padding: 40px;">Loading tenders...</div>
</div>
<script>
const API_KEY = 'YOUR_API_KEY_HERE';
const API_BASE = 'https://api.tendersoko.africa/api/v1';
// Load tenders on page load
document.addEventListener('DOMContentLoaded', function() {
loadTenders();
});
async function loadTenders() {
try {
const response = await fetch(`${API_BASE}/tenders`, {
headers: {
'Authorization': 'Bearer ' + API_KEY
}
});
const data = await response.json();
if (data.status === 'OK') {
displayTenders(data.tenders);
} else {
showError('Failed to load tenders: ' + data.message);
}
} catch (error) {
showError('Network error: ' + error.message);
}
}
function displayTenders(tenders) {
const container = document.getElementById('tenders-container');
if (tenders.length === 0) {
container.innerHTML = '<div style="text-align: center; padding: 40px;">No tenders found</div>';
return;
}
container.innerHTML = tenders.map(tender => `
<div class="tender-card">
<div class="tender-title" onclick="viewTenderDetails(${tender.id})">
${escapeHtml(tender.title)}
</div>
<div class="tender-meta">
<div class="meta-item">
<i>📍</i>
<span>${escapeHtml(tender.county)} • ${escapeHtml(tender.country_name || tender.country)}</span>
</div>
<div class="meta-item">
<i>📅</i>
<span>Closes: ${tender.closing_date} (${tender.days_remaining} days left)</span>
</div>
<div class="meta-item">
<i>🛠️</i>
<span>${escapeHtml(tender.category_name || 'General')}</span>
</div>
<div class="meta-item">
<i>📄</i>
<span>${tender.docsCount || 0} Documents</span>
</div>
</div>
<div class="tender-actions">
<button class="action-btn" onclick="viewTenderDetails(${tender.id})">
View Details
</button>
<button class="action-btn" onclick="downloadTenderAdvert(${tender.id})">
Download Advert
</button>
</div>
</div>
`).join('');
}
function downloadTenderAdvert(tenderId) {
// Show notice about download
const notice = document.createElement('div');
notice.className = 'download-notice';
notice.innerHTML = '<i class="fas fa-info-circle"></i> Download starting...';
document.body.appendChild(notice);
setTimeout(() => notice.remove(), 3000);
// Trigger download using iframe method
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = `${API_BASE}/advert_doc/${tenderId}`;
document.body.appendChild(iframe);
// Cleanup
setTimeout(() => {
document.body.removeChild(iframe);
}, 5000);
}
async function viewTenderDetails(tenderId) {
try {
const response = await fetch(`${API_BASE}/tenders/${tenderId}`, {
headers: {
'Authorization': 'Bearer ' + API_KEY
}
});
const data = await response.json();
if (data.status === 'OK') {
const tender = data.tender;
// Display description (already sanitized by API)
const description = tender.description || 'No description available';
const modal = document.createElement('div');
modal.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; display: flex; align-items: center; justify-content: center;';
modal.innerHTML = `
<div style="background: white; padding: 30px; border-radius: 12px; max-width: 800px; width: 90%; max-height: 80vh; overflow-y: auto;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h3 style="margin: 0;">${escapeHtml(tender.title)}</h3>
<button onclick="this.closest('[style*=\"position: fixed\"]').remove()" style="background: none; border: none; font-size: 1.5rem; cursor: pointer; color: #64748b;">×</button>
</div>
<div>
<p><strong>Company:</strong> ${escapeHtml(tender.company)}</p>
<p><strong>Location:</strong> ${escapeHtml(tender.county)}, ${escapeHtml(tender.country_name)}</p>
<p><strong>Closing Date:</strong> ${tender.closing_date}</p>
<p><strong>Description:</strong></p>
<div style="background: #f8fafc; padding: 15px; border-radius: 6px; line-height: 1.6;">
${description}
</div>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button class="action-btn" onclick="downloadTenderAdvert(${tender.id})">
Download Advert
</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
}
} catch (error) {
alert('Failed to load tender details');
}
}
function showError(message) {
const container = document.getElementById('tenders-container');
container.innerHTML = \`<div style="color: #ef4444; text-align: center; padding: 20px;">\${escapeHtml(message)}</div>\`;
}
// HTML escaping function
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
</body>
</html>
Enhanced integration with filtering capabilities:
// Advanced integration with filtering
const API_KEY = 'YOUR_API_KEY_HERE';
const API_BASE = 'https://api.tendersoko.africa/api/v1';
let countries = [];
let categories = [];
let sectors = [];
// Initialize on page load
document.addEventListener('DOMContentLoaded', async function() {
await loadReferenceData();
await loadTenders();
});
// Load reference data for filters
async function loadReferenceData() {
try {
// Load countries with secure headers
const countriesResponse = await fetch(`${API_BASE}/countries`, {
headers: {
'Authorization': 'Bearer ' + API_KEY
}
});
const countriesData = await countriesResponse.json();
if (countriesData.status === true) {
countries = countriesData.countries;
populateCountryFilter();
}
// Load categories with secure headers
const categoriesResponse = await fetch(`${API_BASE}/categories`, {
headers: {
'Authorization': 'Bearer ' + API_KEY
}
});
const categoriesData = await categoriesResponse.json();
if (categoriesData.status === true) {
categories = Object.entries(categoriesData.categories).map(([id, name]) => ({ id: parseInt(id), name }));
populateCategoryFilter();
}
// Load sectors with secure headers
const sectorsResponse = await fetch(`${API_BASE}/sectors`, {
headers: {
'Authorization': 'Bearer ' + API_KEY
}
});
const sectorsData = await sectorsResponse.json();
if (sectorsData.status === true) {
sectors = sectorsData.sectors;
populateSectorFilter();
}
} catch (error) {
console.error('Failed to load reference data:', error);
}
}
// Load tenders with filters using secure headers
async function loadTenders(filters = {}) {
try {
const params = new URLSearchParams();
// Add filters but NOT the API key
if (filters.country) params.append('country', filters.country);
if (filters.category) params.append('category', filters.category);
if (filters.sector) params.append('sector', filters.sector);
if (filters.county) params.append('county', filters.county);
if (filters.type === 'today') {
params.append('action', 'today');
} else if (filters.type === 'closing') {
params.append('action', 'closing');
if (filters.days) params.append('days', filters.days);
}
const endpoint = filters.type === 'today' ? '/today' :
filters.type === 'closing' ? '/closing' : '/tenders';
const response = await fetch(`${API_BASE}${endpoint}?${params.toString()}`, {
headers: {
'Authorization': 'Bearer ' + API_KEY
}
});
const data = await response.json();
if (data.status === 'OK') {
displayTenders(data.tenders);
} else {
showError('Failed to load tenders: ' + data.message);
}
} catch (error) {
showError('Network error: ' + error.message);
}
}
// Display tenders with proper escaping
function displayTenders(tenders) {
const container = document.getElementById('tenders-container');
if (tenders.length === 0) {
container.innerHTML = '<div style="text-align: center; padding: 40px;">No tenders found</div>';
return;
}
container.innerHTML = tenders.map(tender => {
const safeTitle = escapeHtml(tender.title);
const safeCompany = escapeHtml(tender.company || 'N/A');
const safeCounty = escapeHtml(tender.county || 'N/A');
const safeCountry = escapeHtml(tender.country_name || tender.country || 'N/A');
const safeCategory = escapeHtml(tender.category_name || 'General');
return `
<div class="tender-card">
<div class="tender-title" onclick="viewTenderDetails(${tender.id})">
${safeTitle}
</div>
<div class="tender-meta">
<div class="meta-item">
<i>📍</i>
<span>${safeCounty} • ${safeCountry}</span>
</div>
<div class="meta-item">
<i>📅</i>
<span>Closes: ${tender.closing_date} (${tender.days_remaining} days left)</span>
</div>
<div class="meta-item">
<i>🏢</i>
<span>${safeCompany}</span>
</div>
<div class="meta-item">
<i>🛠️</i>
<span>${safeCategory}</span>
</div>
</div>
<div class="tender-actions">
<button class="action-btn" onclick="viewTenderDetails(${tender.id})">
View Details
</button>
<button class="action-btn" onclick="downloadTenderAdvert(${tender.id})">
Download Advert
</button>
</div>
</div>
`;
}).join('');
}
// View tender details
async function viewTenderDetails(tenderId) {
try {
const response = await fetch(`${API_BASE}/tenders/${tenderId}`, {
headers: {
'Authorization': 'Bearer ' + API_KEY
}
});
const data = await response.json();
if (data.status === 'OK') {
const tender = data.tender;
const safeTitle = escapeHtml(tender.title);
const safeCompany = escapeHtml(tender.company);
const safeCounty = escapeHtml(tender.county);
const safeCountry = escapeHtml(tender.country_name);
const description = tender.description || 'No description available';
const modal = document.createElement('div');
modal.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; display: flex; align-items: center; justify-content: center;';
modal.innerHTML = `
<div style="background: white; padding: 30px; border-radius: 12px; max-width: 800px; width: 90%; max-height: 80vh; overflow-y: auto;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h3 style="margin: 0;">${safeTitle}</h3>
<button onclick="this.closest('[style*=\"position: fixed\"]').remove()" style="background: none; border: none; font-size: 1.5rem; cursor: pointer; color: #64748b;">×</button>
</div>
<div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 20px;">
<div>
<strong>Company:</strong>
<p>${safeCompany}</p>
</div>
<div>
<strong>Location:</strong>
<p>${safeCounty}, ${safeCountry}</p>
</div>
<div>
<strong>Closing Date:</strong>
<p>${tender.closing_date}</p>
</div>
<div>
<strong>Sector:</strong>
<p>${escapeHtml(tender.sector || 'Not specified')}</p>
</div>
</div>
<p><strong>Description:</strong></p>
<div style="background: #f8fafc; padding: 15px; border-radius: 6px; line-height: 1.6;">
${description}
</div>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button class="action-btn" onclick="downloadTenderAdvert(${tender.id})">
Download Advert
</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
}
} catch (error) {
alert('Failed to load tender details');
}
}
// Download tender advert
function downloadTenderAdvert(tenderId) {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = \`\${API_BASE}/advert_doc/\${tenderId}\`;
document.body.appendChild(iframe);
setTimeout(() => {
if (iframe.parentNode) {
document.body.removeChild(iframe);
}
}, 5000);
}
// Filter functions
function filterByCountry(countryCode) {
loadTenders({ country: countryCode });
}
function filterByCategory(categoryId) {
loadTenders({ category: categoryId });
}
function showTodaysTenders() {
loadTenders({ type: 'today' });
}
function showClosingTenders(days = 7) {
loadTenders({ type: 'closing', days: days });
}
// HTML escaping function
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Helper functions to populate filter dropdowns
function populateCountryFilter() {
const select = document.getElementById('country-filter');
if (select && countries.length > 0) {
select.innerHTML = '<option value="">All Countries</option>' +
countries.map(country =>
\`<option value="\${country.code}">\${country.name}</option>\`
).join('');
}
}
function populateCategoryFilter() {
const select = document.getElementById('category-filter');
if (select && categories.length > 0) {
select.innerHTML = '<option value="">All Categories</option>' +
categories.map(category =>
\`<option value="\${category.id}">\${category.name}</option>\`
).join('');
}
}
function populateSectorFilter() {
const select = document.getElementById('sector-filter');
if (select && sectors.length > 0) {
select.innerHTML = '<option value="">All Sectors</option>' +
sectors.map(sector =>
\`<option value="\${sector.id}">\${sector.name}</option>\`
).join('');
}
}
// Example usage:
// filterByCountry('KE'); // Get tenders for Kenya
// filterByCountry('TZ'); // Get tenders for Tanzania
// filterByCategory(5); // Get tenders in category ID 5
Complete solution for handling document downloads:
// Professional Document Manager for Tendersoko API
class TendersokoDocumentManager {
constructor(apiKey) {
this.apiKey = apiKey;
this.apiBase = 'https://api.tendersoko.africa/api/v1';
}
/**
* Download tender advert - uses iframe method
*/
downloadAdvert(tenderId) {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = `${this.apiBase}/advert_doc/${tenderId}`;
document.body.appendChild(iframe);
setTimeout(() => {
if (iframe.parentNode) {
document.body.removeChild(iframe);
}
}, 5000);
return { success: true };
}
/**
* Download document
*/
downloadDocument(docId) {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = `${this.apiBase}/documents/${docId}/download`;
document.body.appendChild(iframe);
setTimeout(() => {
if (iframe.parentNode) {
document.body.removeChild(iframe);
}
}, 5000);
return { success: true };
}
/**
* Get document list for a tender
*/
async getTenderDocuments(tenderId) {
try {
const url = `${this.apiBase}/tenders/${tenderId}/documents`;
const response = await fetch(url, {
headers: {
'Authorization': 'Bearer ' + this.apiKey
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (data.status && data.documents) {
return {
success: true,
documents: data.documents
};
} else {
return { success: false, error: 'No documents found', documents: [] };
}
} catch (error) {
return { success: false, error: error.message, documents: [] };
}
}
}
// Usage Example
const documentManager = new TendersokoDocumentManager('YOUR_API_KEY_HERE');
// Download advert
documentManager.downloadAdvert(1);
// Download specific document
documentManager.downloadDocument(1);
// Get and display documents
documentManager.getTenderDocuments(1)
.then(result => {
if (result.success) {
console.log(`Found ${result.documents.length} documents`);
}
});
// Node.js/Express example for secure server-side proxy
const express = require('express');
const axios = require('axios');
const app = express();
// Your secure endpoint that hides the API key
app.get('/api/tenders', async (req, res) => {
try {
const response = await axios.get('https://api.tendersoko.africa/api/v1/tenders', {
headers: {
'Authorization': 'Bearer YOUR_SERVER_API_KEY'
},
params: req.query
});
res.json(response.data);
} catch (error) {
res.status(error.response?.status || 500).json({
error: 'Failed to fetch tenders'
});
}
});
// Document download proxy
app.get('/api/documents/:id/download', async (req, res) => {
try {
const response = await axios.get(`https://api.tendersoko.africa/api/v1/documents/${req.params.id}/download`, {
headers: {
'Authorization': 'Bearer YOUR_SERVER_API_KEY'
},
responseType: 'stream'
});
res.set('Content-Type', response.headers['content-type']);
res.set('Content-Disposition', response.headers['content-disposition']);
response.data.pipe(res);
} catch (error) {
res.status(error.response?.status || 500).json({
error: 'Failed to download document'
});
}
});
// Advert download proxy
app.get('/api/advert_doc/:id', async (req, res) => {
try {
const response = await axios.get(`https://api.tendersoko.africa/api/v1/advert_doc/${req.params.id}`, {
headers: {
'Authorization': 'Bearer YOUR_SERVER_API_KEY'
},
responseType: 'stream'
});
res.set('Content-Type', response.headers['content-type']);
res.set('Content-Disposition', response.headers['content-disposition']);
response.data.pipe(res);
} catch (error) {
res.status(error.response?.status || 500).json({
error: 'Failed to download advert'
});
}
});