Reverse geocode a coordinate to its country, region and municipality

You have a coordinate — a GPS fix, a tap on a map, a delivery location — and you need to know which country, state or province, and city or district it falls in, as codes your systems can store. This guide does that with one HTTP request.

Examples run against the live API on 17 September 2026.

Is this the right tool?

This is reverse geocoding to administrative areas: coordinate in, the areas that contain it out. It is the right tool when the answer you need is “which region is this in?”

  • Yes: tax or pricing by region, which country a sign-up came from, routing an order to a regional team, tagging events with a municipality.
  • No: turning a coordinate into a street address or postcode. Results stop at the municipality. For addresses, see the comparison of reverse geocoding APIs.

1. Get an API key

To try it without signing up, fetch the shared demo key. It is capped at 1,000 lookups a month across everyone using it, so use it for experiments only.

shell
export ATLASFETCH_KEY=$(curl -s https://api.atlasfetch.xyz/demo/key | jq -r .key)

For your own limits, sign up free and create a key in the dashboard. Keys are shown once and stored hashed.

2. Look up a coordinate

Send lat and lng to GET /location/lookup with your key as a Bearer token.

curl
curl -H "Authorization: Bearer $ATLASFETCH_KEY" \
  "https://api.atlasfetch.xyz/location/lookup?lat=-33.9249&lng=18.4241"
JavaScript (Node 18+, or any runtime with fetch)
const res = await fetch(
  "https://api.atlasfetch.xyz/location/lookup?lat=-33.9249&lng=18.4241",
  { headers: { Authorization: `Bearer ${process.env.ATLASFETCH_KEY}` } },
);
if (!res.ok) throw new Error(`AtlasFetch ${res.status}: ${(await res.json()).error}`);

const { base } = await res.json();
console.log(base.country?.code, base.region?.code, base.municipal?.name);
// ZA ZA-WC City of Cape Town
Python (requests)
import os
import requests

res = requests.get(
    "https://api.atlasfetch.xyz/location/lookup",
    params={"lat": -33.9249, "lng": 18.4241},
    headers={"Authorization": f"Bearer {os.environ['ATLASFETCH_KEY']}"},
    timeout=10,
)
res.raise_for_status()

base = res.json()["base"]
print(base["country"]["code"], base["region"]["code"], base["municipal"]["name"])
# ZA ZA-WC City of Cape Town

3. Read the response

response · 200
{
  "base": {
    "country":   { "code": "ZA",     "name": "South Africa" },
    "region":    { "code": "ZA-WC",  "name": "Western Cape" },
    "municipal": { "code": "B-ZA-1", "name": "City of Cape Town" }
  },
  "sets": {},
  "errors": []
}
FieldWhat it isSafe to store as a key?
country.codeISO 3166-1 alpha-2Yes
region.codeISO 3166-2 subdivision (state, province, nation)Yes
municipal.codeThe official ISO 3166-2 code where OpenStreetMap carries one, e.g. GB-WSM for the City of Westminster; otherwise a generated B-<country>-<n>ISO codes, yes. Generated B- codes, no — they are not yet stable across a full data reload. Store the name alongside.

Every response carries errors[], empty or not, and an X-Lookups-Remaining header with what is left of your monthly allowance.

Municipal is not always a city

municipal is the finest administrative unit availablefor that place, not a consistent kind of thing. Where a country’s finest tier is patchy, a coarser but complete tier answers instead, because for most uses a county beats nothing. Real results:

Coordinateregionmunicipal
Downtown Los AngelesUS-CA CaliforniaLos Angeles (a city)
Rural KansasUS-KS KansasBarton County (a county)
JohannesburgZA-GP GautengCity of Johannesburg Metropolitan Municipality
LondonGB-ENG EnglandGB-WSM City of Westminster (a London borough)
Do not label the field “City” in your UI or schema. “Municipality” or “Local area” is honest about what comes back.

Ask only for the layers you need

base takes a comma list of country, region and municipal. A call costs one lookup however many layers you ask for, so this is about response size and clarity, not price.

curl
curl -H "Authorization: Bearer $ATLASFETCH_KEY" \
  "https://api.atlasfetch.xyz/location/lookup?lat=-33.9249&lng=18.4241&base=country,region"

When nothing contains the point

A layer that matched nothing comes back as null, never missing. Mid-Atlantic:

response · 200
{ "base": { "country": null, "region": null, "municipal": null }, "sets": {}, "errors": [] }

Coastal points can do this for one layer only: a region boundary may extend over territorial water while the municipality’s does not. Handle null per field.

Also get an H3 cell or Plus Code

Add encode=h3,pluscode to get the point as grid codes in the same call, useful for bucketing or joining with other data. Precision is h3res (0-15, default 9, about 400 m) and pluslen (default 10, about 14 m). It costs nothing extra.

response · 200 (base=country&encode=h3,pluscode)
{
  "base": { "country": { "code": "ZA", "name": "South Africa" } },
  "sets": {},
  "encoded": { "h3": "89ad361519bffff", "pluscode": "4FRW3CGF+2J" },
  "errors": []
}

Grid codes are output only. An H3 cell or Plus Code names an area, not a point, so neither is accepted as the location.

Errors and billing

response · 400 (lng missing)
{ "error": "lng is required. A location is given as a coordinate pair, e.g. ?lat=51.5072&lng=-0.1276" }
StatusMeaningBilled?
200Answered. Check errors[] for anything skipped.Yes, even if nothing matched
400Invalid parameters; the message says whatNo
401Missing, malformed or revoked keyNo
402Monthly allowance used up; retrying will not help until it resetsNo
429Per-key rate limit; honour Retry-AfterNo

Metering happens before matching, so a lookup that matches nothing still counts: the work was done either way. The coordinates you send are not stored.

Next

Using an AI assistant? The AtlasFetch MCP server lets Claude, Cursor and other clients run these lookups directly. Machine-readable reference: llms.txt.

Data

Reference boundaries derive from OpenStreetMap under the ODbL; attribution and share-alike obligations pass through to you. See data attribution.