IP Geolocation API in JavaScript: Node.js Examples and Recipes

Updated

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:

Shell
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:

JavaScript
// 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:

JavaScript
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:

JavaScript
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:

ParameterWhat it does
fieldsComma-separated dot paths, for example ip,location.country.alpha2,timezone.id
languageNames in de, en, es, fr, ja, pt, ru or zh (default en)
hostname1 or true adds the reverse DNS name; this adds response time
JavaScript
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:

JavaScript
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:

CodeMeaningWhat to do
401Missing or invalid API key (invalid_api_key)Check IPLOOKUPAPI_KEY; do not retry
403Feature not in your plan, or the key is not allowedUpgrade the plan or drop the field; do not retry
404No data for this IPTreat as “unknown location”
422Validation error, for example an invalid IP or languageFix the input; the body lists the errors
429Rate 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
500Server errorRetry 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:

JavaScript
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:

JavaScript
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:

JavaScript
// 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.

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:

JavaScript
// 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:

JavaScript
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:

JavaScript
// 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:

Shell
npx wrangler secret put IPLOOKUPAPI_KEY

The 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.