Skip to content
worldgovdata
Documentation / 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.code is carried through so your caller can branch on insufficient_credits rather than on a string match against a message.
  • Retries the right things. 429, 5xx and transport failures, with Retry-After honoured, exponential fallback, a cap and jitter. Every other 4xx fails immediately, because retrying it cannot help.
  • Pages the catalogue. limit/offset against meta.total, with an empty-page guard so the loop cannot spin.
  • Chunks the data endpoint. /v1/data has 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

What each client needs and how to run it
LanguageRequiresSave asRun
cURLbash 3.2+, curl, jq, awkwgd.shsource ./wgd.sh
JavaScriptNode 18+ — no dependencieswgd.mjsnode wgd.mjs
PythonPython 3.9+, requestswgd.pypython wgd.py
RR 4.1+ (native pipe), httr2wgd.Rsource("wgd.R")
GoGo 1.18+ — standard library onlywgd.gogo run wgd.go

All five read this

export WGD_API_KEY="wgd_live_…"

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
fi

The 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.sh and you have wgd_get, wgd_indicators_all and wgd_data_all in your shell.
  • jq is required. Parsing JSON with grep and sed is where shell scripts go to die.
  • Retries treat an empty status code — no response at all — as transient, alongside 429 and 5xx.
  • set -euo pipefail is 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 your package.json — the file uses top-level await.
  • 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.
  • WgdError extends Error and carries status, code and the parsed body.
  • 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. A Session is used throughout so the TLS connection is reused across a chunked pull.
  • from is 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() and data() are generators, so a large pull streams rather than materialising. Wrap them in list() if you want it all at once.
  • WgdError subclasses RuntimeError and carries status, code and body.

R

  • install.packages("httr2"), and R 4.1 or later for the native |> pipe. httr2 owns the retry loop — req_retry() reads Retry-After itself and only falls back to the backoff function when the header is absent.
  • is_transient is widened deliberately. httr2’s default covers 429 and 503 only, which would leave a 502 from a load balancer unretried.
  • req_error(body = …) surfaces our error.code in the R condition message, so a failure reads insufficient_credits: … rather than HTTP 402.
  • Results come back as nested lists. do.call(rbind, lapply(rows, as.data.frame)) is the usual next step, or tibble::tibble if you are in the tidyverse.

Go

  • Standard library only — no go.mod dependencies. Save it, go run wgd.go. Go 1.18 or later, for any.
  • Get decodes into a caller-supplied value rather than returning any, so you keep your types. Indicator and Observation are starting points; unknown JSON keys are ignored, so add fields freely.
  • Observation.Value, CILo and CIHi are *float64. That is not fussiness: a null measurement and a measurement of zero are different facts, and a bare float64 silently 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.
  • *APIError implements error; use errors.As to reach Code.

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.licence and meta.attribution arrive 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.