[{
"page": 1,
"pages": 1,
"per_page": "50",
"total": 1
},
[{
"id": "NY.GDP.MKTP.CD","name":
"GDP (current US$)",
"unit": "",
"source": {
"id": "2",
"value": "World Development Indicators"},
"sourceNote": "GDP at purchaser's prices ... ",
"sourceOrganization": "World Bank national accounts data, and OECD National Accounts data files.",
"topics": [
{"id": "19","value": "Climate Change"},
{"id": "3","value": "Economy & Growth"}
]
}]
]Getting to know the response structure
Sample JSON response structure is a two-element list. Element 0 holds metadata (page, pages, per_page, total). Element 1 holds the records with fields such as id, name, unit, source, topics.
Initial checks
Inflation, consumer prices (annual %) data requested for the USA.
import requests
url = "https://api.worldbank.org/v2/country/USA/indicator/FP.CPI.TOTL.ZG"
response = requests.get(url, params={"format": "json"})
data = response.json() # convert response JSON into Python object
# print(data)
print(f"The response object is a {type(response).__name__}.")
print(f"The data object is a {type(data).__name__}.")
print(f"The length of the response object is {len(data)}.")
print(f"The first element of the response object is a {type(data[0]).__name__}.")
print(f"The second element of the response object is a {type(data[1]).__name__}.")
print(type(response))The response object is a Response.
The data object is a list.
The length of the response object is 2.
The first element of the response object is a dict.
The second element of the response object is a list.
<class 'requests.models.Response'>
Metadata inspected:
meta, records = data # unpack: first element is meta, second is records
print(meta){'page': 1, 'pages': 2, 'per_page': 50, 'total': 65, 'sourceid': '2', 'lastupdated': '2026-01-28'}
From the metadata/pagination dictionary:
- page 1 is shown
- 2 pages in total
- 50 records per page
- 65 records in total
- source id = 2 (World Development Indicators)
- last updated on 2026-01-28
First two elements of the records:
print(type(records))
print(records[:2])<class 'list'>
[{'indicator': {'id': 'FP.CPI.TOTL.ZG', 'value': 'Inflation, consumer prices (annual %)'}, 'country': {'id': 'US', 'value': 'United States'}, 'countryiso3code': 'USA', 'date': '2024', 'value': 2.94952520485207, 'unit': '', 'obs_status': '', 'decimal': 1}, {'indicator': {'id': 'FP.CPI.TOTL.ZG', 'value': 'Inflation, consumer prices (annual %)'}, 'country': {'id': 'US', 'value': 'United States'}, 'countryiso3code': 'USA', 'date': '2023', 'value': 4.11633838374488, 'unit': '', 'obs_status': '', 'decimal': 1}]
Total number of records returned in the first call:
print(len(data[1]))50
There are 50 records, which matches the API’s default per-page size.
All 65 records are fetched in a single request by adjusting the per_page parameter:
url = "https://api.worldbank.org/v2/country/USA/indicator/FP.CPI.TOTL.ZG"
response = requests.get(url, params={"format": "json", "per_page": 70}) # 70: simply a number above 65
data = response.json()
print(len(data[1]))65
Handling multi-page responses
Looping through pages with the default per_page=50 setting:
response = requests.get(url, params={"format": "json"})
meta, records = response.json()
print(meta)
print(meta["pages"]){'page': 1, 'pages': 2, 'per_page': 50, 'total': 65, 'sourceid': '2', 'lastupdated': '2026-01-28'}
2
Total records per page:
last_page = meta["pages"]
for page in range(1, last_page + 1):
meta, records = requests.get(url, params={"format": "json", "page": page}).json()
print(f"Page {page} has {len(records)} records")Page 1 has 50 records
Page 2 has 15 records
Page-by-page fetch to build the full dataset:
url = "https://api.worldbank.org/v2/country/USA/indicator/FP.CPI.TOTL.ZG"
all_records = []
page = 1
while True:
meta, records = requests.get(url, params={"format": "json", "page": page}).json()
if not records:
break
all_records.extend(records)
if page == meta["pages"]:
break
page += 1
print("Total records:", len(all_records))
print("First record:", all_records[0])Total records: 65
First record: {'indicator': {'id': 'FP.CPI.TOTL.ZG', 'value': 'Inflation, consumer prices (annual %)'}, 'country': {'id': 'US', 'value': 'United States'}, 'countryiso3code': 'USA', 'date': '2024', 'value': 2.94952520485207, 'unit': '', 'obs_status': '', 'decimal': 1}
Fetching multiple indicators (CPI and Gini) for multiple countries (USA, NLD, TUR)
sourceid=2 must be included when querying multiple indicators as explained here.
Hard-coded URL approach:
url = "https://api.worldbank.org/v2/country/USA;TUR;NLD/indicator/FP.CPI.TOTL.ZG;SI.POV.GINI"
all_records = []
page = 1
while True:
meta, records = requests.get(url, params={"format": "json", "page": page, "source": 2}).json()
if not records:
break
all_records.extend(records)
if page == meta["pages"]:
break
page += 1
print("Total records:", len(all_records))
print("First record:", all_records[0])Total records: 390
First record: {'indicator': {'id': 'FP.CPI.TOTL.ZG', 'value': 'Inflation, consumer prices (annual %)'}, 'country': {'id': 'NL', 'value': 'Netherlands'}, 'countryiso3code': 'NLD', 'date': '2024', 'value': 3.34754304219953, 'unit': '', 'obs_status': '', 'decimal': 1}
Parameterized URL approach:
# Building URL
base_url = "https://api.worldbank.org/v2/country/{country}/indicator/{indicator}"
countries = ["USA", "TUR", "NLD"]
indicators = ["FP.CPI.TOTL.ZG", "SI.POV.GINI"]
country_str = ";".join(countries)
indicator_str = ";".join(indicators)
url = base_url.format(country=country_str, indicator=indicator_str)
print(url)
# Querying data
all_records = []
page = 1
while True:
meta, records = requests.get(url, params={"format": "json", "page": page, "source": 2}).json()
if not records:
break
all_records.extend(records)
if page == meta["pages"]:
break
page += 1
print("Total records:", len(all_records))
print("First record:", all_records[0])https://api.worldbank.org/v2/country/USA;TUR;NLD/indicator/FP.CPI.TOTL.ZG;SI.POV.GINI
Total records: 390
First record: {'indicator': {'id': 'FP.CPI.TOTL.ZG', 'value': 'Inflation, consumer prices (annual %)'}, 'country': {'id': 'NL', 'value': 'Netherlands'}, 'countryiso3code': 'NLD', 'date': '2024', 'value': 3.34754304219953, 'unit': '', 'obs_status': '', 'decimal': 1}
View the output as a DataFrame and tidy the fields
import pandas as pd
df = pd.DataFrame(all_records)
df = df[["countryiso3code", "date", "indicator", "value"]]
df = df.rename(columns={"countryiso3code": "country"})
df["indicator"] = df["indicator"].str.get("id") # the indicator column is a dict; only the indicator id is extracted
df| country | date | indicator | value | |
|---|---|---|---|---|
| 0 | NLD | 2024 | FP.CPI.TOTL.ZG | 3.347543 |
| 1 | NLD | 2023 | FP.CPI.TOTL.ZG | 3.838394 |
| 2 | NLD | 2022 | FP.CPI.TOTL.ZG | 10.001208 |
| 3 | NLD | 2021 | FP.CPI.TOTL.ZG | 2.675720 |
| 4 | NLD | 2020 | FP.CPI.TOTL.ZG | 1.272460 |
| ... | ... | ... | ... | ... |
| 385 | USA | 1964 | SI.POV.GINI | 37.400000 |
| 386 | USA | 1963 | SI.POV.GINI | 36.700000 |
| 387 | USA | 1962 | SI.POV.GINI | NaN |
| 388 | USA | 1961 | SI.POV.GINI | NaN |
| 389 | USA | 1960 | SI.POV.GINI | NaN |
390 rows × 4 columns
Creating a custom function for future use
Function for one country and one indicator:
def fetch_indicator(indicator: str, country: str = "all"):
all_records = []
page = 1
base_url = "https://api.worldbank.org/v2/country/{country}/indicator/{indicator}"
while True:
url = base_url.format(country=country, indicator=indicator)
meta, records = requests.get(url, params={"format": "json", "page": page, "source": 2}).json()
if not records:
break
all_records.extend(records)
if page == meta["pages"]:
break
page += 1
return all_recordsTest:
all_records = fetch_indicator("FP.CPI.TOTL.ZG", "USA")
print("Total records:", len(all_records))
print("First record:", all_records[0])Total records: 65
First record: {'indicator': {'id': 'FP.CPI.TOTL.ZG', 'value': 'Inflation, consumer prices (annual %)'}, 'country': {'id': 'US', 'value': 'United States'}, 'countryiso3code': 'USA', 'date': '2024', 'value': 2.94952520485207, 'unit': '', 'obs_status': '', 'decimal': 1}
Function for multiple countries and multiple indicators:
def fetch_indicators(indicators: str | list, countries: str | list = "all", per_page=50):
if isinstance(indicators, list):
indicators = ";".join(indicators)
if isinstance(countries, list):
countries = ";".join(countries)
all_records = []
page = 1
base_url = "https://api.worldbank.org/v2/country/{countries}/indicator/{indicators}"
while True:
url = base_url.format(countries=countries, indicators=indicators)
meta, records = requests.get(url, params={"format": "json", "page": page, "source": 2}).json()
if not records:
break
all_records.extend(records)
if page == meta["pages"]:
break
page += 1
return all_recordsTest:
countries = ["USA", "TUR"]
indicators = ["FP.CPI.TOTL.ZG", "SI.POV.GINI"]
USA_TUR_indicators = fetch_indicators(indicators, countries, 100)
print("Total records:", len(USA_TUR_indicators))
print("First record:", USA_TUR_indicators[0])Total records: 260
First record: {'indicator': {'id': 'FP.CPI.TOTL.ZG', 'value': 'Inflation, consumer prices (annual %)'}, 'country': {'id': 'TR', 'value': 'Turkiye'}, 'countryiso3code': 'TUR', 'date': '2024', 'value': 58.5064507300343, 'unit': '', 'obs_status': '', 'decimal': 1}
Making the custom function more robust (checks + final tweaks)
def fetch_indicators(indicators: str | list, countries: str | list = "all", per_page=50):
if isinstance(indicators, list):
indicators = ";".join(indicators)
if isinstance(countries, list):
countries = ";".join(countries)
base_url = "https://api.worldbank.org/v2/country/{countries}/indicator/{indicators}"
url = base_url.format(countries=countries, indicators=indicators)
params = {"format": "json", "source": 2, "per_page": per_page}
all_records = []
page = 1
while True:
params["page"] = page
resp = requests.get(url, params=params, timeout=10) # sets a max wait per request so it won’t hang
resp.raise_for_status() # stop fast on HTTP 4xx/5xx
data = resp.json()
if not (isinstance(data, list) and len(data) >= 2):
break # unexpected shape, exit the loop
meta, records = data
if page == 1:
print("meta:", meta) # inspect first-call metadata
if not records:
break # no rows on this page -> break
all_records.extend(records)
pages = int(meta.get("pages", 0)) # safely pulls page count (defaults to 0 if missing)
if page >= pages: # makes sure the loop stops if we hit or pass the last page
break
page += 1
return all_recordsTest:
countries = ["USA", "TUR", "NLD"]
indicators = ["FP.CPI.TOTL.ZG", "SI.POV.GINI"]
all_records = fetch_indicators(indicators, countries)
print("Total records:", len(all_records))
print("First record:", all_records[0])meta: {'page': 1, 'pages': 8, 'per_page': 50, 'total': 390, 'sourceid': None, 'lastupdated': '2026-01-28'}
Total records: 390
First record: {'indicator': {'id': 'FP.CPI.TOTL.ZG', 'value': 'Inflation, consumer prices (annual %)'}, 'country': {'id': 'NL', 'value': 'Netherlands'}, 'countryiso3code': 'NLD', 'date': '2024', 'value': 3.34754304219953, 'unit': '', 'obs_status': '', 'decimal': 1}
Lastly, review the output in a clean dataframe format:
df = pd.DataFrame(all_records)
df = df[["countryiso3code", "date", "indicator", "value"]]
df = df.rename(columns={"countryiso3code": "country"})
df["indicator"] = df["indicator"].str.get("id") # the indicator column itself was a dictionary; only the indicator id is extracted
df| country | date | indicator | value | |
|---|---|---|---|---|
| 0 | NLD | 2024 | FP.CPI.TOTL.ZG | 3.347543 |
| 1 | NLD | 2023 | FP.CPI.TOTL.ZG | 3.838394 |
| 2 | NLD | 2022 | FP.CPI.TOTL.ZG | 10.001208 |
| 3 | NLD | 2021 | FP.CPI.TOTL.ZG | 2.675720 |
| 4 | NLD | 2020 | FP.CPI.TOTL.ZG | 1.272460 |
| ... | ... | ... | ... | ... |
| 385 | USA | 1964 | SI.POV.GINI | 37.400000 |
| 386 | USA | 1963 | SI.POV.GINI | 36.700000 |
| 387 | USA | 1962 | SI.POV.GINI | NaN |
| 388 | USA | 1961 | SI.POV.GINI | NaN |
| 389 | USA | 1960 | SI.POV.GINI | NaN |
390 rows × 4 columns