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

PlanMonthly requestsPer minuteAPI keys
Free1,00051
Builder3,000,0001,0005
Pro30,000,0005,00025

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.

  1. Set explicit request timeouts.
  2. Retry only transient failures (429 per-minute, 5xx) — don't hot-loop on a monthly 429.
  3. Batch related observations into one /v1/data call instead of many single-resource requests.
  4. Cache GET /v1/countries using ETag and Cache-Control.
  5. 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,za over three separate requests.
  • Prefer metric_keys=population_total,gdp_current_usd where a combined response is acceptable.
  • Use latest=true when you do not need a full time series.
  • One /v1/countries/{code}/signals call replaces several single-domain requests when you need a country snapshot.

On this page