Documentation navigation

Boundaries API

Boundary polygons as GeoJSON. Charges 5 quota units per served polygon; region boundaries require a paid plan, country boundaries do not.

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

Fetch an area's boundary polygon as GeoJSON

GET/v1/boundaries/{geoname_id}

Quota cost: 5 units Try it

Returns the boundary polygon for one country or region as a bare GeoJSON geometry.

COSTS 5 QUOTA UNITS

This endpoint charges five units against the monthly quota, not one. The premium is priced on PAYLOAD rather than on query time: country GeoJSON averages 62.6 KB and reaches 1.9 MB, and region GeoJSON reaches 2.8 MB — two to three orders of magnitude above an ordinary list response.

?simplify= does NOT reduce the cost. It trades server CPU for client bytes (simplification measures 209–248 ms against 10.3 ms for the plain fetch), so the route is expensive to serve either way.

REJECTIONS COST ONE UNIT, NOT FIVE. A 400, a 422, either 404 and the 403 below all charge the standard single unit, so probing which ids are fetchable — and bouncing off the paywall — is not billed at the premium rate. Only a request that actually reaches the polygon fetch is charged five, including one that reaches it and then times out.

Plan requirements

REGION boundaries require a paid plan. COUNTRY boundaries are available on every plan, including Free. A Free key requesting a region boundary receives a 403 tier_upgrade_required carrying an upgrade link — a visible refusal, not a silent omission.

This is the same split the geometry field follows on the country and region endpoints, with one deliberate difference: there the field is simply absent from a 200, while here the refusal is explicit and tells you what to do about it.

Response shape

data.geometry is a BARE GeoJSON geometry — the {"type": ..., "coordinates": ...} object — and NOT a GeoJSON Feature. There is no properties wrapper; the three sibling fields carry that information. This matches the geometry /v1/countries/{code} and /v1/regions/{id} already return.

type is Polygon OR MultiPolygon. Do not pin it: applying ?simplify= can collapse a MultiPolygon into a Polygon for areas whose smaller parts disappear at the requested tolerance.

?fields= IS NOT SUPPORTED on this endpoint and is ignored if sent. Field selection here works by serialising the whole object and then dropping keys, so ?fields=name on a 1.9 MB polygon would build the polygon in full and discard it — strictly more expensive than not sending the parameter. Callers who want only the name should use /v1/countries/{code} or /v1/regions/{id}.

Parameters

Name Type Required Description
geoname_id integer required

GeoNames id of a country or a region. A city id — or any id that does not name an area — is a 404 area_not_an_area, not a 400.

simplify number optional

Douglas-Peucker tolerance in EPSG:4326 DEGREES, applied before the polygon is serialised. Omit it for full precision.

DEGREES, NOT METRES. The upper bound of 10 is roughly 1,100 km, chosen to make the unit obviously wrong to anyone who typed a value in metres. Simplification stops changing the shape above about 1 degree, so values beyond that buy nothing.

0 is accepted and is a no-op: the response reports simplify: null, because no tolerance was actually applied.

A negative, non-finite or out-of-range value is a 422, never a 500.

SUPPLY IT AT MOST ONCE. ?simplify=0.01&simplify=0.5 is a 422 rather than a request served with one of the two values silently dropped: two tolerances are two conflicting instructions, and the server does not guess which was meant.

lang string optional

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

Code samples

Response

{
  "data": {
    "geoname_id": 6252001,
    "name": "United States",
    "type": "country",
    "geometry": {
      "type": "MultiPolygon",
      "coordinates": [
        [
          [
            [
              -124.7,
              48.4
            ],
            [
              -124.6,
              48.4
            ],
            [
              -124.6,
              48.3
            ],
            [
              -124.7,
              48.4
            ]
          ]
        ]
      ]
    },
    "simplify": null
  }
}

Errors

Status Code When
400 bad_request The path segment is not an integer. A malformed id is a 400 rather than a 404, so a typo is distinguishable from a coverage gap.
401 authentication_required No API key was supplied
401 authentication_failed The supplied API key is not valid
403 tier_upgrade_required The authenticated key's plan does not include the requested feature. NOT RETRYABLE, AND THIS MATTERS FOR CLIENT CODE. Generated SDKs and hand-written clients commonly retry 429 with backoff. This is a 403 and must not be routed into that path: no amount of waiting changes the answer, because nothing is exhausted and no window resets. The only resolution is to raise the plan at `error.upgrade.upgrade_url`. It carries no `Retry-After` and no `X-RateLimit-*` semantics of its own, which is the machine-readable form of the same statement.
404 area_not_an_area The id is not an area
404 area_no_boundary No boundary polygon is held for that area
422 validation_error `simplify` was malformed. `error.details` names the field and what was wrong with it. A 422 HERE, WHERE `/v1/resolve` USES 400 FOR ITS COORDINATES. That is deliberate: this is a shape failure decidable from the request text alone and it has a field name to report, which is this API's 422 convention. The two endpoints follow two conventions; see the 400 on `/v1/resolve`.
429 rate_limit_exceeded Per-second throttle exceeded
429 quota_exceeded Monthly quota exhausted
503 area_query_timeout Building the boundary polygon 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 polygon is large enough that assembling and serialising it ran out of its time budget. It is worth retrying. THE EFFECTIVE REMEDY ON THIS ROUTE IS `simplify`, not `bbox` — this operation has no `bbox` and no `within`. A larger tolerance means fewer vertices to generalise, serialise and transmit, so a request that times out at the full resolution frequently succeeds at `?simplify=0.01`.

400 bad_request

{
  "error": {
    "code": "bad_request",
    "message": "invalid boundary geoname_id",
    "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"
  }
}

403 tier_upgrade_required

{
  "error": {
    "code": "tier_upgrade_required",
    "message": "Boundary geometry is not included on your current plan. Upgrade to fetch boundary polygons.",
    "request_id": "req_abc123",
    "upgrade": {
      "upgrade_url": "https://geosearch.dev/dashboard#billing"
    }
  }
}

404 area_not_an_area

{
  "error": {
    "code": "area_not_an_area",
    "message": "The path segment must be the GeoNames id of a country or a region. The id supplied names something else — a city, for instance — or names nothing at all.",
    "request_id": "req_abc123"
  }
}

404 area_no_boundary

{
  "error": {
    "code": "area_no_boundary",
    "message": "That area exists, but we hold no boundary polygon for it yet. Coverage is strongest for countries and for first- and second-level regions; see the documentation for the current coverage map.",
    "request_id": "req_abc123"
  }
}

422 validation_error

{
  "error": {
    "code": "validation_error",
    "message": "Validation failed",
    "details": [
      {
        "field": "simplify",
        "issue": "must not be negative; a tolerance is a distance, and PostGIS refuses a negative one"
      }
    ],
    "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": "Building that boundary polygon took too long to complete. This is usually temporary — please retry, or ask for a coarser polygon with a larger ?simplify= tolerance.",
    "request_id": "req_abc123"
  }
}