API documentation
A public, read-only REST API. Every score comes with its provenance — source, license, dates, confidence tier, and the exact normalization formula. An agent gets the number and the reason to trust it.
Get a free key
Scores, rankings, and comparisons need an API key. It's free and instant — enter an email, get a wsk_… key. No payment, no approval. (The catalog endpoints below work without a key.)
The key is shown once — store it in an env var. Lost it? Just generate another.
Authentication
Send your key as a bearer token on every request to a scored endpoint:
Authorization: Bearer wsk_your_key_here
Open (no key): GET /api/v1/metrics and GET /api/v1/countries — the catalog (names, units, tiers, categories), so you can explore before signing up.
Keyed: everything with scores — /countries/{iso3}, /metrics/{slug}/ranking, /compare.
Base URL: https://worldscore-pearl.vercel.app/api/v1. CORS is enabled for browser and agent clients.
Rate limits
Per key: 1,000 requests/day and 60 requests/minute. Every response carries:
X-RateLimit-Limit: 1000 X-RateLimit-Remaining: 987 X-RateLimit-Reset: 43200 # seconds until the daily window resets (00:00 UTC)
Exceed a window and you get 429 with a Retry-After header (seconds). The minute cap stops a runaway loop from burning the whole day's quota in seconds.
Endpoints
All under https://worldscore-pearl.vercel.app/api/v1. 🔓 = no key, 🔑 = key required.
- 🔓
GET /metrics— metric catalog (tier, direction, unit, methodology). - 🔓
GET /countries— every country (ISO 3166-1 alpha-3, region, centroid). - 🔑
GET /countries/{iso3}— all scores for one country, each with provenance + a per-metric year series. - 🔑
GET /metrics/{slug}/ranking— full ranking, best first.slug=crimetakes?crimeType=. - 🔑
GET /compare?countries=ESP,PRT,FRA— 2+ countries across metrics; optional&metrics=.
Example response (GET /metrics/gdp-per-capita-ppp/ranking, trimmed):
{
"apiVersion": "v1",
"metric": { "id": "gdp-per-capita-ppp", "name": "GDP per capita (PPP)",
"confidenceTier": "A", "direction": "higher_better", "metricKind": "evaluative" },
"coverage": { "withData": 202, "total": 250 },
"count": 202,
"ranking": [
{ "name": "Luxembourg", "iso3": "LUX", "region": "Europe",
"score": 100, "percentileRank": 99.75,
"raw": { "value": 143342.9, "unit": "international $ (PPP, current)" },
"rank": 1, "total": 202, "confidenceTier": "A",
"provenance": { "source": { "name": "World Bank", "license": "CC BY 4.0" },
"sourceYear": 2024, "normalization": { "method": "log_minmax" } } },
…
]
}The provenance object, field by field
Every scored value carries these — read them before trusting a number:
score— the metric's native 0–100 level. Comparable within a metric, not across metrics (different normalizations).percentileRank— position among countries with data (0–100), direction-applied. Use this to compare across metrics.raw—{ value, unit }, the real source figure before normalization.rank/total— position out of countries with data.confidenceTier—A–D(see below).reliabilityCaution— present on Tier C/D: a plain-language warning about what the number does and doesn't mean.scoreComparability— a reminder thatscoreis per-metric andpercentileRankis cross-metric.metricKind—evaluative(higher = better) ordescriptive(a neutral magnitude, not better/worse).provenance.source—{ name, url, license, publisher }.provenance.sourceYear/retrievedAt— the data's reference year and our snapshot date.provenance.normalization—method+params(e.g. the dataset min/max), so you can reconstruct the score by hand.series(country endpoint) —[{ year, score }], each point normalized within its own year's dataset (a real trend, not an artefact of today's extremes).
Confidence tiers & when NOT to trust a number
An agent must know a metric's reliability before quoting it:
- Tier A — official statistics, hard to game (World Bank, UNESCO, WHO-via-WDI). Trust as a factual figure.
- Tier B — solid but with a caveat (a projection, a survey, or a stale/pre-pandemic reference year). Quote with the year.
- Tier C — a proxy or a measure confounded by reporting (e.g. police-recorded crime measures reporting propensity as much as crime). Carries a
reliabilityCaution. Never present as a definitive ranking. - Tier D — reserved for hand-transcribed/unlicensed data; none ships.
Rule of thumb for agents: if reliabilityCaution is present, surface it alongside the number. If metricKind is descriptive, do not say a higher value is “better”.
Error codes
400— bad request (e.g. fewer than 2 countries for compare). The body explains.401— missing/unknown API key on a keyed endpoint.404— unknown country or metric slug.429— rate limit hit; seeRetry-After.503— key signup temporarily unavailable.
Every error body is { "error": { "status", "message" } }.
Copy-paste snippets
curl
# Catalog — no key curl https://worldscore-pearl.vercel.app/api/v1/metrics # A ranking — with key curl -H "Authorization: Bearer $WORLDSCORE_API_KEY" \ "https://worldscore-pearl.vercel.app/api/v1/metrics/gdp-per-capita-ppp/ranking"
JavaScript (fetch)
const res = await fetch(
"https://worldscore-pearl.vercel.app/api/v1/countries/ESP",
{ headers: { Authorization: `Bearer ${process.env.WORLDSCORE_API_KEY}` } },
);
const { scores } = await res.json();
for (const s of scores) {
console.log(s.metric.name, s.raw?.value, s.confidenceTier, s.reliabilityCaution ?? "");
}Python (requests)
import os, requests
h = {"Authorization": f"Bearer {os.environ['WORLDSCORE_API_KEY']}"}
r = requests.get(
"https://worldscore-pearl.vercel.app/api/v1/compare",
params={"countries": "ESP,PRT,FRA", "metrics": "gdp-per-capita-ppp,corruption-cpi"},
headers=h,
)
r.raise_for_status()
for m in r.json()["metrics"]:
print(m["metric"]["name"], "→", [c["raw"] for c in m["values"]])