Skip to main content

Postal Code Lookup API

REAL DATA

Look up real postal codes in 21 countries — place, region, coordinates and GeoNames accuracy — with each country's coverage stated, not implied.

Authoritative reference data or standards computation

Base URL
/api/postal-codes
Capabilities
4 routes
Last updated
Data sources · 2

GET /api/postal-codes

The code's place, administrative hierarchy, estimated coordinates and GeoNames accuracy. The code is matched in normalised form, so the spacing and hyphens are up to you.

Live requestRuns against the public API
No key required
GET/api/postal-codes?country=US&postalCode=94103

Request parameters

ISO 3166-1 alpha-2 code of a supported country. 21 are bundled — any other code is a 400 listing them all, never another country's codes. `GET /api/postal-codes/countries` returns each one's coverage.

The postal code. Case, spaces and hyphens are ignored when matching (`624 66` = `62466`, `100-0001` = `1000001`).

Advanced response options4 options

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/postal-codes?country=US&postalCode=94103"
const res = await fetch("https://randomapi.dev/api/postal-codes?country=US&postalCode=94103");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/postal-codes?country=US&postalCode=94103").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/postal-codes?country=US&postalCode=94103"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 2
country enum required

ISO 3166-1 alpha-2 code of a supported country. 21 are bundled — any other code is a 400 listing them all, never another country's codes. `GET /api/postal-codes/countries` returns each one's coverage.

allowed: AT | AU | BE | CA | CH | CZ | DE | DK | ES | FI | FR | GB | IT | JP | MX | NL | NO | NZ | PL | SE | US
example: country=US
postalCode string required

The postal code. Case, spaces and hyphens are ignored when matching (`624 66` = `62466`, `100-0001` = `1000001`).

allowed: 1 – 24
example: postalCode=94103
Universal parameters Shared response and formatting options 4
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=country,countryName
exclude list

Return all fields except these (comma-separated).

example: exclude=accuracy
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 14
country string

ISO 3166-1 alpha-2 code of the country the shard belongs to.

example: US

countryName string

English country name, joined from the ISO 3166-1 dataset.

example: United States

postalCode string

The code exactly as GeoNames publishes it, including any internal space or hyphen (`624 66`, `100-0001`). Lookups accept the separator-free form too.

example: 94103

place string

The first place name GeoNames lists for this code. Where several places share a code, the others are not carried — `placeCount` says how many there were.

example: San Francisco

placeCount integer

How many distinct place names GeoNames lists for this code (1 when the code belongs to one place). Above 1 means `place` is one of several.

example: 1

admin1 string nullable

First-order division name (state, region, prefecture); null when the upstream row has none.

example: California

admin1Code string nullable

First-order division code exactly as GeoNames publishes it, which is not one scheme across countries: a USPS state code in the US, a province code in Canada, an INSEE region number in France, a FIPS 10-4 code in Japan (Tokyo To is `40`, not its JIS number 13). A country can also carry two codes for the same division — `/regions` lists each of them.

example: CA

admin2 string nullable

Second-order division name (county, province, municipality); null when absent upstream — New Zealand is the one bundled country whose rows carry no second-order division at all.

example: City and County of San Francisco

admin2Code string nullable

Second-order division code exactly as GeoNames publishes it. Its meaning is country-specific, and in the UK not even internally consistent: a three-digit FIPS county code in the US, a GeoNames feature id in Japan, a department number in France, and a mixture of council codes, GeoNames ids and ONS codes in the UK. Treat it as an upstream label, not a key to join on.

example: 075

admin3 string nullable

Third-order division name (community); null when the upstream row has none, which is most of the bundled codes — Australia, Czechia, Denmark, the Netherlands, New Zealand, Norway and Sweden carry none at all.

example: null

admin3Code string nullable

Third-order division code; null when absent upstream.

example: null

latitude float

Estimated WGS84 latitude of the code, as published. Not a surveyed centroid — see `accuracy`.

example: 37.7725

longitude float

Estimated WGS84 longitude of the code, as published.

example: -122.4147

accuracy integer nullable

GeoNames' own accuracy column. The upstream readme defines 1 = estimated, 4 = geonameid and 6 = centroid of addresses or shape; 1, 3, 4, 5, 6 all occur in the data and 3 and 5 are undefined upstream. Null for the 42% of codes that leave the column blank.

example: 4

Documented examples

