Plainwork

Plainwork / Countrydrop: REST Countries v3.1 drop-in API / Countrydrop API reference: REST Countries v2 and v3.1

Countrydrop API reference

The REST Countries v2 and v3.1 endpoints, with the same paths, parameters and JSON. Base URL: https://pw-d20261017a29.plainwork-apps.workers.dev. Put it where https://restcountries.com was. The machine-readable description is at /openapi.json (OpenAPI 3.1).

Try it

Examples: /v3.1/alpha/de · /v3.1/all?fields=name,cca2,flags · /v3.1/currency/eur?fields=name · /v2/alpha/fr · /v3.1/alpha/x (shows a 400). Each request counts toward your free 1,000 a day.

Switching from restcountries.com

// before (now returns "This API version has been deprecated")
fetch("https://restcountries.com/v3.1/all?fields=name,flags")

// after: same path, same JSON
fetch("https://pw-d20261017a29.plainwork-apps.workers.dev/v3.1/all?fields=name,flags")

Old https://restcountries.eu/rest/v2/… URLs work too: keep the path and change the host (see restcountries.eu paths).

v3.1 endpoints

PathReturns
/v3.1/all or /v3.1/all?fields=a,bEvery country (250). Without fields you get every field, about 860 KB (v2: about 280 KB) before compression, which is how restcountries.com behaved until June 2025. It then started demanding 1 to 10 fields, so tutorials that call /all on its own work here again. Ask only for the fields you use.
/v3.1/alpha/{code}Matches on cca2, ccn3, cca3 or cioc: de, 276, deu, ger. It returns an array, or one object when you add ?fields=, which is also how the original behaved.
/v3.1/alpha?codes=de,fr,itSeveral codes, as an array.
/v3.1/name/{name}Name contains the text (common, official or an alternative spelling). Add ?fullText=true for an exact common or official name.
/v3.1/currency/{code or name}eur or euro.
/v3.1/capital/{capital}Capital contains the text; accents are ignored, so bogota finds Bogotá.
/v3.1/region/{region}, /v3.1/subregion/{subregion}europe, northern europe.
/v3.1/lang/{code or name}spa or spanish.
/v3.1/demonym/{demonym}, /v3.1/translation/{name}Demonym or any translated name contains the text, for example peruvian or alemania.
/v3.1/independent?status=true|falseIndependent countries (the default) or the others.

Every v3.1 endpoint accepts ?fields=: a comma-separated list of the field names to keep, for example name, cca2, cca3, flags, capital, currencies, languages, idd, population, region, latlng, timezones, translations. Names are not trimmed, which matches the original: name, capital (with a space) keeps only name.

v2 endpoints

PathReturns
/v2/all or /v2/all?fields=a,bEvery country, with every field unless you pass fields.
/v2/alpha/{code}One object, by alpha2Code or alpha3Code.
/v2/alpha?codes=fr,deSeveral codes, as an array.
/v2/name/{name} (?fullText=true)Name contains the text (accents ignored). Exact names are listed first, then alternative spellings.
/v2/currency/{code}Exactly 3 letters (eur); anything else is 400.
/v2/callingcode/{code}, /v2/capital/, /v2/region/, /v2/subregion/, /v2/demonym/Exact calling code; capital contains the text; region, subregion and demonym must match exactly (case ignored).
/v2/lang/{iso639}2-letter (es) or 3-letter (spa) language code.
/v2/regionalbloc/{acronym}eu, asean, au, caricom, efta, usan and others.

Numbers and property order

Answers are written byte for byte the way the original wrote them, which matters for typed clients. The original was Java, and its decimal fields (area, latlng, capitalInfo.latlng, gini) were Java Doubles, so whole numbers keep a .0 and numbers of 10 million or more use Java's exponent form: "area":377930.0, "latlng":[36.0,138.0], "area":1.7098246E7 (Russia). Dart code such as json['area'] as double or List<double>.from(json['latlng']) depends on this. Property order and empty values follow the original's two serialisers too: answers without ?fields= leave out empty lists and empty strings (Iceland has no borders key) but write "postalCode":{"format":null,"regex":null}; answers with ?fields= keep empty lists ("borders":[]) and start with flags. We compared our output with 947 country records that people saved from the original API and committed to public GitHub repositories, wherever the data hasn't changed since: every one is byte-identical, and so are 925 saved area, latlng and gini values.

One v2 quirk is kept on purpose. List answers filtered with ?fields= always include "independent", and it is false unless you asked for that field. The original API did the same thing.

