Platform
Rate Limits
Per-plan request quotas, 429 behavior, and client patterns for retries, batching, and caching on the Africa API.
Your plan sets a monthly request quota and a per-minute rate limit, shared across the account's API keys. Exceeding either limit returns 429 Too Many Requests.
Plan Limits
| Plan | Monthly requests | Per minute | API keys |
|---|---|---|---|
| Free | 1,000 | 5 | 1 |
| Analyst | 250,000 | 60 | 3 |
| Team | 2,000,000 | 1,000 | 5 |
Monthly counters reset on the first of each calendar month in UTC. Per-minute limits use fixed minute windows. Plans have the same data and endpoint access. See pricing for plan details, and manage billing in the dashboard.
What a 429 Looks Like
The response body tells you which limit you hit:
{ "detail": "rate limit exceeded for plan 'free' (5 requests/minute)" }{
"detail": "This request was blocked because the monthly request limit for Free (1,000 requests) was reached. Upgrade to Analyst: https://africa-api.com/dashboard?section=billing"
}- Per-minute 429: back off and retry after the next minute window begins.
- Monthly 429: wait for the counter to reset on the 1st, or choose a plan with a quota above your current usage. Retrying does not restore quota.
Responses do not currently include X-RateLimit-* or Retry-After headers. Check monthly usage in the dashboard, and inspect the detail string to distinguish the two limit errors.
Recommended Client Controls
- Set explicit request timeouts.
- Retry transient failures (
429per-minute,5xx) with backoff. Stop retrying when the monthly quota is exhausted. - Batch related observations into one
/v1/datacall instead of many single-resource requests. - Cache
GET /v1/countriesusingETagandCache-Control. - Avoid request spikes when hydrating dashboards.
Retry Template (JavaScript)
async function requestWithBackoff(url, init, maxAttempts = 4) {
let delayMs = 300;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const response = await fetch(url, init);
if (response.ok) return response;
let retryable = response.status >= 500;
if (response.status === 429) {
const body = await response.clone().json().catch(() => null);
// Monthly quota 429s won't clear on retry; return them to the caller.
retryable = !body?.detail?.includes("monthly request limit");
}
if (!retryable || attempt === maxAttempts) return response;
await new Promise((resolve) => setTimeout(resolve, delayMs + Math.random() * 200));
delayMs *= 2;
}
}Query Design Tips
Make each request count against your quota:
- Prefer
country_codes=ng,ke,zaover three separate requests. - Prefer
metric_keys=population_total,gdp_current_usdwhere a combined response is acceptable. - Use
latest=truewhen you do not need a full time series. - One
/v1/countries/{code}/signalscall replaces several single-domain requests when you need a country snapshot.