Build-generated requests and complete responses
4
San Francisco's SoMa ZIP
GET /api/postal-codes?country=US&postalCode=94103
{
  "data": {
    "country": "US",
    "countryName": "United States",
    "postalCode": "94103",
    "place": "San Francisco",
    "placeCount": 1,
    "admin1": "California",
    "admin1Code": "CA",
    "admin2": "City and County of San Francisco",
    "admin2Code": "075",
    "admin3": null,
    "admin3Code": null,
    "latitude": 37.7725,
    "longitude": -122.4147,
    "accuracy": 4
  },
  "meta": {
    "endpoint": "postal-codes",
    "params": {
      "country": "US",
      "postalCode": "94103"
    },
    "generatedAt": "2026-09-15T19:47:18.000Z"
  }
}
A Swedish code typed without its space
GET /api/postal-codes?country=SE&postalCode=62466
{
  "data": {
    "country": "SE",
    "countryName": "Sweden",
    "postalCode": "624 66",
    "place": "Fårö",
    "placeCount": 1,
    "admin1": "Gotland",
    "admin1Code": "05",
    "admin2": null,
    "admin2Code": null,
    "admin3": null,
    "admin3Code": null,
    "latitude": 57.9167,
    "longitude": 19.1333,
    "accuracy": 4
  },
  "meta": {
    "endpoint": "postal-codes",
    "params": {
      "country": "SE",
      "postalCode": "62466"
    },
    "generatedAt": "2026-09-15T19:47:18.000Z"
  }
}
Chiyoda, Tokyo
GET /api/postal-codes?country=JP&postalCode=100-0001
{
  "data": {
    "country": "JP",
    "countryName": "Japan",
    "postalCode": "100-0001",
    "place": "Chiyoda",
    "placeCount": 1,
    "admin1": "Tokyo To",
    "admin1Code": "40",
    "admin2": "Chiyoda Ku",
    "admin2Code": "1864529",
    "admin3": null,
    "admin3Code": null,
    "latitude": 35.6841,
    "longitude": 139.7521,
    "accuracy": null
  },
  "meta": {
    "endpoint": "postal-codes",
    "params": {
      "country": "JP",
      "postalCode": "100-0001"
    },
    "generatedAt": "2026-09-15T19:47:18.000Z"
  }
}
A UK outward code
GET /api/postal-codes?country=GB&postalCode=sw1
{
  "data": {
    "country": "GB",
    "countryName": "United Kingdom",
    "postalCode": "SW1",
    "place": "Pimlico",
    "placeCount": 12,
    "admin1": "England",
    "admin1Code": "ENG",
    "admin2": "Greater London",
    "admin2Code": "11609024",
    "admin3": null,
    "admin3Code": null,
    "latitude": 51.489,
    "longitude": -0.137,
    "accuracy": 4
  },
  "meta": {
    "endpoint": "postal-codes",
    "params": {
      "country": "GB",
      "postalCode": "sw1"
    },
    "generatedAt": "2026-09-15T19:47:18.000Z"
  }
}

GET /api/postal-codes/regions

Every state, region or prefecture that appears in the country's codes, largest first, with how many codes sit in it — the values the admin1 filter on /search accepts. Codes whose upstream row names no division are not counted here; /countries reports how many of those a country has.

Live requestRuns against the public API
No key required
GET/api/postal-codes/regions?country=US

Request parameters

ISO 3166-1 alpha-2 code of a supported country. 21 are bundled — any other code is a 400 listing them all, never another country's codes. `GET /api/postal-codes/countries` returns each one's coverage.

Advanced response options6 options

Limit the number of returned records (1–100). Defaults to all matches.

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Response format: json envelope, ndjson (one record per line) or csv.

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/postal-codes/regions?country=US"
const res = await fetch("https://randomapi.dev/api/postal-codes/regions?country=US");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/postal-codes/regions?country=US").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/postal-codes/regions?country=US"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 1
country enum required

ISO 3166-1 alpha-2 code of a supported country. 21 are bundled — any other code is a 400 listing them all, never another country's codes. `GET /api/postal-codes/countries` returns each one's coverage.

allowed: AT | AU | BE | CA | CH | CZ | DE | DK | ES | FI | FR | GB | IT | JP | MX | NL | NO | NZ | PL | SE | US
example: country=US
Universal parameters Shared response and formatting options 6
count int

Limit the number of returned records (1–100). Defaults to all matches.

default: all matches
allowed: 1 – 100
example: count=3
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=country,countryName
exclude list

Return all fields except these (comma-separated).

example: exclude=postalCodes
format enum

Response format: json envelope, ndjson (one record per line) or csv.

