Documentation navigation

Postal Codes API

Postal/ZIP codes with nearest-neighbor search

Authentication: X-API-Key header on every request. Get a key.

List postal codes

GET/v1/postal-codes

Quota cost: 1 unit — 2 with ?within= Try it

Returns a paginated list of postal codes with optional filtering by country and code.

Parameters

Name Type Required Description
lang string optional

ISO 639-1 language code for localized names (e.g., de, fr, ja)

country string optional

Filter by ISO alpha-2 country codes (comma-separated)

code string optional

Filter by postal code

within integer optional

Return only results whose coordinates fall geometrically inside the boundary of the given area, identified by its GeoNames id. Countries and administrative regions are valid areas; a city id is not.

Accepts exactly one id. A COMMA-SEPARATED LIST IS REJECTED with a 422 naming the limit — a deliberate departure from the comma-separated convention country uses, because each value would be a separate polygon intersection. Ask for one area per request.

COST: a request using this parameter consumes 2 quota units instead of 1, on every plan including Free. Combining it with bbox still costs 2, not 3 — the highest multiplier applies rather than the sum, and adding a box makes the query cheaper to serve, so it is never penalised.

This asks a GEOMETRIC question and can therefore return a different set of cities than /v1/regions/{id}/cities, which asks an administrative one. See that endpoint's description for when and why the two disagree.

Two failures are reported with distinct 400 codes so a typo is distinguishable from a coverage gap: area_not_an_area means the id does not name a country or region at all, and area_no_boundary means it does but no boundary polygon is available for it yet.

bbox string optional

Return only results inside the bounding box, given as four comma-separated numbers in the order w,s,e,n — west longitude, south latitude, east longitude, north latitude.

Longitudes must be within [-180, 180] and latitudes within [-90, 90].

A box where WEST IS GREATER THAN EAST wraps the antimeridian and is fully supported: bbox=170,-20,-170,-10 is a box around Fiji, evaluated as the union of the two halves it spans. Latitude has no equivalent wrap-around meaning, so s greater than n is a validation error rather than a wrapped box.

Charged at the standard request cost of 1 unit. Adding it to a within query narrows the candidate set before the polygon test and does not raise the charge.

cursor string optional

Pagination cursor from a previous response

limit integer optional

Number of results per page (1-100, default 25)

fields string optional

Comma-separated list of fields to include in the response

sort string optional

Sort field. Allowed: postal_code, country_code, place_name, id.

Code samples

Response

{
  "data": [
    {
      "id": 1140425,
      "country_code": "US",
      "postal_code": "94105",
      "place_name": "San Francisco",
      "admin_name1": "California",
      "admin_code1": "CA",
      "admin_name2": "San Francisco",
      "admin_code2": "075",
      "admin_name3": "",
      "admin_code3": "",
      "latitude": 37.7864,
      "longitude": -122.3892,
      "accuracy": 4,
      "country": {
        "iso_code": "US",
        "name": "United States"
      }
    }
  ],
  "meta": {
    "next_cursor": "eyJpZCI6MjV9",
    "prev_cursor": "eyJpZCI6MX0",
    "has_next": true,
    "has_prev": false,
    "count": 25
  }
}

Errors

Status Code When
400 area_not_an_area The within id is not an area
400 area_no_boundary No boundary polygon is held for that area
400 bad_request A malformed cursor, sort or pagination value
401 authentication_required No API key was supplied
401 authentication_failed The supplied API key is not valid
422 validation_error More than one area requested
422 validation_error A bounding box whose south edge is north of its north edge
429 rate_limit_exceeded Per-second throttle exceeded
429 quota_exceeded Monthly quota exhausted
503 area_query_timeout The containment query exceeded the statement timeout that bounds it. This is a 503 and NOT a 500, deliberately: the server is healthy and the request was valid — this one query against an unusually large or complex boundary simply ran out of its time budget. It is therefore worth retrying, and worth retrying with a narrower query. Adding `bbox` or further filters alongside `within` reduces the candidate set before the polygon test and is the most effective remedy. THIS RESPONSE BELONGS TO THE `within=` ROUTES ONLY. `GET /v1/resolve` and `GET /v1/boundaries/{geoname_id}` have their own 503 components (`ResolveQueryTimeout` and `BoundaryQueryTimeout`) because the remedy above is false on both: neither accepts `bbox` or `within`.

400 area_not_an_area

{
  "error": {
    "code": "area_not_an_area",
    "message": "The `within` parameter must be the GeoNames id of a country or a region, for example within=6252001 for the United States. The id supplied names something else — a city, for instance — or names nothing at all.",
    "request_id": "req_abc123"
  }
}

400 area_no_boundary