restcountries.eu paths

/rest/v2/… takes the old restcountries.eu URLs, which shut down in 2021, and serves them from the same v2 data. There, fields and codes may also be separated by ; as on .eu. This is best effort: the .eu service and its data were older than the v2 data served here.

Keys and limits

TierLimitHow
Free1,000 requests a day per IP addressNothing to send. CORS is open, so it works from browser code.
Key ($4/month)10,000 requests a day per keyHeader x-api-key: KEY, Authorization: Bearer KEY, or ?key=KEY. Keep keys in server code.

Days reset at 00:00 UTC. Every answer carries x-tier, x-ratelimit-limit and x-ratelimit-remaining (your own count). On top of the per-IP limit, all free requests together are capped at 40,000 a day, because our Workers plan has a daily cap. That total is counted in one place for everyone. If it is ever reached, every free request gets a 429 that says so until 00:00 UTC, even from an IP that has made no requests yet, and keys keep working. Key checks are cached for up to 10 minutes. If our key service can't be reached, a key verified in the last 10 minutes by the same Cloudflare instance keeps working; otherwise the request is answered on the free tier with the header x-key-status: unverified instead of failing. Lookups are case-insensitive, and the data never changes between requests, so cache answers on your side and you will rarely get near a limit.

Errors

Our errors are JSON with the same status and message keys the original used. Ours add error (a machine code) and docs.

StatusBody or causeWhat to do
400{"status":400,"message":"Bad Request"} for a code that isn't 2–3 characters, a v2 currency that isn't 3 letters, or status other than true/falseFix the request.
404{"status":404,"message":"Not Found"}: nothing matched (or an unknown path, with our error field)Treat as "no results".
401invalid_or_inactive_key: a key was sent but isn't active (cancelled, or mistyped)Fix the key, or remove it to use the free tier.
429daily_limit_reached, with resets_at and a Retry-After headerWait for the reset or use a key.
405 / 414Not GET, HEAD or OPTIONS / path or query too longUse GET with a normal-length URL.
500internal_errorRetry after a few seconds.
Non-JSONAn HTML page from Cloudflare, with no CORS headers, so browser code sees a network error. Examples: 1015 (rate limited), 1102 (the Worker went over its CPU limit) or a 5xxTreat any answer that isn't JSON as temporary and retry with backoff. Our heaviest queries measured up to 9 ms of CPU (wrangler tail, 133 requests including crafted 800 KB filtered lists), under the 10 ms limit, but the limit is Cloudflare's to enforce.

A robust check in JavaScript:

async function countries(path) {
  const r = await fetch("https://pw-d20261017a29.plainwork-apps.workers.dev" + path);
  const body = await r.text();
  let data;
  try { data = JSON.parse(body); } catch { throw new Error("Temporary error (" + r.status + "), retry later"); }
  if (r.status === 404) return [];                    // nothing matched
  if (!r.ok) throw new Error(data.message);           // 400, 401, 429 ...
  return Array.isArray(data) ? data : [data];
}

Python:

import requests
r = requests.get("https://pw-d20261017a29.plainwork-apps.workers.dev/v3.1/alpha/jp",
                 params={"fields": "name,capital"}, timeout=10)
print(r.json() if r.headers.get("content-type", "").startswith("application/json") else "retry later")

Where the data comes from

The data is REST Countries' own countriesV3.1.json (last changed 25 Feb 2026) and countriesV2.json (12 Nov 2025), from its MPL-2.0 source repository. The values are unchanged; they are written the way the original API wrote them (see numbers and property order). The v2 records carry only the fields the original v2 classes had. The endpoint logic was ported from the same repository's Java code. We also wrote a second, separate implementation of that logic in Python, and 8,500+ queries built from every country's codes, names, capitals, currencies, languages and demonyms give the same matches in both. Both were written by us, so the byte comparison with saved original responses is the outside check. Flag and coat-of-arms image URLs point to flagcdn.com and mainfacts.com, as they always did. Population and other fast-moving figures are frozen at that release. Countrydrop is not affiliated with REST Countries.

Deliberate differences: /all without fields returns everything, as it did until June 2025, where the final API answered 400. /v2/regionalbloc/ matches a bloc's acronym even when the bloc lists no other acronyms; the original code skipped those blocs. Results are returned in data-file order, where the original's v3.1 order was arbitrary.

More from the catalogue

Other small tools

See everything →