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=crime takes ?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 that score is per-metric and percentileRank is cross-metric.
  • metricKind — evaluative (higher = better) or descriptive (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; see Retry-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"]])