Documentation / Core concepts
Getting started
Reference
Core concepts
Pagination
Only one endpoint paginates. The one that returns observations does not — it has a hard row cap instead, and a different technique for getting past it.
/v1/indicators takes limit and offset. /v1/data takes neither and refuses anything over 100,000 rows. Both are covered below.
Which endpoints paginate
/v1/indicators— the only paged endpoint.limit,offset, andmeta.total./v1/data— no paging. Returns everything that matches, or refuses if that is over 100,000 rows. You narrow the query instead.- Everything else — bounded by nature. The country spine is a few hundred rows; a ranking is one row per country; topics is thirteen rows. They return the lot.
limit and offset
On /v1/indicators
| Parameter | Description |
|---|---|
limitinteger, 1–500 | Page size. Omit it and you get every match in one response — which is the right choice for a narrow filter and the wrong one for the whole catalogue.default: none (all matches) · example: 500 |
offsetinteger, ≥ 0 | Rows to skip. Applied after the sort, alongside limit. 0 and an omitted value mean the same thing.default: 0 · example: 500 |
Both are validated: a limit above 500 or a negative offset returns 400 bad_request with a details array naming the parameter. Paging is free — /v1/indicators costs no credits at any page count.
Walking the catalogue
meta.total, and when it lies
meta.count is how many rows are in this response. meta.total is how many match the filter overall. Loop until you have collected total rows.
The paging fields
- On an unpaged response,
totalequalscount. The full count is only computed when the response is actually paged. That is not a bug to work around — if you did not page, you have everything, so the two numbers are the same fact. limitis not echoed inmeta.offsetis; the page size is not. Track your own page size rather than reading it back.offsetis omitted when it was 0.metadrops every field you did not send, so an absent key means “not applied”.
Walking every page
Two exit conditions, not one: the authoritative total, and an empty page. The second matters because the catalogue can change size between your first request and your last, and a loop that only trusts total can spin.
Fetch all indicators
# Every governance indicator, one JSON object per line. Requires jq.
# /v1/indicators is free, so this costs nothing however many pages it takes.
limit=500; offset=0; total=1
while [ "$offset" -lt "$total" ]; do
page="$(curl -sS "https://api.worldgovdata.com/v1/indicators?topic=governance&limit=$limit&offset=$offset")"
total="$(printf '%s' "$page" | jq -r '.meta.total // 0')"
printf '%s' "$page" | jq -c '.data[]'
offset=$(( offset + limit ))
doneComplete clients with this built in, in all five languages, are on Client libraries.
The row cap on /v1/data
A single /v1/data request will not return more than 100,000 rows. Over that, it refuses — and tells you the real count so you know how far over you are.
too_many_rows · 400
- It costs nothing. The count runs before any data is read, so an over-cap request is refused for free. Probing the size of a query this way is legitimate.
- It is not a paging hint. There is no page 2 to ask for. You narrow the query — fewer countries, or a shorter year range — and issue it again.
- Most queries never see it. One indicator across every country and every year it holds is usually well under the cap. It bites on the deep-history series, some of which carry centuries.
Fetching a full panel
The technique is to split the query on the axes it has: the country list, and the year range. Countries first — rows are roughly uniform per country, so a country slice is predictable — with years as the fallback for a single country whose history still will not fit.
Plan the chunk size
The upper bound on a query is one row per country-year. So the largest safe chunk is:
The arithmetic
That is an upper bound, so the estimate is conservative and the first attempt almost always succeeds. The recipe below still handles the case where it does not, which is what makes it correct rather than merely usually right.
Do the free work first
You need the country list and the year span before you can plan, and both are free: /v1/countries for the codes and /v1/indicators/{id} for coverage.yr_min and coverage.latest_year. Using latest_year rather than yr_max is deliberate: yr_max can include projection years a few series carry, and paying for forecasts you did not want is an avoidable surprise.
The recipe
Split on countries; if a single-country chunk is still over the cap, split its years; only give up when one country in one year cannot fit, which cannot happen. Every other error code — 402, 403, 404 — is not splittable and is re-raised immediately rather than triggering a pointless bisection.
Fetch a full panel
from typing import Iterator, Sequence
MAX_ROWS = 100000 # the API's hard cap on one /v1/data request
def plan_chunk(n_countries: int, year_from: int, year_to: int) -> int:
"""Largest country-chunk that cannot exceed the cap.
The upper bound is one row per country-year, so a chunk of N countries holds
at most N * years rows. Solve N * years <= MAX_ROWS.
"""
years = max(1, year_to - year_from + 1)
return max(1, min(n_countries, MAX_ROWS // years))
def fetch_panel(
client, # the Wgd client from /docs/libraries
indicator: str,
countries: Sequence[str],
year_from: int,
year_to: int,
) -> Iterator[dict]:
"""Every observation for one indicator across many countries.
Splits on countries first and on years only when a single country still will
not fit. Each chunk is billed on the rows it actually returned, and a chunk
that raises costs nothing.
"""
size = plan_chunk(len(countries), year_from, year_to)
queue: list[tuple[Sequence[str], int, int]] = [
(countries[i : i + size], year_from, year_to)
for i in range(0, len(countries), size)
]
while queue:
group, lo, hi = queue.pop(0)
try:
page = client.get(
"/data",
indicator=indicator,
countries=",".join(group),
**{"from": lo, "to": hi},
)
except WgdError as err:
if err.code != "too_many_rows":
raise # 402, 403 and 404 are not splittable
if len(group) > 1: # split on countries first
mid = len(group) // 2
queue[0:0] = [(group[:mid], lo, hi), (group[mid:], lo, hi)]
elif hi > lo: # one country: split on years
mid = lo + (hi - lo) // 2
queue[0:0] = [(group, lo, mid), (group, mid + 1, hi)]
else:
raise # one country, one year, still over
continue
yield from page["data"]
# ---------------------------------------------------------------------- usage
client = Wgd()
# Both of these are free: resolve the shape of the job before paying for any of it.
codes = [c["iso3"] for c in client.get("/countries")["data"]]
coverage = client.get("/indicators/wdi.gdp_per_capita_ppp")["data"]["coverage"]
rows = list(
fetch_panel(
client,
"wdi.gdp_per_capita_ppp",
codes,
coverage["yr_min"],
coverage["latest_year"],
)
)
print(len(rows), "observations")What chunking costs
Each request rounds up to a whole credit independently, so chunking is very slightly more expensive than one large request: nine chunks of about 1,450 rows cost 135 credits where a single 13,000-row request would have cost 130. That is the price of a job that can resume, and it scales with the number of chunks — which is a reason to use the largest chunk that clears the cap, not the smallest that feels tidy.
The other reason to keep chunk counts low is the rate limit: every chunk is a request against your per-minute budget, whereas the credit cost barely moves. One credit is 100 rows however you slice them.
Two things to try before chunking
- Ask for CSV.
format=csvstreams rather than buffering a JSON array, which is usually the real constraint on a large pull. The row cap still applies, but your process stops being the bottleneck. - Ask whether you want a ranking instead. If what you need is every country for one year, /v1/rankings gives you exactly that for a flat 1 credit, already ordered, with the rank attached.