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(exceptPOST /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 headerX-CountryAtlas-Runrepeats the snapshot run id andX-Cache: HIT|MISStells 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",…}.404unknown country/indicator/topic/group (with a helpfuldetail),400bad combination,422invalid parameter (witherrors[]),429rate 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(orpython -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:
{
"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
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
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.
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
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_latestis computed across countries (World Bank aggregates such as WLD are not stored):kindissumfor additive indicators,weighted_mean(population-weighted) for per-capita / share indicators,medianotherwise;median,mean,weighted_meanandnare always returned.map: withoutyear, the latest year with ≥ 50 countries is used (year_used). Withnearest=trueeach country's latest value within 3 years of the reference year is used andyears{ISO3: year}says which.legend.breaksare quantile breaks (5–7 classes).trend: per yearmedian,mean,weighted_mean(population) for per-capita/percent indicators,sumfor additive ones,n;preferredsays which to plot.
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
GET /series?country=CAN,FRA&indicator=gdp-per-capita,inflation&from=1990&to=2026&freq=A&include_forecast=trueReturns series[] (one per indicator × country), each with values[] (per-point provenance), sources[], provenance, stats{min,max,first,last,cagr}.
Rankings
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.
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
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
GET /regions?kind=world|region|continent|income|org
GET /regions/{slug}?indicator=gdp-per-capita&sort=asc|descGroup 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.
Search
GET /search?q=&limit=10&type=country|indicator|topic|region|sourceTyped 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
GET /homesnapshot (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
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 dimensionsDownloads
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)
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/clear403 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.duckdbread-only and comparesst_ino/st_mtime_nson every request; when the pipelineos.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:
formattedstrings use the indicator'sformat: currency/number compact (53.4k,1.2T), percent3.4 %, years82.1 yrs, tonnes5.2 t, per-10003.2 per 1,000, per-100k1.2 per 100k.