GuidesIP GEOLOCATION
Free IP geolocation APIs tested from the browser
We tested 11 free IP geolocation APIs from a browser on 26 October 2026: 10 work without a key, every cross-origin one sends CORS headers, and ip-api.com's free tier is HTTP only, so HTTPS pages block it. Call the API from the visitor's own browser: through any proxy, cors.dev included, the API sees the proxy's IP, and in our test it placed a North Carolina visitor in Ashburn, Virginia.
Every IP API we tested
We ran each API on 26 October 2026 with curl and an Origin: https://example.com header from a datacenter IP (162.19.169.95, OVH) and US residential IPs, then in Chromium 147.0.7727.15 from http://127.0.0.1:8001 and https://example.com. Every cross-origin API here sends CORS headers, so none needs a proxy. More browser-ready endpoints are in our list of 70 free public APIs.
| API | Key | HTTPS on the free tier | CORS | Free limit | Fields |
|---|---|---|---|---|---|
| ipwho.is | None | Yes | Yes, ACAO * | 1,000 requests/day, counted per site for browser calls | City, lat/lon, ASN, timezone |
| ipapi.co | None | Yes | Yes, echoes Origin | Trial: 1,000 per 24 hours, 30,000/month | City, region, lat/lon, ASN |
| ip-api.com | None | No, HTTP only (HTTPS gives 403) | Yes, ACAO * | 45 requests/minute, non-commercial | Country, region, city, lat/lon |
| ipinfo.io | None for /json; free token for Lite | Yes | Yes, ACAO * | Legacy: 50,000/month; Lite: unlimited | City, lat/lon, org; Lite: country and ASN |
| freeipapi.com | None | Yes | Yes, ACAO * | 60 requests/minute | Country, lat/lon |
| GeoJS | None | Yes | Yes, ACAO * | No rate limits (yet) | Country, city when known, lat/lon, ASN |
| ipify | None | Yes | Yes, ACAO * | No limit | IP address only |
| DB-IP | None | Yes | Yes, ACAO * | 500 requests/day | Country, region, city |
| ipdata | None at ipdata.free; key for api.ipdata.co | Yes | Yes, ACAO * | 1,000 city-level lookups/day, then country only | Country, lat/lon, ASN, timezone |
| ipgeolocation.io | Free key | Yes | Yes, echoes Origin | 1,000 credits/day | Not checked (401 without a key) |
| Cloudflare /cdn-cgi/trace | None | Yes | Same origin only | Not an API; troubleshooting endpoint | IP, country, data center |
Why a proxy reports the wrong location
An IP geolocation API locates the IP address that connects to it. A proxy opens its own connection to the API, and cors.dev does not forward the visitor's IP in any header, so the API locates the proxy. Same connections on 26 October 2026, direct and through proxy.cors.dev:
- From a Spectrum residential IP in Laurinburg, North Carolina (
65.191.24.190),ipinfo.ioreturned that IP and Laurinburg. Through the proxy it returned172.68.138.140, Cloudflare, Ashburn, Virginia. api.ipapi.issaid Laurinburg directly and104.22.104.228in Ashburn through the proxy.api.ipify.orgreturned65.191.24.190directly and2a06:98c0:3600::103, a Cloudflare IPv6 address, through the proxy.- From our datacenter, GeoJS returned
162.19.169.95, OVH SAS, France directly, and2a06:98c0:3600::103, Cloudflare, United States through the proxy.
A shared proxy IP also burns shared quotas. Through the proxy, ipwho.is answered 429 Rate limit exceeded, ipinfo.io 429 Rate limit hit, ipapi.co 429, freeipapi.com a 403 Cloudflare page, and DB-IP the error TEMPORARY_BLOCKED. Run this snippet in your console to see the difference yourself:
const target = 'https://get.geojs.io/v1/ip/geo.json';
async function lookup(url, init) {
const response = await fetch(url, init);
if (!response.ok) {
throw new Error(`${url} failed: ${response.headers.get('X-Cors-Error') ?? response.status}`);
}
return response.json();
}
const direct = await lookup(target);
const proxied = await lookup('https://proxy.cors.dev/' + target, { credentials: 'omit' });
console.table({ direct, proxied }, ['ip', 'city', 'country', 'organization_name']);Locate the visitor with a fallback chain
Free tiers rate-limit by IP or by site, and any provider can be down or block a network. Chain three providers with a 3-second timeout each so one slow API can't hang your page. Every provider in the chain sends Access-Control-Allow-Origin: *, so the browser calls each one directly. Pass the coordinates to a free weather API to show local weather.
const providers = [
{
url: 'https://get.geojs.io/v1/ip/geo.json',
pick: (d) => ({ ip: d.ip, country: d.country_code, city: d.city, lat: Number(d.latitude), lon: Number(d.longitude) }),
},
{
url: 'https://ipinfo.io/json',
pick: (d) => {
const [lat, lon] = (d.loc ?? ',').split(',').map(Number);
return { ip: d.ip, country: d.country, city: d.city, lat, lon };
},
},
{
url: 'https://ipwho.is/',
pick: (d) => d.success && { ip: d.ip, country: d.country_code, city: d.city, lat: d.latitude, lon: d.longitude },
},
];
async function locateVisitor(timeoutMs = 3000) {
const failures = [];
for (const { url, pick } of providers) {
try {
const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const location = pick(await response.json());
if (location?.country) return { ...location, source: new URL(url).hostname };
throw new Error('no location in the response');
} catch (error) {
failures.push(`${new URL(url).hostname}: ${error.message}`);
}
}
throw new Error(`Every IP lookup failed: ${failures.join('; ')}`);
}
console.log(await locateVisitor());Limits that bite in production
- ipwho.is allows 1,000 free requests per day, and browser calls are counted per site (the
Origin), shared by all your visitors. Over the limit you get 429 withRetry-After. From one IP,Origin: https://example.comgot 429 withRetry-After: 42568,Origin: http://127.0.0.1:8001got 429 with 72,094 seconds (about 20 hours), and a fresh origin got 200. - ip-api.com allows 45 requests per minute from each IP address for non-commercial use, and bans IPs that keep going over for 1 hour. HTTPS returns 403
"SSL unavailable for this endpoint", and Chromium 147 onhttps://example.comblocked the HTTP URL:Mixed Content: The page at 'https://example.com/' was loaded over HTTPS, but requested an insecure resource 'http://ip-api.com/json/'. This request has been blocked; the content must be served over HTTPS. - ipapi.co is a free trial: up to 30,000 lookups per month, 1,000 in 24 hours.
- ipinfo.io:
https://ipinfo.io/jsonworks without a token at city level, but this legacy free API is limited to 50,000 requests per month. IPinfo Lite needs a free token and is unlimited, with country and ASN only. - freeipapi.com allows 60 requests per minute, commercial use included.
https://freeipapi.com/api/jsonnow redirects (307) tohttps://free.freeipapi.com/api/v1/json, so call the new URL. - ipdata:
api.ipdata.coneeds a key, whilehttps://ipdata.free/needs none and gives 1,000 city-level lookups a day, then country only.
Country lookup on a Cloudflare site
If your site runs behind Cloudflare, fetch /cdn-cgi/trace from your own domain: same origin, no CORS, no third party. It returns key=value lines including ip, loc (country code) and colo (data center). On cors.dev from our datacenter it returned ip=162.19.169.95, loc=DE, colo=FRA. Cloudflare documents it for troubleshooting which data center serves a request, not as an API.
const response = await fetch('/cdn-cgi/trace');
if (!response.ok) {
throw new Error(`/cdn-cgi/trace failed: ${response.status}`);
}
const trace = Object.fromEntries(
(await response.text())
.trim()
.split('\n')
.map((line) => [line.slice(0, line.indexOf('=')), line.slice(line.indexOf('=') + 1)]),
);
console.log({ ip: trace.ip, country: trace.loc, dataCenter: trace.colo });How accurate IP location is
Use IP location for a country default and treat the city as a guess. Our datacenter IP 162.19.169.95 was in Germany for ipwho.is, ipinfo.io and ipapi.co, and in France for GeoJS, ip.sb, ipapi.is and ipdata.free. For 65.191.24.190, ipinfo.io and ipapi.is said Laurinburg, North Carolina, while ipquery said New York City.
On a VPN or mobile data, the API sees the VPN exit or the carrier's gateway and reports that location. For an exact position, ask for navigator.geolocation permission and turn the coordinates into a place name with a free geocoding API.
Good questions.
Can I use a CORS proxy for IP geolocation?
No. The API locates whatever IP connects to it, so through proxy.cors.dev it found a Cloudflare data center in Ashburn, Virginia instead of our visitor in North Carolina. All the cross-origin APIs we tested send CORS headers, so call them from the visitor's browser.
Which free IP geolocation API works over HTTPS without a key?
Eight of the APIs we tested: ipwho.is, ipapi.co, ipinfo.io/json, free.freeipapi.com, get.geojs.io, api.db-ip.com, ipdata.free and api.ipify.org, which returns the IP only. GeoJS says it has no rate limits yet.
Why does ip-api.com fail on my HTTPS site?
Its free endpoint is HTTP only; https://ip-api.com/json returns 403 "SSL unavailable for this endpoint". Browsers block HTTP requests from HTTPS pages as mixed content, so use an HTTPS provider.
Why does ipwho.is return 429 for my site?
Browser calls to ipwho.is count against one daily pool of 1,000 requests per site, shared by every visitor. When it runs out, every visitor gets 429 with a Retry-After header until the pool resets.
How accurate is IP geolocation with a VPN or mobile data?
The API sees the VPN exit server or the mobile carrier's gateway and returns that location. Even on a home connection, providers disagreed: Laurinburg, North Carolina for two of them, New York City for a third.
Is an IP address personal data under the GDPR?
In the EU, IP addresses are commonly treated as personal data, and a browser call sends the visitor's IP to the provider. Name the provider in your privacy notice and get advice for your case; this is not legal advice.