Narwhal API indexes 1,489 annual World Bank WDI indicators under CC BY 4.0. To get World Bank data in Python, search for an indicator, add a lowercase country code to its ID template, and request observations with your API key. The response includes periods, decimal-string values, attribution and a licence URL. Your plan determines the history window and daily allowance for uncached fetches.
Do it with the API
- Get a free key and prepare Python Create an account at signup, get your API key, and set
NARWHAL_API_KEYin the shell where you will run Python. Replace the placeholder with your key. Installrequestsfor HTTP calls andpandasfor the DataFrame. Each request uses the key in the Authorization header; the complete script in the final step reads it from the environment.python -m pip install requests pandas export NARWHAL_API_KEY="replace-with-your-key" - Search for an indicator Search the catalogue for
gdp per capitawithsource=wdi. Print each result'sid_templateand title, then choose the indicator you need. Catalogue search is open on every plan and calls no source. Theparamsargument handles spaces in the search phrase. Search results describe indicators; a country belongs in the series ID you build next.import os import requests BASE_URL = "https://api.narwhalapi.com" headers = { "Authorization": f"Bearer {os.environ['NARWHAL_API_KEY']}" } response = requests.get( f"{BASE_URL}/v1/economics/catalogue", headers=headers, params={"q": "gdp per capita", "source": "wdi"}, timeout=10, ) response.raise_for_status() catalogue = response.json() for hit in catalogue["results"]: print(hit["id_template"], hit["title"]) - Add a country to the template Replace
{country}in your chosen template with a lowercase ISO 3166-1 alpha-3 code. Indonesia usesidn. A World Bank ID has the formwdi:<indicator>/<iso3>, including the slash between the indicator and country. Pass the complete ID through theidquery parameter. The snippet selects the first search result; review its printed title before you use it.if not catalogue["results"]: raise SystemExit("No matching indicators found.") selected = catalogue["results"][0] series_id = selected["id_template"].replace("{country}", "idn") print(series_id) - Read annual observations Request observations for that ID. Annual periods use
YYYY; optionalstartandendbounds are inclusive. The response limit accepts up to 1,000 observations, with a default of 100. Ifnext_cursorhas a value, pass it ascursorto continue. A larger limit does not extend your plan's history window, and missing years remain absent from the response.response = requests.get( f"{BASE_URL}/v1/economics/catalogue/series/observations", headers=headers, params={"id": series_id, "limit": 1000}, timeout=10, ) response.raise_for_status() observations = response.json()["observations"] - Create a DataFrame Build a DataFrame from the observations, convert
periodto an integer, and convertvalueto a float. Values arrive as decimal strings. UseDecimalinstead offloat()when you want decimal arithmetic. Keep the response's unit label alongside your analysis: an indicator's title describes what its values measure. The conversion does not fill gaps or create observations for missing years.import pandas as pd frame = pd.DataFrame(observations, columns=["period", "value"]) frame["period"] = frame["period"].astype(int) frame["value"] = frame["value"].map(float) print(frame) - Batch countries and handle request errors Run this complete script for the worked GDP series and a latest-value batch for Indonesia and Canada. Join IDs with commas; the latest endpoint accepts 1 to 50 distinct IDs. It charges one call per series returned, in one step. The script prints each country's period and value, reports missing IDs, and stops with an error message for 400, 404 or
429 fetch_limit_reachedresponses.import os import requests import pandas as pd BASE_URL = "https://api.narwhalapi.com" HEADERS = { "Authorization": f"Bearer {os.environ['NARWHAL_API_KEY']}" } def get_json(path, params): response = requests.get( f"{BASE_URL}{path}", headers=HEADERS, params=params, timeout=10, ) try: response.raise_for_status() except requests.HTTPError: problem = response.json() code = problem.get("code", "unknown_error") if response.status_code == 429 and code == "fetch_limit_reached": retry_after = response.headers.get("Retry-After") raise SystemExit( f"Uncached fetch allowance used. Retry-After: {retry_after}" ) if response.status_code in (400, 404): raise SystemExit(f"{code}: {problem.get('detail', '')}") raise return response.json() series_id = "wdi:ny.gdp.mktp.cd/idn" rows = [] params = {"id": series_id, "limit": 1000} while True: payload = get_json( "/v1/economics/catalogue/series/observations", params ) rows.extend(payload["observations"]) cursor = payload.get("next_cursor") if not cursor: break params["cursor"] = cursor frame = pd.DataFrame(rows, columns=["period", "value"]) frame["period"] = frame["period"].astype(int) frame["value"] = frame["value"].map(float) print(frame) countries = ("idn", "can") ids = [f"wdi:ny.gdp.mktp.cd/{country}" for country in countries] batch = get_json( "/v1/economics/catalogue/latest", {"ids": ",".join(ids)}, ) for observation in batch["observations"]: country = observation["id"].rsplit("/", 1)[1] print(country, observation["period"], observation["value"]) for missing in batch["missing"]: print("Missing:", missing)
How do I read the Indonesia GDP example?
The series wdi:ny.gdp.mktp.cd/idn is Indonesia's GDP in current US dollars. A Pro-key request on 27 October 2026 with limit=3 returned these observations. This example uses total GDP; use the template from your catalogue search when you want GDP per capita.
[
{"period": "1967", "value": "5667756627.92093"},
{"period": "1968", "value": "7076465299.81141"},
{"period": "1969", "value": "8337423312.88344"}
]For 1967, float("5667756627.92093") converts the value for numerical work in your DataFrame. Preserve the original string if you also need its decimal representation. Responses include World Bank WDI attribution and the CC BY 4.0 licence URL. Keep that attribution and licence information with the data you use.
Why does my Free key return only a few years?
Free has a 12-month history window, with an annual minimum of the latest 5 observations. Narwhal applies whichever gives you more observations: the plan window or that minimum. Annual series therefore retain several observations even when most fall outside the ordinary window. The annual minimum applies on every plan.
Which limit applies on my plan:
| Plan | Uncached fetches per account per UTC day | History window |
|---|---|---|
| Free | 10 | Last 12 months, with at least the latest 5 annual observations |
| Basic | 100 | Last 5 years, with at least the latest 5 annual observations |
| Pro | 1,000 | Full history |
A range wholly before your allowed window returns 403 plan_upgrade_required. For annual series, Narwhal checks that range after loading the series so it can account for the minimum. Use the plan details to choose the history window your analysis needs.
What does the 429 fetch_limit_reached error mean?
Your account has used its daily allowance and the request needs an uncached fetch. Read Retry-After before retrying; the allowance resets at 00:00 UTC. A cached series still answers after you reach the cap. World Bank cache durations range from 24 hours to 7 days, so another request does not necessarily require a fetch.
For 400 invalid_series_id, check the indicator, slash and lowercase country code. 400 invalid_query means the query is invalid. A 404 not_found can mean the index does not know the indicator; Narwhal remembers that result for 24 hours. Check the ID against your catalogue result before repeating the request.
Stale answers and 400, 404 and 503 responses are uncharged. If the source is unavailable and no cached copy exists, Narwhal returns 503 upstream_busy with Retry-After. See caching and 429 handling for retry handling.
Published .
Next steps
- API referenceAny-series API reference
- World Bank data API
- How-toCache responses and handle 429 errors
- How-toCompare inflation between countries
Create a free key and call the API from your own code. Get a free API key
