Guide

ZIP code database vs API

A CSV of ZIP codes looks simpler than an API, and sometimes it is. How the two compare, where the free files come from, and a hybrid that gives you both.

Updated October 29, 2026 · 6 min read

A ZIP code database is a file, usually a CSV, with the postal codes of one or more countries and their city, state and coordinates. You import it and query it locally. A ZIP code API answers the same questions over HTTP from a database that someone else maintains. A download fits offline systems, very high volumes in one country and one-off analysis. An API fits forms, several countries and anything with radius or distance search, where keeping your own copy current is more work than it looks. This guide compares the two, lists the free files and their gaps, and ends with a hybrid that caches API answers in your own database.

1. Side by side

Database downloadZIP code API
SetupDownload, import, index, write the queriesAn API key and one HTTP request
UpdatesYou re-download and re-importThe provider updates the data
CoverageUsually one file per country; free files vary in completenessMany countries behind one interface (zipcodestack: 182)
Radius and distanceYou build the geo queriesBuilt-in endpoints
SpeedLocal query, no networkA network round trip; cache the answers
CostFree or licence fee, plus your engineering timeFree tier, then plans by volume
Offline useYesNo
LicenceRead it: attribution, share-alike or no redistributionTerms of service; reselling the data is usually not allowed

2. Free ZIP code databases and their gaps

GeoNames postal codes (download.geonames.org/export/zip): one tab-separated file per country with code, place, administrative areas and coordinates. According to its readme:

US Census ZCTAs (census.gov): ZIP Code Tabulation Areas are polygons that approximate ZIP code areas, built from 2020 Census blocks. They are made for statistics and maps. The Census Bureau notes that not all valid ZIP codes are represented by a ZCTA.

Whatever file you use, check four things before you rely on it:

CheckWhy
CompletenessPartial files return “not found” for real codes
DateA snapshot from years ago misses new codes
LicenceAttribution and redistribution terms apply to your product, too
Leading zeros02108 (Boston) becomes 2108 if a spreadsheet or a numeric column reads it

3. How often postal codes change

Codes are added, retired and redrawn as areas are built up and delivery routes change. Now and then a country redesigns its whole system: Ireland introduced Eircodes in 2015, and South Korea moved from six-digit to five-digit codes the same year. A file you downloaded once drifts out of date quietly, and the first sign is usually a customer whose real postal code your form rejects.

4. When a download is the right choice

5. When an API is the better choice

What a zipcodestack request costs depends on the endpoint:

EndpointRequests counted
Search (/v1/search)1 per postal code found, up to 100 codes per call
Validate, reverse geocoding, autocomplete1 per call
Distance1 per distance calculated
Radius, polygon10 per call
Codes by city or state1 per 10 codes returned
Coverage, provinces, statusFree

The free plan has 1,000 requests a month for testing and personal projects. Commercial use starts with Small at $34.99 a month for 70,000 requests; Medium is $59.99 for 250,000, Large $159.99 for 2,500,000 and Business $249 for 6,000,000. All plans are on the pricing page.

6. A hybrid that works well

Call the API when a code is first needed and keep the answer in your own database for a while. Postal codes change slowly, so most lookups are answered locally, and stale entries refresh themselves. This Python example (Python 3.11, requests) keeps answers in SQLite for 30 days:

import json
import os
import sqlite3
import time

import requests

API_URL = "https://api.zipcodestack.com/v1/search"
MAX_AGE = 30 * 24 * 3600  # 30 days

db = sqlite3.connect("postal_cache.sqlite3")
db.execute(
    "CREATE TABLE IF NOT EXISTS postal_cache ("
    " country TEXT, code TEXT, places TEXT, fetched_at INTEGER,"
    " PRIMARY KEY (country, code))"
)


def lookup(code: str, country: str) -> list:
    """The places of a postal code, from the cache or the API; [] if the code is not found."""
    key = (country.upper(), " ".join(code.split()).upper())
    row = db.execute(
        "SELECT places, fetched_at FROM postal_cache WHERE country = ? AND code = ?", key
    ).fetchone()
    if row and time.time() - row[1] < MAX_AGE:
        return json.loads(row[0])

    response = requests.get(
        API_URL,
        params={"codes": key[1], "country": key[0]},
        headers={"apikey": os.environ["ZIPCODESTACK_KEY"]},
        timeout=10,
    )
    response.raise_for_status()
    body = response.json()
    if "results" not in body:  # parameter errors come back with status 200
        raise ValueError(f"zipcodestack: {body}")
    places = next(iter(body["results"].values()), []) if body["results"] else []

    db.execute(
        "INSERT OR REPLACE INTO postal_cache VALUES (?, ?, ?, ?)",
        (*key, json.dumps(places), int(time.time())),
    )
    db.commit()
    return places


if __name__ == "__main__":
    for place in lookup("1010", "at"):
        print(place["postal_code"], place["city"], place["latitude"], place["longitude"])

Against a local server that answers with the search documentation’s example, lookup("1010", "at") printed 1010 Wien, Innere Stadt 48.2077 16.3705; the second call for the same code, and the second call for a code that does not exist, were answered from SQLite without a request. One detail in the code: on most endpoints, a missing or invalid parameter comes back with status 200 and an error body instead of results, so the function checks for results before it caches anything.

Caching answers for your own application is the point of this pattern; passing the data on to others is a different matter, so read the terms of service before you build a product on top of it. To see the data first, try the free tools or the bulk ZIP code lookup, which turns a list of codes into a CSV with city, state and coordinates.

Frequently asked questions

Where can I download a free ZIP code database?

GeoNames publishes free postal code files for about 120 countries under CC BY 4.0, which requires credit to GeoNames. Some files are incomplete: Brazil has only major codes, and Chile, Ireland and China only partial codes. For the US, the Census Bureau publishes ZIP Code Tabulation Areas (ZCTAs), which approximate ZIP code areas for statistics.

Are ZCTAs the same as ZIP codes?

No. ZCTAs are the Census Bureau's area approximations of USPS ZIP codes, built from census blocks, and the Bureau notes that not all valid ZIP codes are represented by a 2020 ZCTA. Use them for maps and statistics, not to validate addresses.

How often do postal codes change?

Codes are added, retired and redrawn as areas are built up and delivery routes change, and now and then a country redesigns its system: Ireland introduced Eircodes in 2015, and South Korea moved to five-digit codes the same year. A downloaded file is a snapshot that you have to refresh yourself.

Is a ZIP code API fast enough for a checkout form?

Yes for most forms: one request per lookup, made from your server. For bulk jobs, look up as many as 100 codes per request, and cache the answers in your own database so repeated lookups never leave your network.

How much does a ZIP code API cost compared with a download?

Free downloads cost nothing up front, but you pay with the time to import, update and query them. zipcodestack's free plan has 1,000 requests a month for testing and personal projects; commercial use starts with the Small plan at $34.99 a month for 70,000 requests.

Postal code API

Get this data in your app

zipcodestack looks up, validates and searches postal codes in 182 countries: city, state, coordinates, distances and radius search, as JSON.

/v1/search documentation International postal code API All documentation

1,000 free requests every month. No credit card required.

GET https://api.zipcodestack.com/v1/search?codes=10001&country=us

{
  "results": {
    "10001": [{
      "postal_code": "10001",
      "country_code": "US",
      "latitude": 40.7484,
      "longitude": -73.9967,
      "city": "New York",
      "state": "New York",
      "state_code": "NY",
      "province": "New York"
    }]
  }
}

Keep reading

Start using our ZIP code API for free today!

Get 1,000 requests / month for free