IP Geolocation API in JavaScript: Node.js Examples and Recipes
Short answer
To geolocate an IP address in JavaScript, send a GET request to https://api.iplookupapi.com/v1/info?ip=8.8.8.8 from server code, such as Node.js 18+, a Next.js route handler or a Cloudflare Worker, with your API key in the apikey header. The JSON response holds the country, city, time zone, ISP and ASN. Never make the call from the browser, because the API key would be public.
On this page
The iplookupapi REST API returns location, connection, time zone and currency data for any IPv4 or IPv6 address. From JavaScript you call it with the built-in fetch, on a server only. This page covers a small client, single and batch lookups, errors, and recipes for Express, Next.js, Cloudflare Workers and caching.
Install and set up
You need Node.js 18 or later, which ships the global fetch. The npm package @everapi/iplookupapi-js (version 1.0.0) stops on import with ReferenceError: Iplookupapi is not defined, so this page uses a small client of its own with no dependencies. Register for a free API key and store it in an environment variable. Never write the key into your source code:
export IPLOOKUPAPI_KEY="your_api_key"The examples use ES modules. Set "type": "module" in your package.json or name your files .mjs.
Create a fetch client for the API
This helper sends the key in the apikey header, throws on errors, retries rate limits and server errors with exponential backoff, and stops each request after five seconds:
// lib/iplookup.js: server-side only. Never import this file in browser code.
const BASE_URL = "https://api.iplookupapi.com/v1";
export class IplookupError extends Error {
constructor(status, body) {
super(body?.error?.message ?? body?.message ?? `HTTP ${status}`);
this.status = status;
this.code = body?.error?.code ?? null; // e.g. "quota_exceeded"
this.body = body;
}
}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function call(path, { params = {}, body, timeoutMs = 5000, retries = 3 } = {}) {
const apiKey = process.env.IPLOOKUPAPI_KEY;
if (!apiKey) throw new Error("IPLOOKUPAPI_KEY is not set");
const url = new URL(`${BASE_URL}/${path}`);
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null) url.searchParams.set(key, String(value));
}
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, {
method: body ? "POST" : "GET",
headers: { apikey: apiKey, ...(body && { "Content-Type": "application/json" }) },
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(timeoutMs), // throws a TimeoutError when exceeded
});
const data = await res.json().catch(() => null);
if (res.ok) return data;
// Retry rate limits and server errors; a used-up monthly quota will not recover.
const retryable =
res.status >= 500 || (res.status === 429 && data?.error?.code !== "quota_exceeded");
if (!retryable || attempt >= retries) throw new IplookupError(res.status, data);
await sleep(Math.min(30_000, 1000 * 2 ** attempt) + Math.random() * 250);
}
}
// One IP. Leave ip undefined to look up the caller's own address.
export async function lookupIp(ip, params = {}) {
const res = await call("info", { params: { ip, ...params } });
return res.data;
}
// Any number of IPs: deduped, then sent in chunks of 1,000.
export async function batchLookup(ips, params = {}) {
const unique = [...new Set(ips)];
const result = { data: {}, errors: {} };
for (let i = 0; i < unique.length; i += 1000) {
const res = await call("batch", {
params,
body: { ips: unique.slice(i, i + 1000) },
timeoutMs: 60_000,
});
Object.assign(result.data, res.data);
Object.assign(result.errors, res.errors ?? {});
}
return result;
}
// Quota and plan. Free: does not count against your quota.
export function getStatus() {
return call("status", { retries: 0 });
}In a CommonJS project, save the file as iplookup.mjs and load it with await import("./lib/iplookup.mjs"). In TypeScript, save it as iplookup.ts and type the fields your code reads.
Look up an IP address in Node.js
Pass the address to lookupIp(). It resolves to the data object of the response:
import { lookupIp } from "./lib/iplookup.js";
const geo = await lookupIp("8.8.8.8");
console.log(geo.location.country.alpha2); // country code
console.log(geo.location.city.name); // city
console.log(geo.timezone.id); // IANA time zone
console.log(geo.connection.isp, geo.connection.asn);The result holds ip, type (v4 or v6), range_type, connection (ASN, organization, ISP, range), location (continent, country with currencies and calling codes, region, city, zip, latitude, longitude), timezone and security. The info endpoint documentation lists every field. Network data starts at the Basic plan, and security flags such as is_vpn start at Professional.
Look up your own IP address
Call lookupIp() without an address, and the API answers for the IP that sent the request:
const me = await lookupIp();
console.log(me.ip, me.location.country.name);On a server, this is the server’s public IP, not your visitor’s.
Choose fields, language and reverse DNS
The second argument becomes query parameters:
| Parameter | What it does |
|---|---|
fields | Comma-separated dot paths, for example ip,location.country.alpha2,timezone.id |
language | Names in de, en, es, fr, ja, pt, ru or zh (default en) |
hostname | 1 or true adds the reverse DNS name; this adds response time |
const geo = await lookupIp("2001:4860:4860::8888", {
fields: "ip,location.country.name,location.city.name,timezone.id",
language: "de",
});Check your quota
The status endpoint returns your plan and quota. It does not count against your quota or rate limit:
import { getStatus } from "./lib/iplookup.js";
const { quotas } = await getStatus();
console.log(quotas.month.used, "of", quotas.month.total, "used,", quotas.month.remaining, "left");Handle errors, rate limits and timeouts
The helper throws an IplookupError with status and code for every error response, and a TimeoutError when the API does not answer in time. These are the status codes to handle:
| Code | Meaning | What to do |
|---|---|---|
| 401 | Missing or invalid API key (invalid_api_key) | Check IPLOOKUPAPI_KEY; do not retry |
| 403 | Feature not in your plan, or the key is not allowed | Upgrade the plan or drop the field; do not retry |
| 404 | No data for this IP | Treat as “unknown location” |
| 422 | Validation error, for example an invalid IP or language | Fix the input; the body lists the errors |
| 429 | Rate limit (rate_limit_exceeded) or monthly quota used up (quota_exceeded) | Retry the rate limit with backoff; for the quota, wait for quota.resets_at or upgrade |
| 500 | Server error | Retry with backoff |
The helper retries 429 rate limits and 5xx errors up to three times. Catch the rest and decide whether your page can work without location data:
import { lookupIp, IplookupError } from "./lib/iplookup.js";
try {
const geo = await lookupIp("8.8.8.8");
console.log(geo.location.country.alpha2);
} catch (err) {
if (err instanceof IplookupError && err.code === "quota_exceeded") {
console.error("Monthly quota used up, resets at", err.body?.quota?.resets_at);
} else {
console.error("Lookup failed:", err.status ?? err.name, err.message);
}
}Look up many IPs with the batch endpoint
The batch endpoint takes up to 1,000 IPv4 or IPv6 addresses in one POST request and returns one result per address, keyed by IP. batchLookup() removes duplicates and splits longer lists into chunks of 1,000:
import { batchLookup } from "./lib/iplookup.js";
const ips = ["8.8.8.8", "80.110.0.1", "2001:4860:4860::8888", "not-an-ip"];
const { data, errors } = await batchLookup(ips, {
fields: "ip,location.country.alpha2,connection.asn",
});
for (const [ip, info] of Object.entries(data)) {
console.log(ip, info.location?.country?.alpha2 ?? "-", info.connection?.asn ?? "-");
}
console.log("Not looked up:", errors);Each valid address counts as one request. Invalid items land in errors and are not billed; duplicates in one batch are billed once. If your remaining quota cannot cover a batch, the API answers 429 before any lookup, so call getStatus() before a large job. The batch IP lookup API page and the batch documentation cover the details.
Recipes for Express, Next.js and Cloudflare Workers
Cache results to save requests
The location of an address rarely changes within hours. This cache keeps up to 10,000 results for six hours and drops the least recently used entry when it is full:
// lib/cache.js: a small in-memory cache with a time limit (TTL) and LRU eviction.
export class TtlCache {
constructor({ max = 10_000, ttlMs = 6 * 60 * 60 * 1000 } = {}) {
this.max = max;
this.ttlMs = ttlMs;
this.map = new Map(); // Map keeps insertion order: oldest entry first
}
get(key) {
const hit = this.map.get(key);
if (!hit) return undefined;
this.map.delete(key);
if (hit.expires < Date.now()) return undefined;
this.map.set(key, hit); // move to the end: most recently used
return hit.value;
}
set(key, value) {
this.map.delete(key);
this.map.set(key, { value, expires: Date.now() + this.ttlMs });
if (this.map.size > this.max) this.map.delete(this.map.keys().next().value);
}
}The cache lives in one process. With several instances, each keeps its own copy, or you use a shared store.
Express middleware: get the client IP and geolocate it
Only the server sees the client’s IP address; Express exposes it as req.ip. Behind a load balancer or reverse proxy, req.ip is the proxy’s address unless you set trust proxy to your proxies’ addresses, such as ["loopback", "10.0.0.0/8"], as the Express guide to running behind proxies explains. true trusts the left-most X-Forwarded-For entry, which clients can forge. For CDNs and other setups, see how to get the real client IP; for the browser, Next.js and serverless platforms, see get the IP address in JavaScript.
// server.js
import express from "express";
import { lookupIp } from "./lib/iplookup.js";
import { TtlCache } from "./lib/cache.js";
const app = express();
app.set("trust proxy", ["loopback", "10.0.0.0/8"]); // your proxies: nginx on this host, load balancers
const cache = new TtlCache({ max: 10_000, ttlMs: 6 * 60 * 60 * 1000 }); // 6 hours
async function geolocate(req, res, next) {
const ip = req.ip?.replace(/^::ffff:/, ""); // IPv4-mapped IPv6 to plain IPv4
req.geo = null;
if (!ip) return next();
try {
let geo = cache.get(ip);
if (geo === undefined) {
geo = await lookupIp(ip, {
fields: "location.country.alpha2,location.city.name,timezone.id",
});
cache.set(ip, geo);
}
req.geo = geo;
} catch (err) {
// never fail the page because a lookup failed
console.warn("IP lookup failed:", err.status ?? err.name, err.message);
}
next();
}
app.get("/", geolocate, (req, res) => {
res.json({
country: req.geo?.location?.country?.alpha2 ?? null,
timezone: req.geo?.timezone?.id ?? null,
});
});
app.listen(3000);Note
On your own machine, req.ip is a loopback address such as ::1. Private and loopback addresses have no public location, so the API returns no location data for them. Test with a deployed app or pass a public test address.
Next.js route handler
A route handler runs on the server, so it can read the key from process.env. Create app/api/geo/route.js, as described in the Next.js route handler reference:
// app/api/geo/route.js
import { lookupIp } from "../../../lib/iplookup.js";
export async function GET(request) {
// Vercel sets x-real-ip to the client's address; self-hosted, let your nginx set it
// (proxy_set_header X-Real-IP $remote_addr) and keep Next.js reachable only through nginx.
const ip = request.headers.get("x-real-ip");
if (!ip) return Response.json({ error: "No client IP" }, { status: 400 });
try {
const geo = await lookupIp(ip, {
fields: "location.country.alpha2,location.city.name,timezone.id",
});
return Response.json(geo, { headers: { "Cache-Control": "private, max-age=3600" } });
} catch (err) {
console.warn("IP lookup failed:", err.status ?? err.name, err.message);
return Response.json({ error: "Lookup failed" }, { status: 502 });
}
}Your client components then call your own endpoint, with no key involved:
const geo = await fetch("/api/geo").then((res) => res.json());Name the variable IPLOOKUPAPI_KEY, never NEXT_PUBLIC_IPLOOKUPAPI_KEY. Next.js copies variables with the NEXT_PUBLIC_ prefix into the browser bundle.
Cloudflare Worker
In a Worker, Cloudflare passes the visitor’s address in the CF-Connecting-IP request header:
// worker.js
import { lookupIp } from "./lib/iplookup.js";
export default {
async fetch(request) {
const ip = request.headers.get("CF-Connecting-IP");
if (!ip) return Response.json({ error: "No client IP" }, { status: 400 });
try {
const geo = await lookupIp(ip, { fields: "location.country.alpha2,timezone.id" });
return Response.json(geo);
} catch (err) {
return Response.json({ error: "Lookup failed" }, { status: 502 });
}
},
};Store the key as a secret:
npx wrangler secret put IPLOOKUPAPI_KEYThe helper reads process.env.IPLOOKUPAPI_KEY. In Workers, process.env holds your secrets when Node.js compatibility is on and the compatibility date is 2025-04-01 or later, as the Cloudflare process documentation describes. From compatibility date 2026-08-04, Node.js compatibility is on by default; for older dates, add the nodejs_compat flag.
Why you should not call the API from the browser
Any key in browser code is public. Visitors can read it in the page source or the network panel and use your quota. Bundler prefixes such as NEXT_PUBLIC_ or VITE_ publish the key the same way.
Keep every call to the API on a server. The browser calls your own endpoint, which reads the IP from the request, calls the API with the key from process.env.IPLOOKUPAPI_KEY and returns only what the page needs. All endpoints and parameters are in the API documentation. SDKs for other languages are listed on the official SDKs page, and plan limits are on the pricing page.
Frequently asked questions
Can I call the IP geolocation API directly from browser JavaScript?
You should not. The request needs your API key, and anything in browser code or browser requests is visible to every visitor, who could then use up your quota. Call the API from your own server, for example a Node.js, Next.js or Cloudflare Workers endpoint, and let the browser fetch that endpoint instead.
How do I get the client IP address in JavaScript?
Browser JavaScript cannot read the public IP address of its own connection; only the server that receives the request sees it. In Express, read req.ip after you set trust proxy to match your proxy setup. In a Cloudflare Worker, read the CF-Connecting-IP header. Behind other proxies or CDNs, see how to get the real client IP.
Do I need an SDK to use the IP geolocation API in JavaScript?
No. The API is plain HTTPS and JSON, and Node.js 18 and later, Next.js and Cloudflare Workers all have fetch built in. The small client on this page adds the API key header, a timeout, retries and error handling in about 50 lines, with no dependencies.
How do I look up many IP addresses at once in JavaScript?
Send a POST request with fetch to https://api.iplookupapi.com/v1/batch with a JSON body such as {“ips”: [“8.8.8.8”, “1.1.1.1”]}. One request takes up to 1,000 addresses, so split longer lists into chunks. The batch IP lookup API page explains the format and billing.
Can I test the IP geolocation API in JavaScript for free?
Yes. The Free plan includes 150 requests a month at up to 10 requests a minute, with location, connection, time zone and currency data and the batch endpoint. Paid plans add more requests, network data and, from Professional, VPN and proxy flags. See pricing.