Platform
Rate Limits
Per-plan request quotas, 429 behavior, and client patterns for retries, batching, and caching on the Africa API.
Every API key is governed by two limits set by its plan: a monthly request quota and a per-minute rate limit. Both are enforced per account, and both return 429 Too Many Requests when exceeded — your key is never suspended for hitting a limit.
Plan Limits
| Plan | Monthly requests | Per minute | API keys |
|---|---|---|---|
| Free | 1,000 | 5 | 1 |
| Builder | 3,000,000 | 1,000 | 5 |
| Pro | 30,000,000 | 5,000 | 25 |
Monthly counters reset on the first of each calendar month. Per-minute limits operate on a rolling window. Every plan serves the same data and endpoints — see pricing for plan details and upgrades, which take effect immediately and are self-serve 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": "monthly request limit reached for plan 'free' (1000 requests)" }- Per-minute 429 — transient. Back off and retry; the window clears within seconds.
- Monthly 429 — persistent until the counter resets on the 1st, or instantly resolved by an upgrade.
Responses do not currently include X-RateLimit-* or Retry-After headers. Track your usage in the dashboard, which shows live monthly consumption per key, and parse the detail string if you need to distinguish the two cases programmatically.
Recommended Client Controls
- Set explicit request timeouts.
- Retry only transient failures (
429per-minute,5xx) — don't hot-loop on a monthly 429. - 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 — surface them instead.
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.