IP geolocation API
One request. You have an IP address, or you want the one making the request. This returns a country, a region, a city and a coordinate.
Every request needs an API key; see Authentication. A successful lookup is one normal API call. There is no separate plan to turn on.
import { Unmap } from "@unmap/sdk";
const unmap = new Unmap({ key: "um_live_..." });
const here = await unmap.geoip.locate();
if (here.found) console.log(here.city, here.country_code);
// Show here.attribution.text, linked to here.attribution.url.curl "https://api.unmap.dev/geoip?ip=8.8.8.8&key=um_live_..."const here = await unmap.geoip.locate("8.8.8.8");Lookup
GET /geoip
| Parameter | Required | Description |
|---|---|---|
ip | no | An IPv4 or IPv6 address. Omit it to look up the address that made the request. |
{
"ip": "8.8.8.8",
"found": true,
"country_code": "US",
"country": "United States",
"region": "California",
"city": "Mountain View",
"coordinates": [-122.085, 37.422],
"accuracy": "city",
"release": "2026-11",
"attribution": { "text": "IP Geolocation by DB-IP", "url": "https://db-ip.com" }
}coordinates is [longitude, latitude], the same order as the rest of the API. ip is the
canonical form of the address that was looked up: dotted IPv4, or IPv6 in its shortest form. An
IPv4-mapped IPv6 address comes back as IPv4. release is the month of the database that
answered. region and city are null when the database does not have them.
Your own address
const here = await unmap.geoip.locate();With no ip, the answer is for the machine making the request. From a browser that is the
visitor. From a server it is the server. A request that goes through a proxy is located at the
proxy, so a server that wants its visitor's location must pass the visitor's address as ip.
Addresses with no answer
An address the database does not list, such as unassigned IPv6 space, is 200 with
found: false and no location fields.
{ "ip": "1000::1", "found": false, "release": "2026-11", "attribution": { "text": "IP Geolocation by DB-IP", "url": "https://db-ip.com" } }Private, loopback, link-local and other reserved ranges are not looked up. They are 400
invalid_ip, because there is nothing to find. NAT64 (64:ff9b::/96), 6to4 (2002::/16) and
IPv4-mapped addresses are read through to the IPv4 address inside them first, so
::ffff:10.0.0.1 is rejected the same way 10.0.0.1 is.
Accuracy
Treat the answer as the area an internet provider serves, not a place. It is often only the right country, or a nearby city, and mobile networks, VPNs and corporate networks can place a person hundreds of kilometres from where they are. Use it to pick a language, a currency or a starting map view. Do not use it to identify a person or a street address.
Errors
| Status | Code | When |
|---|---|---|
400 | invalid_ip | ip is not an IPv4 or IPv6 address, or it is private or reserved. Also when you omitted it and the caller's address could not be read, or is private. Pass ip. |
400 | bad_request | You omitted ip and there is no caller address to default to. |
503 | service_unavailable | The dataset is missing, unreadable or invalid. Retry later. |
{ "error": "ip must be an IPv4 or IPv6 address", "code": "invalid_ip" }Only a 200 is billed. Every 400 and 503 is free.
Privacy
The address you look up is not logged, stored or counted; the request itself counts as one
call, like any other. Usage rows carry the endpoint and nothing else, and the address is not part of any cache key. Responses carry
Cache-Control: private, max-age=300: your browser may reuse one for five minutes, and no
shared cache stores it.
Attribution
The data is the IP to City Lite database from DB-IP, under
CC BY 4.0. Every 200 carries
attribution. Show attribution.text, linked to attribution.url, wherever your product
shows or uses the result. The attribution page has the full credit.
Coverage and updates
The lookup is worldwide. Every other Unmap API is Canadian-first; this one is not. DB-IP
publishes a new file each month. It is checked weekly, built into small immutable objects, and
promoted by one manifest write, so a bad release is rolled back by pointing at the last one. If
a refresh fails, the Worker keeps serving the previous release for up to an hour before it
answers 503.