Prerequisites
- Node.js 18 or higher (built-in
fetch— no npm install required) - An ip-api.io API key — get one free
- Your API key stored in an environment variable:
IP_API_IO_KEY
await, which requires "type": "module" in your
package.json, or save the file with a .mjs extension.
If you prefer CommonJS, wrap the code in an async function and call it.
Quickstart: look up an IP address
Pass any IPv4 or IPv6 address to GET /api/v1/ip/{ip}.
The response includes geolocation and security signals in a single call.
Store your API key in an environment variable — never hardcode it.
const API_KEY = process.env.IP_API_IO_KEY;
const ip = "8.8.8.8";
const resp = await fetch(
`https://ip-api.io/api/v1/ip/${ip}?api_key=${API_KEY}`,
{ signal: AbortSignal.timeout(5000) }
);
if (!resp.ok) throw new Error(`ip-api.io error: ${resp.status}`);
const data = await resp.json();
console.log(data.ip); // "8.8.8.8"
console.log(data.location.country); // "United States"
console.log(data.location.city); // "Mountain View"
console.log(data.location.latitude); // 37.4056
console.log(data.location.longitude); // -122.0775
console.log(data.location.timezone); // "America/Los_Angeles"
console.log(data.location.local_time); // "2024-05-20T15:16:52-07:00"
GET /api/v1/ip/?api_key=YOUR_KEY — and the API resolves it from
the incoming request. Useful when calling server-side with the client IP
extracted from X-Forwarded-For.
Detect VPN, proxy, Tor, and threats
Every response includes a suspicious_factors object with seven boolean flags.
You get security intelligence in the same call, at no extra cost per lookup.
const data = await lookupIp("185.220.101.45"); // example Tor exit node
const sf = data.suspicious_factors;
if (sf.is_vpn || sf.is_proxy || sf.is_tor_node) {
console.log("Anonymous or masked traffic — consider blocking or CAPTCHA");
}
if (sf.is_datacenter) {
console.log("Cloud/datacenter IP — likely automated or bot traffic");
}
if (sf.is_threat) {
console.log("Active threat signal — block this request");
}
// Combine signals for a risk decision
const isRisky = sf.is_vpn || sf.is_proxy || sf.is_tor_node || sf.is_threat;
console.log(`Risk flag: ${isRisky}`); // true
VERY_LOW / LOW / MEDIUM / HIGH /
VERY_HIGH risk level, use the
Risk Score API at
/api/v1/risk-score/{ip}.
TypeScript types
Copy this interface into your project to get full type safety on every field. No external type package needed.
interface SuspiciousFactors {
is_vpn: boolean;
is_proxy: boolean;
is_tor_node: boolean;
is_datacenter: boolean;
is_threat: boolean;
is_spam: boolean;
is_crawler: boolean;
}
interface Location {
country: string | null;
country_code: string | null;
city: string | null;
latitude: number | null;
longitude: number | null;
zip: string | null;
timezone: string | null;
local_time: string | null;
local_time_unix: number | null;
is_daylight_savings: boolean | null;
}
interface IpApiResponse {
ip: string;
suspicious_factors: SuspiciousFactors;
location: Location;
}
// Typed lookup function
async function lookupIp(ip: string): Promise<IpApiResponse> {
const apiKey = process.env.IP_API_IO_KEY!;
const resp = await fetch(
`https://ip-api.io/api/v1/ip/${ip}?api_key=${apiKey}`,
{ signal: AbortSignal.timeout(5000) }
);
if (!resp.ok) throw new Error(`ip-api.io error: ${resp.status}`);
return resp.json() as Promise<IpApiResponse>;
} Express.js middleware
Attach IP intelligence to every request as req.ipIntel.
The middleware never crashes — if ip-api.io is unavailable, it sets
req.ipIntel = null and calls next().
Treat ip-api.io as a non-critical enrichment, not a hard dependency.
import express from "express";
const API_KEY = process.env.IP_API_IO_KEY;
// Extract the real client IP from X-Forwarded-For (set by reverse proxies).
// Only trust this header when behind a reverse proxy you control —
// an attacker can spoof it on a directly-exposed server.
function getClientIp(req) {
const xff = req.headers["x-forwarded-for"];
return xff ? xff.split(",")[0].trim() : req.socket.remoteAddress;
}
async function ipIntelMiddleware(req, res, next) {
try {
const ip = getClientIp(req);
const resp = await fetch(
`https://ip-api.io/api/v1/ip/${ip}?api_key=${API_KEY}`,
{ signal: AbortSignal.timeout(5000) }
);
req.ipIntel = resp.ok ? await resp.json() : null;
} catch {
req.ipIntel = null;
}
next();
}
const app = express();
app.use(ipIntelMiddleware);
app.get("/check", (req, res) => {
const data = req.ipIntel;
if (!data) return res.status(503).json({ error: "IP lookup unavailable" });
res.json({
ip: data.ip,
country: data.location.country,
is_vpn: data.suspicious_factors.is_vpn,
is_proxy: data.suspicious_factors.is_proxy,
is_tor: data.suspicious_factors.is_tor_node,
is_threat: data.suspicious_factors.is_threat,
});
});
app.listen(3000);
app.set('trust proxy', 1) to your Express app and use
req.ip directly — Express will populate it from
X-Forwarded-For automatically.
Only enable this when you are actually behind a trusted reverse proxy.
Next.js API route
Create a server-side API route so your frontend can call
/api/ip-lookup on your own domain. The API key stays in
environment variables and is never sent to the browser.
// app/api/ip-lookup/route.ts (Next.js App Router)
import { NextRequest, NextResponse } from "next/server";
const API_KEY = process.env.IP_API_IO_KEY!;
export async function GET(request: NextRequest): Promise<NextResponse> {
const xff = request.headers.get("x-forwarded-for");
const clientIp = xff ? xff.split(",")[0].trim() : "127.0.0.1";
try {
const resp = await fetch(
`https://ip-api.io/api/v1/ip/${clientIp}?api_key=${API_KEY}`,
{ signal: AbortSignal.timeout(5000) }
);
if (!resp.ok) {
return NextResponse.json({ error: "IP lookup failed" }, { status: 502 });
}
const data = await resp.json();
return NextResponse.json(data);
} catch {
return NextResponse.json({ error: "IP lookup unavailable" }, { status: 503 });
}
}
fetch('/api/ip-lookup') — they never touch ip-api.io directly
and never see the API key. Add export const runtime = 'edge'
to run this route on Vercel Edge for lower latency.