Documentation navigation

Cities API

City data with geographic coordinates

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

List cities

GET/v1/cities

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

Returns a paginated list of cities with optional filtering by country, admin code, name, population, timezone, and elevation.

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)

admin1 string optional

Filter by admin1 code (state/province)

name string optional

Filter by city name (trigram fuzzy search)

population_min integer optional

Minimum population filter

population_max integer optional

Maximum population filter

timezone string optional

Filter by IANA timezone ID

min_elevation integer optional

Minimum elevation in meters

max_elevation integer optional

Maximum elevation in meters

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: name, population, elevation, id.

Code samples

Response

{
  "data": [
    {
      "id": 5391959,
      "geoname_id": 5391959,
      "name": "San Francisco",
      "ascii_name": "San Francisco",
      "country_code": "US",
      "admin1_code": "CA",
      "population": 873965,
      "timezone": "America/Los_Angeles",
      "latitude": 37.77493,
      "longitude": -122.41942
    }
  ],
  "meta": {
    "next_cursor": "eyJpZCI6NTM5MTk2MH0",
    "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"
  }
}

Get city by ID

GET/v1/cities/{id}

Quota cost: 1 unit Try it

Returns a single city by its numeric ID.

Parameters

Name Type Required Description
id integer required

City ID

lang string optional

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

fields string optional

Comma-separated list of fields to include in the response

Code samples

Response

{
  "data": {
    "id": 5391959,
    "geoname_id": 5391959,
    "name": "San Francisco",
    "ascii_name": "San Francisco",
    "country_code": "US",
    "admin1_code": "CA",
    "admin2_code": "075",
    "population": 873965,
    "elevation": 16,
    "timezone": "America/Los_Angeles",
    "latitude": 37.77493,
    "longitude": -122.41942,
    "country": {
      "iso_code": "US",
      "name": "United States"
    },
    "region": {
      "id": 5332921,
      "name": "California",
      "admin_code": "CA"
    }
  }
}

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
404 not_found Resource not found
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"
  }
}

404 not_found

{
  "error": {
    "code": "not_found",
    "message": "The requested resource was not found",
    "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"
    }
  }
}

Get administrative hierarchy for a city

GET/v1/cities/{id}/hierarchy

Quota cost: 1 unit Try it

Returns the full administrative hierarchy for a city, ordered from the city itself up through region, country, and continent.

Parameters

Name Type Required Description
id integer required

City ID

lang string optional

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

Code samples

Response

{
  "data": [
    {
      "geoname_id": 5391959,
      "name": "San Francisco",
      "type": "city",
      "depth": 0
    },
    {
      "geoname_id": 5332921,
      "name": "California",
      "type": "region",
      "depth": 1
    },
    {
      "geoname_id": 6252001,
      "name": "United States",
      "type": "country",
      "depth": 2
    },
    {
      "geoname_id": 6255149,
      "name": "North America",
      "type": "continent",
      "depth": 3
    }
  ],
  "meta": {
    "count": 4
  }
}

Errors

Status Code When
401 authentication_required No API key was supplied
401 authentication_failed The supplied API key is not valid
404 not_found Resource not found
429 rate_limit_exceeded Per-second throttle exceeded
429 quota_exceeded Monthly quota exhausted

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"
  }
}

404 not_found

{
  "error": {
    "code": "not_found",
    "message": "The requested resource was not found",
    "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"
    }
  }
}

Find nearby cities

GET/v1/cities/nearby

Quota cost: 1 unit Try it

Returns cities near a given latitude/longitude within a specified radius. Results are ordered by distance. Uses PostGIS spatial index for fast lookups.

Parameters

Name Type Required Description
lat number required

Latitude (-90 to 90)

lon number required

Longitude (-180 to 180)

radius number optional

Search radius in kilometers (default 50, max 200)

limit integer optional

Maximum results to return (1-250, default 10)

fields string optional

Comma-separated list of fields to include in the response

Code samples

Response

{
  "data": [
    {
      "id": 5391959,
      "name": "San Francisco",
      "country_code": "US",
      "population": 873965,
      "timezone": "America/Los_Angeles",
      "latitude": 37.77493,
      "longitude": -122.41942,
      "distance_km": 0.5
    }
  ],
  "meta": {
    "count": 1
  }
}

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"
    }
  }
}