# Postal Codes API — GeoSearch

> The GeoSearch Postal Codes API: 2 endpoints with parameters, quota costs, response bodies and code samples in six languages.

Base URL: https://geosearch.dev · Auth: X-API-Key header on every request.
HTML version: https://geosearch.dev/docs/api/postal-codes

Postal/ZIP codes with nearest-neighbor search

## List postal codes

`GET /v1/postal-codes` · Quota cost: 1 unit — 2 with ?within=

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

#### curl

```bash
curl -H "X-API-Key: YOUR_KEY" \
  "https://geosearch.dev/v1/postal-codes?country=US&code=94105"
```

#### Go

```go
// go get github.com/geosearch-dev/geosearch-go
client := geosearch.NewAPIClient(geosearch.NewConfiguration())
ctx := context.WithValue(context.Background(), geosearch.ContextAPIKeys,
    map[string]geosearch.APIKey{"apiKeyAuth": {Key: "YOUR_KEY"}})
codes, _, err := client.PostalCodesAPI.ListPostalCodes(ctx).
    Country("US").Code("94105").Execute()
```

#### Python

```python
# pip install git+https://github.com/geosearch-dev/geosearch-python.git
import geosearch

cfg = geosearch.Configuration(api_key={"apiKeyAuth": "YOUR_KEY"})
with geosearch.ApiClient(cfg) as client:
    codes = geosearch.PostalCodesApi(client).list_postal_codes(
        country="US", code="94105").data
```

#### TypeScript

```typescript
// npm install github:geosearch-dev/geosearch-typescript
import { Configuration, PostalCodesApi } from "@geosearch/client";

const api = new PostalCodesApi(new Configuration({ apiKey: "YOUR_KEY" }));
const codes = (await api.listPostalCodes({ country: "US", code: "94105" })).data;
```

#### Ruby

```ruby
# gem 'geosearch', git: 'https://github.com/geosearch-dev/geosearch-ruby.git'
require "geosearch"

GeoSearch.configure { |c| c.api_key["X-API-Key"] = "YOUR_KEY" }
codes = GeoSearch::PostalCodesApi.new.list_postal_codes(country: "US", code: "94105").data
```

#### PHP

```php
// composer config repositories.geosearch vcs https://github.com/geosearch-dev/geosearch-php.git
// composer require geosearch-dev/geosearch-php
$cfg = GeoSearch\Configuration::getDefaultConfiguration()
    ->setApiKey("X-API-Key", "YOUR_KEY");
$codes = (new GeoSearch\Api\PostalCodesApi(null, $cfg))->listPostalCodes(null, "US", "94105");
// lang, country, code shown — the remaining 6 parameters are positional and optional
```

### Response

```json
{
  "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`. |

## Find nearest postal codes

`GET /v1/postal-codes/nearest` · Quota cost: 1 unit

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

#### curl

```bash
curl -H "X-API-Key: YOUR_KEY" \
  "https://geosearch.dev/v1/postal-codes/nearest?lat=37.7749&lon=-122.4194&limit=3"
```

#### Go

```go
// go get github.com/geosearch-dev/geosearch-go
client := geosearch.NewAPIClient(geosearch.NewConfiguration())
ctx := context.WithValue(context.Background(), geosearch.ContextAPIKeys,
    map[string]geosearch.APIKey{"apiKeyAuth": {Key: "YOUR_KEY"}})
codes, _, err := client.PostalCodesAPI.NearestPostalCode(ctx).
    Lat(37.7749).Lon(-122.4194).Limit(3).Execute()
```

#### Python

```python
# pip install git+https://github.com/geosearch-dev/geosearch-python.git
import geosearch

cfg = geosearch.Configuration(api_key={"apiKeyAuth": "YOUR_KEY"})
with geosearch.ApiClient(cfg) as client:
    codes = geosearch.PostalCodesApi(client).nearest_postal_code(
        37.7749, -122.4194, limit=3).data
```

#### TypeScript

```typescript
// npm install github:geosearch-dev/geosearch-typescript
import { Configuration, PostalCodesApi } from "@geosearch/client";

const api = new PostalCodesApi(new Configuration({ apiKey: "YOUR_KEY" }));
const codes = (await api.nearestPostalCode({ lat: 37.7749, lon: -122.4194, limit: 3 })).data;
```

#### Ruby

```ruby
# gem 'geosearch', git: 'https://github.com/geosearch-dev/geosearch-ruby.git'
require "geosearch"

GeoSearch.configure { |c| c.api_key["X-API-Key"] = "YOUR_KEY" }
codes = GeoSearch::PostalCodesApi.new.nearest_postal_code(37.7749, -122.4194, limit: 3).data
```

#### PHP

```php
// composer config repositories.geosearch vcs https://github.com/geosearch-dev/geosearch-php.git
// composer require geosearch-dev/geosearch-php
$cfg = GeoSearch\Configuration::getDefaultConfiguration()
    ->setApiKey("X-API-Key", "YOUR_KEY");
$codes = (new GeoSearch\Api\PostalCodesApi(null, $cfg))->nearestPostalCode(37.7749, -122.4194, null, 3);
// lat, lon, lang, limit — the full positional parameter list
```

### Response

```json
{
  "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 |

