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.
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 -H "Authorization: Bearer $ATLASFETCH_KEY" \
"https://api.atlasfetch.xyz/location/lookup?lat=-33.9249&lng=18.4241"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 Townimport 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 Town3. Read the response
{
"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": []
}| Field | What it is | Safe to store as a key? |
|---|---|---|
country.code | ISO 3166-1 alpha-2 | Yes |
region.code | ISO 3166-2 subdivision (state, province, nation) | Yes |
municipal.code | The 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:
| Coordinate | region | municipal |
|---|---|---|
| Downtown Los Angeles | US-CA California | Los Angeles (a city) |
| Rural Kansas | US-KS Kansas | Barton County (a county) |
| Johannesburg | ZA-GP Gauteng | City of Johannesburg Metropolitan Municipality |
| London | GB-ENG England | GB-WSM City of Westminster (a London borough) |
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 -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:
{ "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.
{
"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
{ "error": "lng is required. A location is given as a coordinate pair, e.g. ?lat=51.5072&lng=-0.1276" }| Status | Meaning | Billed? |
|---|---|---|
| 200 | Answered. Check errors[] for anything skipped. | Yes, even if nothing matched |
| 400 | Invalid parameters; the message says what | No |
| 401 | Missing, malformed or revoked key | No |
| 402 | Monthly allowance used up; retrying will not help until it resets | No |
| 429 | Per-key rate limit; honour Retry-After | No |
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.