Research and public data

restcountries.com

REST Countries - Scraping & Data Extraction

Notes an AI agent reads before it works on restcountries.com: what to open, what to call, what breaks. Written by browser-harness contributors after real runs, and kept on GitHub under the MIT license.

Give it to your agent

Any coding agent

Start the task with this line. The Markdown version is the same notes without the page around them.

Read https://browser-use.com/skills/rest-countries.md, then <your task>

Browser Harness

Save the files into your workspace and turn domain skills on. The code below calls its helpers, such as http_get and js. What Browser Harness is.

d="${BH_AGENT_WORKSPACE:-$HOME/.config/browser-harness/agent-workspace}/domain-skills/rest-countries"
mkdir -p "$d" && cd "$d"
curl -sLO https://raw.githubusercontent.com/browser-use/browser-harness/main/agent-workspace/domain-skills/rest-countries/scraping.md
export BH_DOMAIN_SKILLS=1

https://restcountries.com - open JSON API for country data. Never use the browser. All data is reachable via http_get. No auth required, no API key.

Do this first

Fetch all 250 countries in one call with a field filter - almost always the fastest approach.

import json
from helpers import http_get
 
data = http_get("https://restcountries.com/v3.1/all?fields=name,cca2,capital,population,area,region")
countries = json.loads(data)
# countries is a list of 250 dicts — confirmed 2026-04-18
 
for c in countries:
    name       = c["name"]["common"]          # "Germany"
    official   = c["name"]["official"]        # "Federal Republic of Germany"
    code       = c["cca2"]                    # "DE"
    capital    = c["capital"][0] if c.get("capital") else None   # list — may be empty
    population = c["population"]              # 83491249
    area       = c["area"]                    # 357114.0 (km²)
    region     = c["region"]                  # "Europe"
    print(code, name, population)
# Confirmed output (first result varies — API returns unsorted):
# CI Ivory Coast 31719275

Use the ?fields= query param to limit response size - essential when fetching all 250.

Common workflows

Lookup a single country by code (ISO 3166-1 alpha-2 or alpha-3)

import json
from helpers import http_get
 
# Single code — returns a list (one element)
data = http_get("https://restcountries.com/v3.1/alpha/DE")
country = json.loads(data)[0]
 
# But: /alpha/CODE?fields=... returns a plain dict, not a list — watch for this
data2 = http_get("https://restcountries.com/v3.1/alpha/DE?fields=name,cca2,currencies,languages,flags")
country2 = json.loads(data2)          # dict, NOT list
 
name       = country2["name"]["common"]                             # "Germany"
currencies = country2["currencies"]                                 # {"EUR": {"name": "euro", "symbol": "€"}}
currency_codes = list(currencies.keys())                            # ["EUR"]
currency_name  = currencies["EUR"]["name"]                          # "euro"
languages  = country2["languages"]                                  # {"deu": "German"}
lang_names = list(languages.values())                               # ["German"]
flag_png   = country2["flags"]["png"]                               # "https://flagcdn.com/w320/de.png"
flag_svg   = country2["flags"]["svg"]                               # "https://flagcdn.com/de.svg"
flag_alt   = country2["flags"]["alt"]                               # description text
 
print(name, currency_codes, lang_names)
# Confirmed: Germany ['EUR'] ['German']

Batch lookup - multiple codes in one call

Use /alpha?codes= for fetching a known list of countries - always returns a list.

import json
from helpers import http_get
 
codes = ["US", "GB", "FR", "DE", "JP", "CN", "IN", "BR", "AU", "CA"]
data = http_get(f"https://restcountries.com/v3.1/alpha?codes={','.join(codes)}&fields=name,cca2,population")
countries = json.loads(data)
# Returns list, order NOT guaranteed to match input order
for c in countries:
    print(c["cca2"], c["name"]["common"], c["population"])
# Confirmed: 10 results, returned in arbitrary order

Search by name

import json
from helpers import http_get
 
# Partial match (default) — may return multiple results
data = http_get("https://restcountries.com/v3.1/name/united")
results = json.loads(data)
# Returns 7 countries: United States, UK, UAE, Tanzania, Mexico, ...
 
# Exact match — use fullText=true with the full common or official name
data2 = http_get("https://restcountries.com/v3.1/name/united%20kingdom?fullText=true")
results2 = json.loads(data2)
# Returns exactly 1 result: United Kingdom
print(results2[0]["name"]["common"])  # United Kingdom

Filter by region

import json
from helpers import http_get
 
data = http_get("https://restcountries.com/v3.1/region/europe?fields=name,cca2,population")
countries = json.loads(data)
# 53 European countries — confirmed
 
# Sort by population
ranked = sorted(countries, key=lambda x: x["population"], reverse=True)
for c in ranked[:5]:
    print(c["cca2"], c["name"]["common"], f"{c['population']:,}")
# Confirmed top 5: RU Russia, DE Germany, FR France, GB United Kingdom, IT Italy

Valid region values: africa, americas, asia, europe, oceania, antarctic

Filter by subregion

import json
from helpers import http_get
 
data = http_get("https://restcountries.com/v3.1/subregion/Western%20Europe?fields=name,cca2")
countries = json.loads(data)
print([c["cca2"] for c in countries])
# Confirmed: ['FR', 'NL', 'MC', 'DE', 'BE', 'LI', 'CH', 'LU']