default: json
allowed: json | ndjson | csv
example: format=csv
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 5
country string

ISO 3166-1 alpha-2 code.

example: US

countryName string

English country name.

example: United States

admin1Code string nullable

Division code as GeoNames publishes it; null where the file carries a name but no code.

example: CA

admin1 string nullable

Division name; null where the file carries a code but no name.

example: California

postalCodes integer

Postal codes in this division.

example: 2600

Documented examples

Build-generated requests and complete responses
3
US states, largest first
GET /api/postal-codes/regions?country=US
{
  "data": [
    {
      "country": "US",
      "countryName": "United States",
      "admin1Code": "TX",
      "admin1": "Texas",
      "postalCodes": 2600
    },
    {
      "country": "US",
      "countryName": "United States",
      "admin1Code": "CA",
      "admin1": "California",
      "postalCodes": 2593
    },
    {
      "country": "US",
      "countryName": "United States",
      "admin1Code": "PA",
      "admin1": "Pennsylvania",
      "postalCodes": 2176
    },
    {
      "country": "US",
      "countryName": "United States",
      "admin1Code": "NY",
      "admin1": "New York",
      "postalCodes": 2154
    },
    {
      "country": "US",
      "countryName": "United States",
      "admin1Code": "IL",
      "admin1": "Illinois",
      "postalCodes": 1575
    },
    {
      "country": "US",
      "countryName": "United States",
    …
    "generatedAt": "2026-09-15T19:47:18.000Z"
  }
}
Japanese prefectures
GET /api/postal-codes/regions?country=JP
{
  "data": [
    {
      "country": "JP",
      "countryName": "Japan",
      "admin1Code": "12",
      "admin1": "Hokkaido",
      "postalCodes": 9205
    },
    {
      "country": "JP",
      "countryName": "Japan",
      "admin1Code": "01",
      "admin1": "Aichi Ken",
      "postalCodes": 8755
    },
    {
      "country": "JP",
      "countryName": "Japan",
      "admin1Code": "40",
      "admin1": "Tokyo To",
      "postalCodes": 8747
    },
    {
      "country": "JP",
      "countryName": "Japan",
      "admin1Code": "22",
      "admin1": "Kyoto Fu",
      "postalCodes": 6680
    },
    {
      "country": "JP",
      "countryName": "Japan",
      "admin1Code": "29",
      "admin1": "Niigata Ken",
      "postalCodes": 5954
    },
    {
      "country": "JP",
      "countryName": "Japan",
    …
    "generatedAt": "2026-09-15T19:47:18.000Z"
  }
}
Danish regions as CSV
GET /api/postal-codes/regions?country=DK&format=csv
country,countryName,admin1Code,admin1,postalCodes
DK,Denmark,17,Capital Region,577
DK,Denmark,21,South Denmark,164
DK,Denmark,18,Central Jutland,144
DK,Denmark,20,Zealand,129
DK,Denmark,19,North Denmark,75
DK,Denmark,84,Region Hovedstaden,64
DK,Denmark,83,Region Syddanmark,3
DK,Denmark,82,Region Midtjylland,2
DK,Denmark,81,Region Nordjylland,1

GET /api/postal-codes/countries

The auditable index of the dataset: how many codes each country carries, how many upstream rows they were deduplicated from, whether the coverage is complete or a documented subset, and the SHA-256 of the file it came from.

Live requestRuns against the public API
No key required
GET/api/postal-codes/countries?count=100

Request parameters

Filter by coverage: 'full' = every code the country's published file carries; 'partial' = a documented subset, named in coverageNote.

Advanced response options6 options

Limit the number of returned records (1–100). Defaults to all matches.

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Response format: json envelope, ndjson (one record per line) or csv.

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/postal-codes/countries?count=100"
const res = await fetch("https://randomapi.dev/api/postal-codes/countries?count=100");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/postal-codes/countries?count=100").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/postal-codes/countries?count=100"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 1
coverage enum

Filter by coverage: 'full' = every code the country's published file carries; 'partial' = a documented subset, named in coverageNote.

default: any
allowed: any | full | partial
example: coverage=partial
Universal parameters Shared response and formatting options 6
count int

Limit the number of returned records (1–100). Defaults to all matches.

default: all matches
allowed: 1 – 100
example: count=3
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=country,countryName
exclude list

Return all fields except these (comma-separated).

example: exclude=sourceSha256
format enum

Response format: json envelope, ndjson (one record per line) or csv.

default: json
allowed: json | ndjson | csv
example: format=csv
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 14
country string

ISO 3166-1 alpha-2 code.

example: GB

countryName string

English country name.

example: United Kingdom

postalCodes integer

Unique postal codes bundled for this country.

example: 3002

upstreamRows integer

Rows in the upstream file before deduplication to one row per code.

example: 27450

places integer

Distinct place names across those codes.

example: 2588

regions integer

First-order divisions those codes fall in.

example: 5

codesWithoutRegion integer

Codes whose upstream row names no first-order division at all. They are returned by /search with a null admin1 and are not counted by /regions.

example: 21

coverage string

'full' or 'partial'.

example: partial

coverageNote string nullable

Exactly what is missing or unusual about this country's codes. Null when there is nothing to declare.

example: Outward codes only (`SW1`, `M9`, `ZE2`) …

withAccuracy integer

Codes carrying a GeoNames accuracy value; the rest expose accuracy as null.

example: 2986

sharedCodes integer

Codes GeoNames lists under more than one place name (placeCount above 1).

example: 2227

maxPlaceCount integer

Most place names GeoNames lists for a single code in this country.

example: 162

sourceFile string

The upstream file this country's codes come from.

example: https://download.geonames.org/export/zip/GB.zip

sourceSha256 string

SHA-256 of the pinned <CC>.txt inside that archive.

example: 8cfd9136650053f28df3ca6d5a94b285411f9f6eb1542ac7746c883734303594

Documented examples

Build-generated requests and complete responses
3
Every supported country
GET /api/postal-codes/countries?count=100
{
  "data": [
    {
      "country": "AT",
      "countryName": "Austria",
      "postalCodes": 2501,
      "upstreamRows": 19225,
      "places": 2113,
      "regions": 9,
      "codesWithoutRegion": 0,
      "coverage": "full",
      "coverageNote": null,
      "withAccuracy": 2501,
      "sharedCodes": 1790,
      "maxPlaceCount": 126,
      "sourceFile": "https://download.geonames.org/export/zip/AT.zip",
      "sourceSha256": "ebe87283d0f46a5bea26c4d6ba369a6cfa17288b47117503a773d9ca5cab33a8"
    },
    {
      "country": "AU",
      "countryName": "Australia",
      "postalCodes": 3171,
      "upstreamRows": 18521,
      "places": 2776,
      "regions": 8,
      "codesWithoutRegion": 0,
      "coverage": "full",
      "coverageNote": null,
      "withAccuracy": 3165,
      "sharedCodes": 2147,
      "maxPlaceCount": 111,
      "sourceFile": "https://download.geonames.org/export/zip/AU.zip",
      "sourceSha256": "f422fe131f44794f22cf757d02d8828041e7ad5198a5ad76023e509e61b5bc09"
    },
    {
      "country": "BE",
      "countryName": "Belgium",
      "postalCodes": 1146,
      "upstreamRows": 2781,
      "places": 1127,
    …
    "generatedAt": "2026-09-15T19:47:18.000Z"
  }
}
Only the partial ones, and why
GET /api/postal-codes/countries?coverage=partial
{
  "data": [
    {
      "country": "CA",
      "countryName": "Canada",
      "postalCodes": 1653,
      "upstreamRows": 1657,
      "places": 1645,
      "regions": 13,
      "codesWithoutRegion": 0,
      "coverage": "partial",
      "coverageNote": "Forward sortation areas only — the first three characters (`M5V`): 1,653 of them, standing for roughly 880,000 full six-character codes. The unit-level file is deliberately never fetched or bundled. Two full codes (`T3T 0E5`, `V3Y 0H2`) sit in the upstream FSA file and are kept exactly as published.",
      "withAccuracy": 1638,
      "sharedCodes": 2,
      "maxPlaceCount": 4,
      "sourceFile": "https://download.geonames.org/export/zip/CA.zip",
      "sourceSha256": "0980e72da7c3c39a066dcad5cf6348adf72e4c7149aee204e7155cf6af8cd61b"
    },
    {
      "country": "GB",
      "countryName": "United Kingdom",
      "postalCodes": 3002,
      "upstreamRows": 27450,
      "places": 2588,
      "regions": 5,
      "codesWithoutRegion": 21,
      "coverage": "partial",
      "coverageNote": "Outward codes only (`SW1`, `M9`, `ZE2`): 3,002 of them, deduplicated from 27,450 upstream rows, standing for roughly 1.8 million full UK postcodes. The unit-level file carries Royal Mail data under Royal Mail copyright and database right and is deliberately never fetched or bundled.",
      "withAccuracy": 2986,
      "sharedCodes": 2227,
      "maxPlaceCount": 162,
      "sourceFile": "https://download.geonames.org/export/zip/GB.zip",
      "sourceSha256": "8cfd9136650053f28df3ca6d5a94b285411f9f6eb1542ac7746c883734303594"
    },
    {
      "country": "NL",
      "countryName": "Netherlands",
      "postalCodes": 4086,
      "upstreamRows": 4334,
      "places": 2367,
    …
    "generatedAt": "2026-09-15T19:47:18.000Z"
  }
}
Countries and code counts only
GET /api/postal-codes/countries?fields=country,countryName,postalCodes
{
  "data": [
    {
      "country": "AT",
      "countryName": "Austria",
      "postalCodes": 2501
    },
    {
      "country": "AU",
      "countryName": "Australia",
      "postalCodes": 3171
    },
    {
      "country": "BE",
      "countryName": "Belgium",
      "postalCodes": 1146
    },
    {
      "country": "CA",
      "countryName": "Canada",
      "postalCodes": 1653
    },
    {
      "country": "CH",
      "countryName": "Switzerland",
      "postalCodes": 3362
    },
    {
      "country": "CZ",
      "countryName": "Czechia",
      "postalCodes": 2694
    },
    {
      "country": "DE",
      "countryName": "Germany",
      "postalCodes": 10813
    },
    {
      "country": "DK",
      "countryName": "Denmark",
    …
    "generatedAt": "2026-09-15T19:47:18.000Z"
  }
}

About this API

Coverage & behavior

336,034 postal codes from the GeoNames postal dumps, one per code, for 21 countries: AT (2,501), AU (3,171), BE (1,146), CA (1,653), CH (3,362), CZ (2,694), DE (10,813), DK (1,159), ES (11,150), FI (3,576), FR (20,418), GB (3,002), IT (4,735), JP (142,577), MX (32,448), NL (4,086), NO (5,132), NZ (1,737), PL (20,299), SE (18,887), US (41,488). Every row carries the place GeoNames lists for the code, the administrative hierarchy above it, an estimated WGS84 point and GeoNames' own accuracy value. The snapshot is pinned by SHA-256 per country and re-checked by a deterministic parity script; nothing is fetched while your request is served.

What "the code exists" does and does not mean

A row here means GeoNames publishes this code for this country. It does not mean the code is currently in service, that an address inside it is deliverable, or that a carrier will route to it. Postal operators issue, retire and re-scope codes constantly, and this is a pinned third-party snapshot — not an operator feed. For "is this string even shaped like a postal code for this country", use the sibling /api/postal-code-formats; this endpoint answers the other half, "does that code exist and where is it".

One row per code, and what that costs

The dumps repeat a code once per place it covers — Sonora's 85203 carries 343 distinct place names and the UK outward code ZE2 carries 162. The 336,034 codes here are deduplicated from 661,604 upstream rows, keeping the first row upstream lists for each code. place is therefore a place for the code, and placeCount says how many distinct names the upstream file had — 12 for SW1, 1 for most codes. The other names are not carried; the count is there so you can see that rather than guess it, and GET /api/postal-codes/countries reports the worst case per country.

Coverage is per country, and three countries are partial

  • Canada (CA) — Forward sortation areas only — the first three characters (M5V): 1,653 of them, standing for roughly 880,000 full six-character codes. The unit-level file is deliberately never fetched or bundled. Two full codes (T3T 0E5, V3Y 0H2) sit in the upstream FSA file and are kept exactly as published.
  • United Kingdom (GB) — Outward codes only (SW1, M9, ZE2): 3,002 of them, deduplicated from 27,450 upstream rows, standing for roughly 1.8 million full UK postcodes. The unit-level file carries Royal Mail data under Royal Mail copyright and database right and is deliberately never fetched or bundled.
  • Netherlands (NL) — Outward codes only: GeoNames publishes the four digits (1011) and not the two-letter suffix, so 4,086 codes stand for roughly 460,000 full Dutch codes. The unit-level file is deliberately not bundled.

GET /api/postal-codes/countries returns that note, the code count, the upstream row count and the pinned SHA-256 for every supported country, so partial coverage is provable instead of implied. Two countries have quirks worth stating up front: France's file is mostly CEDEX business-routing codes, and some German codes name a company rather than a locality — both are in their coverageNote.

A shard is a GeoNames file, not always one ISO 3166-1 entry. GeoNames folds a few separate entries into a neighbour's country file and this snapshot mirrors that scoping exactly, so every row is stamped with the shard it came from: the GB file carries 23 Crown Dependency outward codes — JE1–JE4 (Jersey), GY1–GY10 (Guernsey, Sark and Alderney included) and IM1–IM9 (Isle of Man) — and the US file carries 96960 and 96970, which are in the Marshall Islands, and lists MH among its first-order divisions. JE, GG, IM and MH are separate ISO 3166-1 entries in this project's own /api/countries table; they read as country: "GB" / "US" here because the shard is, not because the code belongs to that country.

accuracy is theirs, not ours

The column comes straight from GeoNames, and its readme defines only three of the values it uses: 1 = estimated, 4 = geonameid, 6 = centroid of addresses or shape. The bundled rows carry 1, 3, 4, 5, 6; 3 and 5 have no upstream definition and are passed through rather than invented over. 58% of codes carry a value at all — the rest are null, not zero. GeoNames also states plainly that where it could not find a matching toponym it averaged the coordinates of neighbouring codes, so treat every point as an approximate locator, never a survey.

Matching rules

Lookups and startsWith compare a normalised form — uppercased, with everything that is not a letter or digit removed — so 624 66 and 62466, 100-0001 and 1000001, sw1 and SW1 all find the same row. The postalCode field always echoes the published spelling. place is a case-insensitive prefix on the name as GeoNames spells it, diacritics included: place=aar finds Aarhus, place=Faro does not find Fårö. admin1 matches a first-order division by its code or its name, case-insensitively; GET /api/postal-codes/regions lists them per country, largest first. The upstream files are not consistent about division codes — Germany carries Nordrhein-Westfalen under both NW and 07, Sweden carries Gotland under both 05 and I — so /regions shows each code as its own row and filtering by the name returns every one of them rather than silently picking one.

Deliberate limits

  • No nearest-code-to-coordinates route. Scanning a country's coordinates per request is the one query this storage cannot answer cheaply, and /api/cities/nearest already answers coordinate queries against real populated places. Use that.
  • No unit-level UK, Canadian or Dutch codes. The upstream full-code files are not bundled: the UK one carries Royal Mail data under Royal Mail copyright and database right, and the three are excluded together rather than one by licence and two by accident.
  • No address validation, no geocoding of a street, no deliverability.
  • No random fixture route. Randomised postal codes belong with fake addresses — /api/addresses generates those — and a seeded route here would make the factual routes look seeded too.
  • Countries GeoNames itself truncates are not bundled. Its readme records that Chile, Ireland and Malta carry only the first characters "for copyright reasons", China only digits ending in 00, Argentina the first five positions, and Brazil only major codes. Ireland yields 139 routing keys that way — a stub is worse than an honest absence.

Use it for

  • Autofill a checkout's city and region from the postal code a customer typed
  • Show the coverage note for a country before promising postal-code lookup in that market
  • Seed a test database with real postal codes, places and coordinates for one region
  • Check whether a postal code your users typed exists at all, after the format check passes

Frequently asked questions

How do I look up a postal code and get the city?

/api/postal-codes?country=US&postalCode=94103 returns the place, the state and county, the coordinates and GeoNames' accuracy value. Unknown codes answer an explicit 404 — never a nearby guess.

Does this API prove a postal code is deliverable?

No. A row means GeoNames publishes that code for that country in the pinned snapshot. It says nothing about whether the code is in service today or whether mail reaches an address inside it — check with the operator for that.

Why are UK and Canadian postal codes incomplete here?

GeoNames splits them: the free country file carries only the outward part for the UK (SW1) and the forward sortation area for Canada (M5V), with the unit-level codes in separate full files. The UK full file contains Royal Mail data under Royal Mail copyright and database right, so none of the full files are bundled. GET /api/postal-codes/countries states this per country.

Can I find postal codes by place name?

Yes — /api/postal-codes/search?country=DK&place=Aarhus returns every code whose place name starts with that text, in ascending code order. Matching is case-insensitive but keeps diacritics as GeoNames spells them.

What does the accuracy field mean?

It is GeoNames' own column, passed through. Its readme defines 1 = estimated, 4 = geonameid and 6 = centroid of addresses or shape; 3 and 5 also occur with no upstream definition, and most Japanese codes leave it blank, which shows here as null.

Is there a route for the nearest postal code to a latitude and longitude?

No, deliberately. The packed shards are indexed by code, place and region, not by position, so a reverse-geocode would scan a whole country per request. /api/cities/nearest answers coordinate queries against real populated places instead.