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.

[{
  "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"}
    ]
  }]
]

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_records

Test:

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_records

Test:

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_records

Test:

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

Resources