Documentation / Reference
Getting started
Reference
Reference
Client libraries
There is no SDK to install. The API is small enough that a complete client fits on one screen — so here is that client, written out properly, in five languages.
Each of these is a whole file. Copy it, set WGD_API_KEY, run it. Nothing is elided and nothing is left as an exercise.
What each client does
The same five things, because these are the five things every real integration ends up needing and the five things people most often get subtly wrong.
- Authenticates from the environment. Reads
WGD_API_KEY. No key ever appears in the source. - Raises a typed error. The API’s
error.codeis carried through so your caller can branch oninsufficient_creditsrather than on a string match against a message. - Retries the right things. 429, 5xx and transport failures, with
Retry-Afterhonoured, exponential fallback, a cap and jitter. Every other 4xx fails immediately, because retrying it cannot help. - Pages the catalogue.
limit/offsetagainstmeta.total, with an empty-page guard so the loop cannot spin. - Chunks the data endpoint.
/v1/datahas no pagination and refuses anything over 100,000 rows, so the country list is the unit of chunking.
Base URL is https://api.worldgovdata.com/v1 in all five, hardcoded as a constant you can override.
Before you run one
| Language | Requires | Save as | Run |
|---|---|---|---|
| cURL | bash 3.2+, curl, jq, awk | wgd.sh | source ./wgd.sh |
| JavaScript | Node 18+ — no dependencies | wgd.mjs | node wgd.mjs |
| Python | Python 3.9+, requests | wgd.py | python wgd.py |
| R | R 4.1+ (native pipe), httr2 | wgd.R | source("wgd.R") |
| Go | Go 1.18+ — standard library only | wgd.go | go run wgd.go |
All five read this
Get a key from your account, or read the quickstart if you have not made one yet.
The clients
Complete client
#!/usr/bin/env bash
#
# worldgovdata shell client.
# Requires: curl, jq, awk (all standard).
# Usage: export WGD_API_KEY=wgd_live_... && source ./wgd.sh
#
set -euo pipefail
WGD_API="${WGD_API:-https://api.worldgovdata.com/v1}"
: "${WGD_API_KEY:?export WGD_API_KEY=wgd_live_... first}"
# --------------------------------------------------------------- one request
# wgd_get <path-with-query> -> JSON body on stdout, non-zero on failure.
# Retries 429, 5xx and transport failures; Retry-After wins when present.
wgd_get() {
local path="$1" attempt=0 max=5
local headers body code wait
while :; do
headers="$(mktemp)"; body="$(mktemp)"
code="$(curl -sS -D "$headers" -o "$body" -w '%{http_code}' \
-H "Authorization: Bearer $WGD_API_KEY" \
-H 'Accept: application/json' \
"${WGD_API}${path}")" || code="000"
if [ "$code" -ge 200 ] && [ "$code" -lt 300 ]; then
cat "$body"
rm -f "$headers" "$body"
return 0
fi
# 429, 5xx and "no response at all" are worth another try. Every other 4xx
# is a bug in the request and will fail identically forever.
if { [ "$code" = "000" ] || [ "$code" = "429" ] || [ "$code" -ge 500 ]; } \
&& [ "$attempt" -lt "$max" ]; then
wait="$(awk 'tolower($1)=="retry-after:"{gsub(/\r/,"",$2); print $2; exit}' "$headers")"
[ -n "$wait" ] || wait="$(( 2 ** attempt ))"
sleep "$(awk -v w="$wait" 'BEGIN{srand(); print w + rand()}')"
attempt=$(( attempt + 1 ))
rm -f "$headers" "$body"
continue
fi
echo "wgd: HTTP $code $(jq -r '.error.code // "-"' <"$body" 2>/dev/null)" >&2
jq -r '.error.message // empty' <"$body" >&2 2>/dev/null || cat "$body" >&2
rm -f "$headers" "$body"
return 1
done
}
# ------------------------------------------------------- paged: /indicators
# Every matching indicator, one compact JSON object per line.
# wgd_indicators_all 'topic=governance&sort=coverage'
wgd_indicators_all() {
local query="${1:-}" limit=500 offset=0 total=1 page
while [ "$offset" -lt "$total" ]; do
page="$(wgd_get "/indicators?limit=${limit}&offset=${offset}${query:+&$query}")"
total="$(printf '%s' "$page" | jq -r '.meta.total // 0')"
printf '%s' "$page" | jq -c '.data[]'
offset=$(( offset + limit ))
done
}
# ------------------------------------------------------- chunked: /data
# /v1/data has no limit/offset and refuses anything over 100,000 rows, so the
# unit of chunking is the country list.
# wgd_data_all wdi.gdp_per_capita_ppp IND CHN USA DEU JPN
wgd_data_all() {
local indicator="$1"; shift
local chunk=() iso
_flush() {
[ "${#chunk[@]}" -gt 0 ] || return 0
wgd_get "/data?indicator=${indicator}&countries=$(IFS=,; echo "${chunk[*]}")" \
| jq -c '.data[]'
chunk=()
}
for iso in "$@"; do
chunk+=("$iso")
[ "${#chunk[@]}" -ge 25 ] && _flush
done
_flush
}
# ------------------------------------------------------------------ example
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
wgd_get /ping | jq .
wgd_data_all wdi.gdp_per_capita_ppp IND CHN USA DEU JPN | head -5
fiThe language switcher is shared across every sample on this site, so choosing one here keeps every other page in your language too.
Language notes
cURL
- Written as a sourceable shell library, not a one-liner:
source ./wgd.shand you havewgd_get,wgd_indicators_allandwgd_data_allin your shell. jqis required. Parsing JSON withgrepandsedis where shell scripts go to die.- Retries treat an empty status code — no response at all — as transient, alongside 429 and 5xx.
set -euo pipefailis deliberate: a failure in the middle of a chunked pull should stop, not carry on writing a half file.
JavaScript
- Node 18 or later for global
fetch. No dependencies at all. Save as.mjs, or set"type": "module"in yourpackage.json— the file uses top-levelawait. - The retry path drains the response body before sleeping. Skip that and Node’s HTTP agent holds the socket open until it times out, which shows up much later as mysterious connection exhaustion.
WgdErrorextendsErrorand carriesstatus,codeand the parsedbody.- It runs in Deno and Bun unchanged. It will run in a browser too — do not do that: the key would be in the bundle. See Authentication.
Python
pip install requests. ASessionis used throughout so the TLS connection is reused across a chunked pull.fromis a Python keyword, so it cannot be a keyword argument. Pass the year bounds as a dict:client.data(indicator, codes, **{"from": 2000, "to": 2024}). This catches everyone once.indicators()anddata()are generators, so a large pull streams rather than materialising. Wrap them inlist()if you want it all at once.WgdErrorsubclassesRuntimeErrorand carriesstatus,codeandbody.
R
install.packages("httr2"), and R 4.1 or later for the native|>pipe. httr2 owns the retry loop —req_retry()readsRetry-Afteritself and only falls back to the backoff function when the header is absent.is_transientis widened deliberately. httr2’s default covers 429 and 503 only, which would leave a 502 from a load balancer unretried.req_error(body = …)surfaces ourerror.codein the R condition message, so a failure readsinsufficient_credits: …rather thanHTTP 402.- Results come back as nested lists.
do.call(rbind, lapply(rows, as.data.frame))is the usual next step, ortibble::tibbleif you are in the tidyverse.
Go
- Standard library only — no
go.moddependencies. Save it,go run wgd.go. Go 1.18 or later, forany. Getdecodes into a caller-supplied value rather than returningany, so you keep your types.IndicatorandObservationare starting points; unknown JSON keys are ignored, so add fields freely.Observation.Value,CILoandCIHiare*float64. That is not fussiness: a null measurement and a measurement of zero are different facts, and a barefloat64silently conflates them.- Every method takes a
context.Context, and the backoff sleep respects cancellation — so a timeout during a long chunked pull actually stops it. *APIErrorimplementserror; useerrors.Asto reachCode.
Adapting them
- Log the request id. Every response carries
X-Request-Id. Recording it turns a vague report into a line we can look up. - Watch
X-Credits-Remaining. It is already in the response. Alert on it rather than discovering a 402 at three in the morning. - Handle 402 as a stop, not a retry. None of these clients retries it, deliberately. Record which chunk you reached and resume there after topping up.
- Bound your concurrency. If you parallelise the chunk loop, cap the pool at your tier’s concurrency number — Rate limits.
- Keep the licence fields.
meta.licenceandmeta.attributionarrive with every data response. Persist them alongside the rows — Licensing.
Or generate one
If you would rather not hand-roll, the API publishes an OpenAPI schema you can point a generator at. It has one quirk worth knowing about before you do — OpenAPI covers it.