SPB Git

spb/countryatlas

Public
12.2 KB

# CountryAtlas API — reference

Base URL: https://www.countryatlas.co/api/v1 (proxied by the Next.js app to the FastAPI process on 127.0.0.1:8291). Interactive docs: /api/v1/docs (Swagger) · /api/v1/redoc · spec /api/v1/openapi.json.

  • All endpoints are GET (except POST /admin/refresh, POST /admin/cache/clear) and return JSON (gzip when accepted).
  • Identifiers: countries by ISO3 or slug (case-insensitive: CAN, can, canada); indicators, topics and groups by slug.
  • Every response carries meta: {built_at, run_id, generated_at}; the header X-CountryAtlas-Run repeats the snapshot run id and X-Cache: HIT|MISS tells whether the in-process cache served it (cache keys include the run id, so a new snapshot invalidates everything).
  • Errors are RFC 7807 application/problem+json: {"type","title","status","detail","instance",…}. 404 unknown country/indicator/topic/group (with a helpful detail), 400 bad combination, 422 invalid parameter (with errors[]), 429 rate limit (120 req/min/IP, Retry-After), 503 {"title":"Data not built yet"} while no snapshot exists.
  • Run locally: CA_DATA_DIR=~/countryatlas-data ca-api (or python -m uvicorn countryatlas.api.main:app --port 8291).

# The provenance object

Every value the API returns (headline metrics, series points, ranking rows, map values, curated lists, changes…) carries a provenance object built from observations → indicator_sources → sources:

json
{
  "value": 55697.66, "period": "2025-01-01", "year": 2025, "unit": "current US$", "is_estimate": false, "is_forecast": false,
  "status": "imported", "formatted": "55.7k",
  "provenance": {
    "source": "worldbank", "source_name": "World Bank", "dataset": "WDI", "series_code": "NY.GDP.PCAP.CD",
    "retrieved_at": "2026-09-11T06:49:30Z", "source_updated_at": "2026-07-13T00:00:00Z",
    "url": "https://data.worldbank.org/indicator/NY.GDP.PCAP.CD?locations=CA", "transform": null, "licence": "CC BY 4.0"
  }
}
field meaning
source / source_name connector id (worldbank, imf, oecd, eurostat, who, fred, owid, bis, ilo) and display name
dataset, series_code dataset and series inside the source (WDI NY.GDP.PCAP.CD, WEO NGDPDPC, OWID co2_per_capita, FRED FEDFUNDS…)
retrieved_at when the pipeline fetched the raw payload (UTC)
source_updated_at last update advertised by the source (may be null when the source does not publish it)
url deep link: World Bank …/indicator/{code}?locations={iso2}, OWID grapher ourworldindata.org/grapher/{slug} or the owid/co2-data / owid/energy-data repos, Eurostat databrowser, WHO GHO indicator page, FRED series page, IMF / OECD / BIS / ILO data portals
transform expression applied at normalisation (e.g. x*1e9), null if none
licence licence of the source

Series responses also list all sources used (sources[], with n_values) and the dominant one as provenance.

# Endpoints

# Health

text
GET /health          → {status: "ok"|"empty", run_id, built_at, observations, countries, indicators, version, cache}

/health never returns 503; status: "empty" means the pipeline has not produced atlas.duckdb yet.

# Countries

text
GET /countries?region=&income=&q=&sort=name|population|gdp|gdp_per_capita|coverage&kind=&limit=&offset=
GET /countries/{id}                       overview: country header, groups, coverage, freshness, headline metrics, topics summary, neighbours
GET /countries/{id}/topics/{topic}        indicators of a topic grouped by subtopic (registry order); indicators without data → has_data:false
GET /countries/{id}/series/{indicator}?from=&to=&freq=A|Q|M&include_forecast=true&include_alt=false
GET /countries/{id}/changes?limit=&kind=  recent detected changes (by severity)
GET /countries/{id}/events?limit=&kind=&indicator=   whole-history timeline
GET /countries/{id}/similar?mode=overall|economic|demographic|energy|social&limit=
GET /countries/{id}/insights
GET /countries/{id}/dna
GET /countries/{id}/download.csv|json?include_forecast=&topic=

