Every request that reaches your server carries one fact the client did not choose to tell you: the address it came from. It is the cheapest fraud signal you have, because you already hold it, and the most misused, because it looks far more precise than it is.
This lesson gets you two things from an IP address. The first is a location: a country you can mostly trust, a region and timezone you can use as defaults, and a city you should treat as a rumour. The second is the network behind it, the Autonomous System that announces the address to the rest of the internet, which tells you whether you are talking to a home broadband line, a mobile carrier or a rented server. The next three lessons build on both.
01What an IP address can tell you
An IP address is a routing label, not an identity. It says which network will deliver packets to the client, and the geolocation databases behind every IP API are educated reconstructions of where those networks put their customers. They are built from registry records, ISP feeds, latency measurements and corrections people submit. Some of that is very good. None of it is a GPS fix.
Three properties follow from that, and they shape every decision in this course:
- Coarse is reliable, fine is not. Country is right for the vast majority of residential and mobile addresses. Region is usually right. City is often the location of the ISP's equipment, or the nearest big town, or the centre of the country when the database has nothing better.
- The address is not the person. Behind one IP there may be a family, a coffee shop, an office of 2,000, or a mobile carrier sharing one public address across thousands of phones (carrier-grade NAT). Behind one person there may be dozens of IPs over a week.
- The address can be borrowed. VPNs, proxies, Tor and cloud servers let anyone appear wherever they like. Detecting that is lesson 2. In this lesson, assume the address is honest and learn what it says.
02Look up an IP
IP Lookup (iplookup) takes one ip query parameter on a GET and costs 10 credits. Try it on the example address from its definition:
curl "https://api.apiverve.com/v1/iplookup?ip=173.172.81.20" \
-H "x-api-key: YOUR_API_KEY"country, region and timezone are on every plan; city, coordinates and accuracyRadius are premium.
The response splits cleanly into fields every plan gets and fields that need a higher plan:
| Field | Plan | What it is good for |
|---|---|---|
country, countryName | all | ISO 3166-1 alpha-2 code and name. Defaults, compliance routing, mismatch checks |
region, regionName | all | State or province. Tax defaults, regional content |
timezone | all | IANA zone such as America/Chicago. Scheduling defaults, "local time" checks |
continent, continentName | all | Coarse routing, analytics |
city, postalCode, coordinates | premium | Display only, never decisions |
accuracyRadius | premium | How far, in kilometres, the true location may be from coordinates |
isEU | premium | EU membership of the country |
asn, asnName | premium | The network announcing the address |
Write your code so the premium fields are optional from the first line. A field that is null on one plan and filled in on another should never be the difference between your code working and throwing.
region deserves a note too: for some addresses it is null even on paid plans, because the database only knows the country. The lookup for Google's public resolver 8.8.8.8 returns country: "US", a null region and city, and an accuracyRadius of 1000 km.
03How accurate is it, really?
Two observations from real calls make the point better than any disclaimer.
First, the same address moves. The sample response stored in the catalog for 173.172.81.20 places it in Kansas City, Missouri. A call made the day this lesson was written placed it in Lincoln, Nebraska, roughly 300 km away, while still reporting a 20 km accuracy radius. Both answers came from the same API. ISPs reassign address blocks between regions and the databases catch up later; the radius describes the database's confidence, not a guarantee.
Second, "no idea" looks like a location. When the database knows only the country, it still returns coordinates: for many US addresses those are [37.751, -97.822], a point in rural Kansas near the geographic centre of the country, paired with an accuracy radius of 1000 km. Put that on a map without reading the radius and you will tell a user in Boston that they are in a wheat field.
So the rules:
- Decide on country, use region and timezone as defaults the user can change, and use city only for display ("Signed in near Lincoln, NE") with words that admit uncertainty.
- If you show coordinates, draw a circle of
accuracyRadius, not a pin, and hide it entirely when the radius is in the hundreds of kilometres. - Never use IP location as proof of residence for anything with legal weight. It is a hint you corroborate with a billing address, a phone number or a document.
An IP lookup returns coordinates [37.751, -97.822] with accuracyRadius 1000. What should a login-history screen show?
04Get the right address before you look it up
Most IP bugs are not in the lookup. They are in the line that decides which address to look up.
Behind a load balancer, CDN or reverse proxy, the TCP connection comes from the proxy, and the client address arrives in the X-Forwarded-For header. In Express, req.ip reads that header only when you set trust proxy, and you should set it to the number of proxies you actually run, not true. Trusting every hop lets any client prepend a forged address to the header and choose what you look up. For a fraud signal, that is the whole game handed to the attacker.
The second bug is spending credits on addresses that cannot be located. Private ranges (10.x, 172.16-31.x, 192.168.x), loopback, link-local and the carrier-grade NAT range 100.64.0.0/10 are not on the public internet. Ask IP Lookup about 10.0.0.1 and you get a 404 with the error no data found for the IP address: a "no result for your input" response, not a missing API. You will see these addresses constantly in development, in health checks and from misconfigured proxies, so filter them before the call.
The third bug is IPv4-mapped IPv6. A Node server listening on :: reports IPv4 clients as ::ffff:203.0.113.7. Strip the prefix before you validate, cache or look up, or one client becomes two cache keys.
IPv6 itself is supported: 2001:4860:4860::8888 resolves to the US and the GOOGLE network, exactly like its IPv4 sibling.
05Build a cached locator
This course uses a small client like the one from the Fundamentals courses: short timeout, key from the environment, envelope unwrapped, and a typed error so callers can tell a 404 from an outage. The project is ESM ("type": "module" in package.json) on Node 18 or later.
const BASE = 'https://api.apiverve.com/v1';
export class VerveError extends Error {
constructor(message, status, outOfCredits = false) {
super(message);
this.status = status;
this.outOfCredits = outOfCredits;
}
}
export async function verve(api, params, { method = 'GET', timeoutMs = 2500 } = {}) {
const key = process.env.APIVERVE_API_KEY;
if (!key) throw new Error('APIVERVE_API_KEY is not set');
let url = `${BASE}/${api}`;
const init = { method, headers: { 'x-api-key': key }, signal: AbortSignal.timeout(timeoutMs) };
if (method === 'GET') {
url += `?${new URLSearchParams(params)}`;
} else {
init.headers['Content-Type'] = 'application/json';
init.body = JSON.stringify(params);
}
const res = await fetch(url, init);
const body = await res.json().catch(() => null);
if (!res.ok || body?.status !== 'ok') {
const outOfCredits = res.status === 429 && res.headers.get('x-api-remaining-credits') === '0';
throw new VerveError(body?.error || `HTTP ${res.status}`, res.status, outOfCredits);
}
return body.data;
}The locator normalises the address, refuses non-public ones without spending a credit, caches by address for a day, and turns the premium fields into explicit nulls:
import net from 'node:net';
import { verve, VerveError } from './verve.js';
const reserved = new net.BlockList();
for (const [addr, prefix] of [
['0.0.0.0', 8], ['10.0.0.0', 8], ['100.64.0.0', 10], ['127.0.0.0', 8],
['169.254.0.0', 16], ['172.16.0.0', 12], ['192.168.0.0', 16],
]) reserved.addSubnet(addr, prefix, 'ipv4');
for (const [addr, prefix] of [['::1', 128], ['fc00::', 7], ['fe80::', 10]]) {
reserved.addSubnet(addr, prefix, 'ipv6');
}
export function publicIp(raw) {
const ip = String(raw ?? '').trim().replace(/^::ffff:(?=\d+\.\d+\.\d+\.\d+$)/i, '');
const family = net.isIP(ip);
if (!family) return null;
return reserved.check(ip, family === 4 ? 'ipv4' : 'ipv6') ? null : ip;
}
const TTL_MS = 24 * 60 * 60 * 1000;
const cache = new Map();
export async function locate(raw) {
const ip = publicIp(raw);
if (!ip) return { ip: String(raw), located: false, reason: 'not-public' };
const hit = cache.get(ip);
if (hit && hit.expires > Date.now()) return hit.value;
let value;
try {
const d = await verve('iplookup', { ip });
value = {
ip,
located: true,
country: d.country,
region: d.region ?? null,
timezone: d.timezone ?? null,
city: d.city ?? null, // premium
accuracyKm: d.accuracyRadius ?? null, // premium
asn: d.asn ?? null, // premium
};
} catch (err) {
if (!(err instanceof VerveError) || err.status !== 404) throw err;
value = { ip, located: false, reason: 'no-data' };
}
cache.set(ip, { value, expires: Date.now() + TTL_MS });
return value;
}
// node locate.js 173.172.81.20
if (process.argv[1]?.endsWith('locate.js') && process.argv[2]) {
console.log(await locate(process.argv[2]));
}The Python version leans on the standard library's ipaddress module, whose is_global property already excludes private, loopback, link-local and shared ranges:
import ipaddress
import os
import sys
import time
import requests
TTL = 24 * 60 * 60
_cache = {}
def public_ip(raw):
try:
ip = ipaddress.ip_address(str(raw).strip())
except ValueError:
return None
if ip.version == 6 and ip.ipv4_mapped:
ip = ip.ipv4_mapped
return str(ip) if ip.is_global else None
def locate(raw):
ip = public_ip(raw)
if ip is None:
return {"ip": str(raw), "located": False, "reason": "not-public"}
hit = _cache.get(ip)
if hit and hit[1] > time.time():
return hit[0]
res = requests.get(
"https://api.apiverve.com/v1/iplookup",
params={"ip": ip},
headers={"x-api-key": os.environ["APIVERVE_API_KEY"]},
timeout=2.5,
)
if res.status_code == 404:
value = {"ip": ip, "located": False, "reason": "no-data"}
else:
res.raise_for_status()
d = res.json()["data"]
value = {
"ip": ip,
"located": True,
"country": d["country"],
"region": d.get("region"),
"timezone": d.get("timezone"),
"city": d.get("city"),
"accuracy_km": d.get("accuracyRadius"),
"asn": d.get("asn"),
}
_cache[ip] = (value, time.time() + TTL)
return value
if __name__ == "__main__":
print(locate(sys.argv[1]))A day is a sensible cache lifetime for location: long enough that a returning visitor costs nothing, short enough to follow reassignments like the Kansas City to Lincoln move above. In production, put the cache in Redis or your database so every instance shares it.
Your Express app runs behind one load balancer and you set app.set('trust proxy', true). What can a client now do?
06Who owns the network: ASN lookup
Every public address is announced to the internet by an Autonomous System, a network with its own number. The ASN is often more useful than the location, because it tells you what kind of connection you are dealing with. A residential ISP, a mobile carrier and a cloud provider are three very different populations of users.
ASN Lookup (asnlookup) costs 4 credits and takes an asn parameter, not an IP:
curl "https://api.apiverve.com/v1/asnlookup?asn=AS16509" \
-H "x-api-key: YOUR_API_KEY"description is on every plan: Amazon.com, Inc. The handle AMAZON-02 and range counts are premium.
description (the registered organisation) and simple (the bare number) are on every plan; handle, country and the announced range counts are premium. Send the number with its AS prefix. The API also accepts a bare 13335, but then echoes asn back without the prefix, so a cache keyed on the response's asn field ends up with two spellings of the same network.
Where does the ASN come from? Three places:
- IP Lookup's premium
asnfield, as in the locator above. On plans that include it you getAS11427andTWC-11427-TEXASwith the location in one call. - Your edge. Many CDNs attach the client's ASN to the request before it reaches you; Cloudflare Workers, for example, expose it as
request.cf.asn. If you already have it, do not pay to look it up again. - The detectors in the next lesson, which return it on premium plans alongside their verdicts.
Notice that the AS names in IP data are registry names, not brands: TWC-11427-TEXAS is a Charter (Spectrum) network and the US address is in Nebraska, not Texas. Use ASN Lookup's description when you want an organisation name a human recognises.
An ASN result changes rarely, so cache it for a week or more. A few dozen networks will account for most of your traffic, which makes this one of the cheapest lookups in the course once the cache is warm.
07What IP location is good for, and what it is not
Good uses share a property: being wrong costs little, or another signal backs the decision up.
- Defaults. Pre-select the country in a form, show prices in a likely currency, suggest a timezone. The user corrects you in one click.
- Mismatch signals. A card issued in one country, a billing address in a second and an IP in a third is worth a second look. Any one of them alone is ordinary; travellers and expats exist.
- Login anomaly hints. "New sign-in from a different country" is a fair email to send. "Account locked because the city changed" is not, given what you saw above.
- Coarse compliance routing. Showing the right consent banner or routing EU traffic to an EU deployment, with the understanding that VPN users will land on the wrong side.
Bad uses treat it as proof: refusing a sale because the city does not match the shipping address, or deciding someone's legal jurisdiction from their IP alone.
08Privacy: an IP address is personal data
Under the GDPR, and in the guidance of most European regulators, an IP address can be personal data, because the ISP can link it to a subscriber. Treat it that way:
- Say in your privacy notice that you look up the location and network of visitor IP addresses for security and fraud prevention, and name the purpose.
- Store the derived, coarse result (country, network type) for as long as you need it, and the raw address only as long as your security purpose requires. A login history keeps a lot of value with a truncated address and a country.
- Do not send IPs to a lookup service that you have no reason to look up. The private-range filter above is a privacy measure as much as a credit saving.
Next you will find out when the address is not telling the truth: Detect VPN, proxy and Tor traffic.