Filter by language

import json
from helpers import http_get
 
data = http_get("https://restcountries.com/v3.1/lang/arabic")
countries = json.loads(data)
print(f"Arabic-speaking countries: {len(countries)}")
# Confirmed: 25 countries
 
# Language param is the language name (English), not the ISO 639-3 code
# Works: arabic, french, spanish, english, portuguese, german, russian, chinese

Filter by currency

import json
from helpers import http_get
 
data = http_get("https://restcountries.com/v3.1/currency/EUR")
countries = json.loads(data)
print(f"EUR countries: {len(countries)}")  # Confirmed: 36
names = [c["name"]["common"] for c in countries]
print(names[:5])
 
# Use ISO 4217 currency code (uppercase)

Filter by capital city

import json
from helpers import http_get
 
data = http_get("https://restcountries.com/v3.1/capital/berlin?fields=name,cca2,capital")
result = json.loads(data)
print(result[0]["name"]["common"], result[0]["capital"])
# Confirmed: Germany ['Berlin']
# Capital param is case-insensitive

Full country detail - all fields

import json
from helpers import http_get
 
data = http_get("https://restcountries.com/v3.1/alpha/US")
c = json.loads(data)[0]
 
# Available top-level keys (confirmed for US/DE):
# name, tld, cca2, ccn3, cca3, cioc, independent, status, unMember,
# currencies, idd, capital, altSpellings, region, subregion, languages,
# translations, latlng, landlocked, borders, area, demonyms, flag (emoji),
# maps, population, gini, fifa, car, timezones, continents, flags,
# coatOfArms, startOfWeek, capitalInfo, postalCode
 
print(c["idd"])          # {"root": "+1", "suffixes": ["201", "202", ...]}
print(c["car"]["side"])  # "right" or "left"
print(c["gini"])         # {"2018": 41.4}  — year keyed, may be absent
print(c["timezones"])    # list of UTC offset strings
print(c["borders"])      # list of cca3 codes for bordering countries
print(c["latlng"])       # [lat, lng] of geographic center

URL reference

EndpointPatternNotes
All countries/v3.1/allAlways add ?fields=
By code/v3.1/alpha/{code}cca2 or cca3; single code → list (no fields) or dict (with fields)
By codes/v3.1/alpha?codes=DE,FR,JPAlways returns list
By name/v3.1/name/{name}Partial; add ?fullText=true for exact match
By region/v3.1/region/{region}africa, americas, asia, europe, oceania, antarctic
By subregion/v3.1/subregion/{subregion}URL-encode spaces as %20
By language/v3.1/lang/{language}English language name
By currency/v3.1/currency/{code}ISO 4217 (EUR, USD, GBP)
By capital/v3.1/capital/{city}Case-insensitive

All endpoints accept ?fields=field1,field2,... to limit response payload.

Gotchas

  • name is a nested object, not a string. Use c["name"]["common"] for the familiar English name, c["name"]["official"] for the full official name. nativeName is a dict keyed by ISO 639-3 language code.

  • /alpha/CODE return type depends on whether ?fields= is present. Without ?fields=, returns a list (one element). With ?fields=..., returns a plain dict. Use json.loads(data)[0] for the no-fields case, json.loads(data) for the fields case. Using /alpha?codes=CODE always returns a list regardless.

  • currencies is a dict keyed by ISO 4217 code, not a list. c["currencies"]["EUR"]["name"] → "euro", c["currencies"]["EUR"]["symbol"] → "€". A country can have multiple currencies - iterate currencies.items().

  • languages is a dict keyed by ISO 639-3 code. c["languages"]["deu"] → "German". Use list(c["languages"].values()) for a simple list of language names.

  • capital is a list and may be empty. Some territories (Antarctica, Bouvet Island, Macau, Heard Island) have no capital - c.get("capital") returns [], not None. Guard with c["capital"][0] if c.get("capital") else None. South Africa has 3 capitals.

  • gini is a dict keyed by year string, may be absent entirely. c["gini"] → {"2016": 31.9}. Many small countries or territories have no gini data - always check c.get("gini").

  • borders uses cca3 codes, not cca2. c["borders"] → ["AUT", "BEL", ...]. Cross-reference with another /alpha?codes= call to resolve to names.

  • translations covers ~45 languages. Each entry: c["translations"]["deu"] → {"official": "Bundesrepublik Deutschland", "common": "Deutschland"}. Useful for multilingual apps.

  • No rate limit headers, no documented rate limit. In practice the API handles rapid sequential calls fine. For bulk crawling hundreds of per-country requests, add a short sleep (time.sleep(0.5)) between calls to be polite.

  • 404 returns JSON, not HTML. {"message": "Not Found", "status": 404}. Wrap calls in try/except and check for this pattern when handling user-supplied country names or codes.

  • ?fields= is the key performance lever. The full all-countries payload without field filtering is ~1.5 MB. With ?fields=name,cca2,population it drops to ~50 KB. Always filter when you don't need all fields.

scraping.md · updated Apr 28, 2026 · 1,208 words

Sites change. These notes describe what worked when they were last updated, not a guarantee they still do. If something broke, the fix is a pull request to browser-harness.