region accepts any group id or slug (ecs, europe-central-asia, oecd, g7…); income accepts HIC|UMC|LMC|LIC or the slug.

bash
curl -s https://www.countryatlas.co/api/v1/countries/canada | jq '.headline[] | {indicator, formatted, year, rank_world, n_world, source: .provenance.source}'
# {"indicator":"gdp-per-capita","formatted":"55.7k","year":2025,"rank_world":21,"n_world":188,"source":"worldbank"} …
curl -s "https://www.countryatlas.co/api/v1/countries/CAN/series/gdp-per-capita?from=2000" | jq '.stats, .provenance'

Headline metric object (MetricValue): value, formatted, period, year, unit, is_estimate, is_forecast, status, prev{period,value}, change{abs,pct,formatted}, change_10y{…}, rank_world/n_world, rank_region/n_region (World Bank region), rank_income/n_income, rank_year, rank_is_stale (rank computed on a year > 2 years older than the indicator's latest year), higher_is_better, sparkline [[year, value]…] (last 30 non-forecast points), provenance.

# Indicators

text
GET /indicators?topic=&q=&featured=&source=&with_data=
GET /indicators/{slug}                    definition, sources (priority order, deep links), coverage (+ by_year), world_latest, freshness, top5/bottom5, years, topics
GET /indicators/{slug}/map?year=&nearest=false&classes=6
GET /indicators/{slug}/trend?group=world&from=&to=&min_n=5
GET /indicators/{slug}/download.csv|json?from=&to=&include_forecast=
  • world_latest is computed across countries (World Bank aggregates such as WLD are not stored): kind is sum for additive indicators, weighted_mean (population-weighted) for per-capita / share indicators, median otherwise; median, mean, weighted_mean and n are always returned.
  • map: without year, the latest year with ≥ 50 countries is used (year_used). With nearest=true each country's latest value within 3 years of the reference year is used and years{ISO3: year} says which. legend.breaks are quantile breaks (5–7 classes).
  • trend: per year median, mean, weighted_mean (population) for per-capita/percent indicators, sum for additive ones, n; preferred says which to plot.
bash
curl -s "https://www.countryatlas.co/api/v1/indicators/life-expectancy/map?year=2023" | jq '{year_used, n, legend, CAN: .values.CAN}'
curl -s "https://www.countryatlas.co/api/v1/indicators/gdp/trend?group=oecd" | jq '.points[-1]'

# Series bundle

text
GET /series?country=CAN,FRA&indicator=gdp-per-capita,inflation&from=1990&to=2026&freq=A&include_forecast=true

Returns series[] (one per indicator × country), each with values[] (per-point provenance), sources[], provenance, stats{min,max,first,last,cagr}.

# Rankings

text
GET /rankings?topic=                      rankable indicators (featured first) with ranking_year / ranking_n
GET /rankings/{indicator}?year=&group=world&sort=asc|desc&limit=50&offset=0&sparkline=true
GET /rankings/{indicator}/history?countries=CAN,USA&from=&to=

Rows: rank (within the requested group), rank_world/n_world (from the snapshot), pct_rank, country{id,slug,name,flag,region,income}, value, formatted, year, change_1y, change_10y, sparkline, provenance. Default sort is asc when higher_is_better=false (e.g. infant mortality), otherwise desc. year_used falls back to the nearest available year; years_available lists them.

bash
curl -s "https://www.countryatlas.co/api/v1/rankings/gdp-per-capita?limit=3" | jq '.rows[] | [.rank, .country.name, .formatted, .change_1y.formatted]'
# [1,"Luxembourg","147.3k","+6.9 %"] [2,"Ireland","131.6k","+16.6 %"] [3,"Switzerland","114.8k","+6.6 %"]

# Compare

text
GET /compare?countries=CAN,USA,FRA&indicators=gdp,gdp-per-capita&from=&to=&mode=absolute|per-capita|index100|pct&include_forecast=
GET /compare/snapshot?countries=CAN,USA&topic=economy   (or &indicators=a,b; default = headline indicators)
GET /compare/download.csv|json?countries=&indicators=&from=&to=

Modes: index100 rebases each series to 100 at the first available year ≥ from; per-capita divides additive (aggregation=sum) indicators by the population series of the same year (already per-capita indicators are left unchanged, transform.applied=false); pct = % change vs previous period. snapshot returns one row per indicator with a values{ISO3: MetricValue} map and best (when higher_is_better is known).

# Regions / groups

text
GET /regions?kind=world|region|continent|income|org
GET /regions/{slug}?indicator=gdp-per-capita&sort=asc|desc

Group page: members with headline values (+ provenance), aggregates (sum population/GDP, population-weighted GDP per capita, median life expectancy…, each with kind), and a member ranking on the chosen indicator.

text
GET /search?q=&limit=10&type=country|indicator|topic|region|source

Typed hits {type, id, slug, name, hint, score, url} from search_index (exact → prefix → word → substring → Jaro-Winkler fuzzy). Hints: Country · North America, Indicator · Economy · annual %, Topic · 12 indicators, Region · Organisation · 38 members. Two-word combos such as housing canada or canada gdp also return country_topic / country_indicator hits with a ready URL (/countries/canada/housing).

# Home

text
GET /home

snapshot (world population / GDP sums, median life expectancy, counts, built_at), lists (largest economies, fastest population growth among countries > 1 M, highest life expectancy, energy transition leaders, highest GDP per capita PPP, lowest unemployment among countries

5 M; 8 rows each with provenance), recent_changes (12 most severe), recently_updated, featured_indicators, trending.

# Changes, sources, methodology

text
GET /changes?limit=&offset=&kind=&indicator=&country=&topic=&min_severity=
GET /sources                              n_observations, n_indicators, licence, last_retrieved_at
GET /sources/{id}                         indicators mapped, datasets, import_runs, freshness
GET /methodology                          registry-derived: topics, units/formats, source priority rule + URL patterns, validation rules, derived computations, DNA dimensions

# Downloads

CSV is streamed with a leading # CountryAtlas export · run … · built … comment line and the columns country_id, country_name, indicator_id, indicator_name, period, year, frequency, value, unit, is_estimate, is_forecast, status, source, source_name, dataset, series_code, retrieved_at, source_updated_at, url, licence. JSON returns {meta, n, columns, rows}.

# Admin (header X-Admin-Token: $CA_ADMIN_TOKEN)

text
GET  /admin/overview          db, meta, table counts, connectors health (import_runs), sources freshness/stale counts, scheduler heartbeat (data_dir/scheduler.json + scheduler.pid), cache stats
GET  /admin/runs?limit=&connector=&status=
GET  /admin/issues?severity=&connector=&indicator=&code=&run_id=&limit=
GET  /admin/coverage          indicator × n_countries / last_year matrix + per-country coverage
GET  /admin/raw?run_id=       raw files stored for a run
POST /admin/refresh           sends SIGUSR1 to the scheduler pid (409 when no scheduler)
POST /admin/cache/clear

403 on a wrong/missing token, 503 {"title":"Admin disabled"} when CA_ADMIN_TOKEN is not set.

# Operations notes

  • Snapshot swap: the API opens ~/countryatlas-data/atlas.duckdb read-only and compares st_ino/st_mtime_ns on every request; when the pipeline os.replace()s a new file, the old connection is closed and the new one opened (DuckDB caches instances per path, so the close must happen first). Responses are cached in-process per (run_id, path, query); nothing survives a new run id.
  • Performance (real snapshot, 2.0 M observations): country overview ≈ 12 ms warm (≈ 170 ms on the very first request while static lookups load), rankings ≈ 20 ms, home ≈ 33 ms, map ≈ 4 ms, cached hits ≈ 1 ms.
  • Formatting: formatted strings use the indicator's format: currency/number compact (53.4k, 1.2T), percent 3.4 %, years 82.1 yrs, tonnes 5.2 t, per-1000 3.2 per 1,000, per-100k 1.2 per 100k.
Files · b9cbc476e6e7b9e9089cbe9ec3938c16ca4f8aaf
Menu

Theme