I Gave 9 Free IP-Geolocation APIs One IP. 3 Pointed to a Farm in Kansas.
Three keyless APIs, one IP, one moment on July 24, 2026. I asked each of them where 8.8.8.8 is.
$ curl -s https://get.geojs.io/v1/ip/geo/8.8.8.8.json
"latitude":"37.751","longitude":"-97.822","accuracy":1000
$ curl -s https://ip.guide/8.8.8.8
"latitude": 37.751, "longitude": -97.822, "city": null
$ curl -s https://reallyfreegeoip.org/json/8.8.8.8
"latitude":37.751,"longitude":-97.822,"time_zone":"America/Chicago"
Same coordinate, to the digit, from three services that do not share a vendor. 37.751, -97.822 is not Google’s DNS server. It is not a data center. It is a spot in a field near Potwin, Kansas, and if you have read Kashmir Hill’s 2016 piece for Fusion, “How an internet mapping glitch turned a random Kansas farm into a digital hell”, you already know why. That point is the geographic center of the contiguous United States. It is the value a database returns when it has no idea, dressed up as a fact and served on an HTTP 200.
The one idea
A keyless IP-geolocation API turns an IP address into a country, a city and a coordinate with no API key, no signup and no card. I verified nine with a live curl on July 24, 2026, and the table is one screen down. The catch is not price. It is that an IP has no true coordinate. These APIs hide that behind a precise-looking number, and not one of them raises a flag that says “this is a fallback”.
Give one IP to all nine and their answers scatter across a continent. On this run the same 8.8.8.8 came back as Ashburn, Virginia from one, San Jose, California from another, and the Kansas centroid from three. That is about 3,800 km of disagreement over a single input, and every answer arrived as HTTP 200 with a latitude and a longitude sitting there looking like measurements. They are not measurements. They are guesses about a network, and some of them are the polite word for “unknown”.
The 9 keyless endpoints, verified
Every row is a live check of 8.8.8.8 on July 24, 2026. I ran the negative controls first, so I know the probe can report failure and not just echo happy paths: a bogus IP 999.999.999.999 returned HTTP 200 with "success":false from ipwho.is and a plain 404 from ip.guide (two providers, two different ways of admitting garbage), and two nonexistent hosts returned curl exit 35 and 6 with an HTTP code of 000. The probe can tell live from dead and valid from junk. Checking beats trusting the list.
| # | Provider | Endpoint | Keyless | Coordinate for 8.8.8.8, as it arrived | Tell |
|---|---|---|---|---|---|
| 1 | ip-api.com | http://ip-api.com/json/8.8.8.8 | yes, HTTP only | 39.03, -77.5 Ashburn VA | HTTPS returns 403 “SSL unavailable, order a key” |
| 2 | ipwho.is | https://ipwho.is/8.8.8.8 | yes | 37.3361663, -121.8905913 San Jose CA | seven decimal places on a guess |
| 3 | freeipapi.com | https://freeipapi.com/api/json/8.8.8.8 | yes | 37.422, -122.085 | 302 then 200, no key |
| 4 | ipinfo.io | https://ipinfo.io/8.8.8.8/json | yes, no token | 37.4056, -122.0775 Mountain View | ships "anycast": true in the same body |
| 5 | ipbase.com | https://api.ipbase.com/v1/json/8.8.8.8 | yes, legacy v1 | 37.413921, -122.085078 | six decimals |
| 6 | geojs.io | https://get.geojs.io/v1/ip/geo/8.8.8.8.json | yes | 37.751, -97.822 | US default centroid, ships "accuracy":1000 |
| 7 | ip.guide | https://ip.guide/8.8.8.8 | yes | 37.751, -97.822, city: null | same centroid, null city |
| 8 | reallyfreegeoip.org | https://reallyfreegeoip.org/json/8.8.8.8 | yes | 37.751, -97.822 | same centroid, blank fields |
| 9 | country.is | https://api.country.is/8.8.8.8 | yes | none, returns {"country":"US"} | the honest one: no coordinate at all |
Nine services, no keys, and the only one I trust without an argument is the one at the bottom that refuses to give you a coordinate.
The same IP, coordinates 3,800 km apart
Line up the answers for a single input and the spread is the story.
ip-api.com 39.03, -77.5 Ashburn, Virginia (east coast)
ipwho.is 37.3361663, -121.8905913 San Jose, CA (west coast)
freeipapi.com 37.422, -122.085
ipinfo.io 37.4056, -122.0775 Mountain View, CA
ipbase.com 37.413921, -122.085078
geojs.io 37.751, -97.822 Kansas centroid
ip.guide 37.751, -97.822 Kansas centroid
reallyfreegeoip 37.751, -97.822 Kansas centroid
country.is US country only
Great-circle distance from the Ashburn answer to the San Jose answer, computed from those exact coordinates: about 3,800 km, roughly 2,390 miles. That is coast to coast for the same eight bytes. Nobody is lying, exactly. 8.8.8.8 is Google Public DNS, an anycast address, which means the same IP is announced from many places at once and has no single physical location by design. Every provider is estimating from a different snapshot of where Google’s network appears, and they disagree because the true answer is “it depends where you are standing”.
That is the honest reading. The APIs do not offer it. They offer a coordinate.
37.751, -97.822 is not a place. It’s an admission.
Three of the nine parked 8.8.8.8 at the same point: 37.751, -97.822. That is not a coincidence and it is not Kansas being interesting. It is the geographic center of the contiguous United States, and it has a documented history. MaxMind, whose free GeoLite database sits under a large slice of the free IP-geolocation world, needed a coordinate to return for US IPs it could only place at country level. It picked the center of the country. The rounding put it on a rented farm outside Potwin, Kansas, and for a decade its residents were visited by police, IRS agents and angry people who had traced stolen phones, scam calls and worse to their front yard, because roughly 600 million “somewhere in the US” IPs all resolved to their lawn. Kashmir Hill reported the whole thing for Fusion in 2016 (linked above).
The relevant part for your code is not the human cost. It is that this coordinate is a placeholder wearing the costume of data. When geojs, ip.guide and reallyfreegeoip hand you 37.751, -97.822, the true message is “we know the country and nothing else”. But there is no confidence: low field, no is_default: true, no null. There is a latitude, a longitude, and an HTTP 200. If your fraud scorer, your “nearest warehouse” router or your compliance geofence reads that number as a location, you have just placed a user in a field in Kansas and moved on. All three even set the time zone to America/Chicago, so the lie is internally consistent.
ipinfo.io knows there’s no point. It gives you one anyway.
If the Kansas centroid is the quiet failure, ipinfo.io is the loud one. Here is its full answer for 8.8.8.8 on the same run:
{
"ip": "8.8.8.8",
"hostname": "dns.google",
"city": "Mountain View",
"loc": "37.4056,-122.0775",
"org": "AS15169 Google LLC",
"anycast": true
}
Read the last two fields together. "anycast": true is ipinfo telling you, in the same breath, that this IP has no single location. And "loc": "37.4056,-122.0775" is ipinfo giving you a single location anyway, to four decimal places. The provider knows the coordinate is meaningless and prints it with precision. ipwho.is does the same thing without the honesty of the anycast flag: 37.3361663, -121.8905913, seven decimal places, which is a resolution of about 11 millimeters, on a value that another provider on the same input placed 3,800 km away. Seven decimals is not accuracy. It is a float that happened to have that many digits, and printing all of them is how a guess starts to look like a survey.
Only two of the nine even hint at their own uncertainty. geojs ships "accuracy":1000, which quietly says “within about 1000 km, good luck”, and country.is refuses to invent a coordinate at all. The other seven give you decimals and let you assume they mean something.
This is not the geocoding post. It’s the opposite.
I wrote a companion piece on free geocoding APIs with no key, and it is worth being explicit about why that one is a happier story, because the two look similar and are not.
Geocoding takes an address and returns a coordinate. “Eiffel Tower, Paris” resolves to 48.8584, 2.2945 because the Eiffel Tower is genuinely there. The input names a real place, the output is that place, and the operation is deterministic: it either resolves or it does not, and when it resolves it is right. The failure modes are rate limits and attribution, not truth.
IP geolocation takes a network identifier and returns a coordinate. An IP is not a place. It is an entry in a routing table that a provider maps, statistically, to wherever that network tends to appear. The output is a probability distribution flattened into a single point, and the flattening is where the Kansas centroid comes from. An address geocoder cannot silently swap the Eiffel Tower for a farm in Kansas. An IP geolocator does it nine hundred times a second and returns 200 every time. Same-looking lat, lon output, completely different epistemics underneath. Treat the address answer as a fact to verify and the IP answer as a guess to bound.
The fix: treat the coordinate as a guess with a floor
You cannot make IP geolocation accurate. You can stop your code from believing it. The rule I use: never key on a bare lat, lon. Read the body’s own flags first, catch the default centroid explicitly, and downgrade to country level when the point is really “unknown”.
This is runnable, standard library only, one live call:
import urllib.request, json
# 37.751, -97.822 is the geographic center of the contiguous US, the value
# MaxMind returned for any US IP it could not actually place. Three keyless
# providers still hand it back. It means "we don't know", printed as a point.
US_DEFAULT = (37.751, -97.822)
def _f(x):
try: return float(x)
except (TypeError, ValueError): return None
def is_default_centroid(lat, lon, tol=0.02):
if lat is None or lon is None: return False
return abs(lat - US_DEFAULT[0]) < tol and abs(lon - US_DEFAULT[1]) < tol
def locate(ip):
req = urllib.request.Request(f"https://ipwho.is/{ip}",
headers={"User-Agent": "geo-guard/1.0"})
d = json.load(urllib.request.urlopen(req, timeout=5))
# HTTP 200 is not success. The body carries its own flag; trust that.
if d.get("success") is False:
return {"ip": ip, "confidence": "none", "why": d.get("message")}
lat, lon = _f(d.get("latitude")), _f(d.get("longitude"))
cc = d.get("country_code")
if is_default_centroid(lat, lon):
return {"ip": ip, "country": cc, "lat": None, "lon": None,
"confidence": "country-only", "why": "US default centroid"}
return {"ip": ip, "country": cc, "lat": lat, "lon": lon,
"confidence": "city-guess"}
for ip in ["8.8.8.8", "999.999.999.999"]:
print(ip.ljust(18), locate(ip))
Output on July 24, 2026:
8.8.8.8 {'ip': '8.8.8.8', 'country': 'US', 'lat': 37.3361663, 'lon': -121.8905913, 'confidence': 'city-guess'}
999.999.999.999 {'ip': '999.999.999.999', 'confidence': 'none', 'why': 'Invalid IP address'}
The point is the labels, not the coordinate. Every value leaves the function tagged none, country-only or city-guess, and nothing downstream ever sees a naked latitude it can mistake for a fix. Run the same is_default_centroid check across all nine providers’ answers for 8.8.8.8 and it fires exactly where it should:
geojs.io 37.751, -97.822 DEFAULT CENTROID -> downgrade to country-only
ip.guide 37.751, -97.822 DEFAULT CENTROID -> downgrade to country-only
reallyfreegeoip.org 37.751, -97.822 DEFAULT CENTROID -> downgrade to country-only
ipwho.is 37.3361663, ... city-guess (keep, but label as estimate)
country.is none country-only (never claimed a point)
Three collapse to country, the rest stay labeled guesses, and the one honest provider needs no help. That is the whole defense: a confidence field you add because the provider would not.
The ones that did not make the list
Three services that show up on these roundups did not earn a row, and I want to say why with what I actually saw:
- ipapi.co returned a clean
200with37.42301, -122.083352on my run, but the analyst who set this up hit429and a “sign up for a paid plan” message the day before. Its free tier is real but rate-limited hard enough that it will fail under any load, so it is a demo, not a dependency. - geoplugin.net returned
403with"geoPlugin API is no longer available for free use ... upgrade to a paid plan". Dead for free, plainly. - ipapi.is answered once with a rich threat-intelligence body (
is_vpn,is_datacenter,is_abuser) and returned nothing on the very next call from the same machine, so I could not verify it reliably. It also flagged8.8.8.8asis_vpn: trueandis_abuser: true, which is its own kind of confident wrong. It is an IP-reputation API more than a geolocator.
Where this comes from, and what I did not measure
I run scrapers and data pipelines in production, 32 published actors and something over 2,000 runs. IP geolocation shows up on the enrichment side of that work: you scrape a directory, you get IPs or hosts, and someone downstream wants a map. The lesson that keeps repeating is the same one as this post: the field that looks most like clean data, a tidy lat, lon, is often the softest number in the record, and it is soft precisely because it looks hard.
I want to be precise about what I did not do. I did not run these nine providers inside those production runs and I am not going to pretend a coordinate broke a specific job on a specific date to give this a number. The honest artifact is the one above: a live probe you can rerun today, showing nine APIs place one IP nine different ways and three of them at a farm in Kansas. If you see a failure count in a post like this with no method next to it, ask where it came from.
Honest limits
- This is a snapshot. All nine were keyless on July 24, 2026. Free status, rate limits and even the default coordinate can change without notice. Re-curl before you rely on any of it.
- 8.8.8.8 is anycast, which is the hardest case on purpose. A residential IP would scatter less, because it has something closer to a real location. I picked the IP that has none to make the disagreement visible, so read the 3,800 km as an upper-ish bound, not a typical error.
- The Kansas centroid is MaxMind’s, and I am attributing the human story, not claiming I found it. The
37.751, -97.822value and the Potwin, Kansas consequences are documented by Kashmir Hill (Fusion, 2016). What is mine is the live 2026 curl showing three keyless providers still return it. - country.is being “honest” is a design choice, not a virtue. It only returns a country because a country is roughly all an IP reliably tells you. That is the point: it is the one that matches its output to what it actually knows.
- One machine, one network, one afternoon. Values drift, especially the seven-decimal ones. Read the table as evidence of shape, not as constants.
The open question
The default-centroid check is easy because that failure is loud once you know the coordinate. What I do not have a clean answer for is the quiet version: a provider that returns a real-looking city 200 km from the truth, with no anycast flag, no accuracy field and a country that happens to be right. There is nothing in the response to catch. Cross-referencing several providers helps until they all inherit the same MaxMind data and agree on the same wrong point, which is exactly what three of these nine did.
If you consume IP geolocation in something that matters, fraud, compliance, routing, how do you bound a coordinate that is confidently, quietly a little bit wrong? I have partial answers involving accuracy radii, country-first logic and provider disagreement as a signal, and I would rather hear a real one. 👇
Written with AI assistance. Every coordinate, status code, HTTP result and command output above is from my own live curls and negative controls on July 24, 2026, printed as they came out.