{
  "error": {
    "code": "area_no_boundary",
    "message": "That area exists, but no boundary polygon is available for it yet, so containment cannot be computed. Boundary coverage is strongest for countries and first- and second-level regions; see the documentation for current coverage.",
    "request_id": "req_abc123"
  }
}

400 bad_request

{
  "error": {
    "code": "bad_request",
    "message": "invalid cursor",
    "request_id": "req_abc123"
  }
}

401 authentication_required

{
  "error": {
    "code": "authentication_required",
    "message": "API key required. Get one at /signup",
    "request_id": "req_abc123"
  }
}

401 authentication_failed

{
  "error": {
    "code": "authentication_failed",
    "message": "Invalid API key",
    "request_id": "req_abc123"
  }
}

422 validation_error

{
  "error": {
    "code": "validation_error",
    "message": "Validation failed",
    "details": [
      {
        "field": "within",
        "issue": "accepts exactly one geoname_id; comma-separated lists are not supported"
      }
    ],
    "request_id": "req_abc123"
  }
}

422 validation_error

{
  "error": {
    "code": "validation_error",
    "message": "Validation failed",
    "details": [
      {
        "field": "bbox",
        "issue": "south latitude must be less than north latitude"
      }
    ],
    "request_id": "req_abc123"
  }
}

429 rate_limit_exceeded

{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded. Try again later.",
    "request_id": "req_abc123"
  }
}

429 quota_exceeded

{
  "error": {
    "code": "quota_exceeded",
    "message": "Monthly quota exhausted. You've used 2,000,000 of 2,000,000 requests for the current period, which resets on March 1, 2026. Upgrade for a higher limit.",
    "request_id": "req_abc123",
    "quota": {
      "limit": 2000000,
      "used": 2000000,
      "resets_at": "2026-03-01T00:00:00Z",
      "upgrade_url": "https://geosearch.dev/dashboard/billing"
    }
  }
}

503 area_query_timeout

{
  "error": {
    "code": "area_query_timeout",
    "message": "The containment query for that area took too long to complete. This is usually temporary — please retry. If it persists, narrow the query by adding a bbox or additional filters.",
    "request_id": "req_abc123"
  }
}

Find nearest postal codes

GET/v1/postal-codes/nearest

Quota cost: 1 unit Try it

Returns the nearest postal codes to a given latitude/longitude using PostGIS spatial index.

Parameters

Name Type Required Description
lang string optional

ISO 639-1 language code for localized names (e.g., de, fr, ja)

lat number required

Latitude (-90 to 90)

lon number required

Longitude (-180 to 180)

limit integer optional

Number of results (1-10, default 1)

Code samples

Response

{
  "data": [
    {
      "id": 1140425,
      "country_code": "US",
      "postal_code": "94105",
      "place_name": "San Francisco",
      "admin_name1": "California",
      "admin_code1": "CA",
      "admin_name2": "San Francisco",
      "admin_code2": "075",
      "admin_name3": "",
      "admin_code3": "",
      "latitude": 37.7864,
      "longitude": -122.3892,
      "accuracy": 4,
      "country": {
        "iso_code": "US",
        "name": "United States"
      }
    }
  ],
  "meta": {
    "next_cursor": "eyJpZCI6MjV9",
    "prev_cursor": "eyJpZCI6MX0",
    "has_next": true,
    "has_prev": false,
    "count": 25
  }
}

Errors

Status Code When
400 validation_error Invalid request parameters
401 authentication_required No API key was supplied
401 authentication_failed The supplied API key is not valid
429 rate_limit_exceeded Per-second throttle exceeded
429 quota_exceeded Monthly quota exhausted

400 validation_error

{
  "error": {
    "code": "validation_error",
    "message": "Invalid request parameters",
    "details": [
      {
        "field": "population_min",
        "issue": "must be a valid integer"
      }
    ],
    "request_id": "req_abc123"
  }
}

401 authentication_required

{
  "error": {
    "code": "authentication_required",
    "message": "API key required. Get one at /signup",
    "request_id": "req_abc123"
  }
}

401 authentication_failed

{
  "error": {
    "code": "authentication_failed",
    "message": "Invalid API key",
    "request_id": "req_abc123"
  }
}

429 rate_limit_exceeded

{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded. Try again later.",
    "request_id": "req_abc123"
  }
}

429 quota_exceeded

{
  "error": {
    "code": "quota_exceeded",
    "message": "Monthly quota exhausted. You've used 2,000,000 of 2,000,000 requests for the current period, which resets on March 1, 2026. Upgrade for a higher limit.",
    "request_id": "req_abc123",
    "quota": {
      "limit": 2000000,
      "used": 2000000,
      "resets_at": "2026-03-01T00:00:00Z",
      "upgrade_url": "https://geosearch.dev/dashboard/billing"
    }
  }
}