Node.js
The official Node.js client library for the VPNDetection API.
Getting Started
npm install vpndetectionRequires Node.js 22 or newer. TypeScript types are included.
Usage
No API key needed to start. The free tier answers ip and is_vpn, and allows 1000 requests per day per source address.
import { VPNDetection } from 'vpndetection';
const client = new VPNDetection();
const result = await client.lookup('45.83.91.1');
console.log(result.isVpn); // trueWith an API key
An API key raises your quota, and raises your features on a paid plan. Create one in the console, then pass it in:
const client = new VPNDetection({ apiKey: process.env.VPNDETECTION_API_KEY });
const result = await client.lookup('45.83.91.1');
console.log(result.isVpn); // true
console.log(result.vpn?.provider); // 'mullvad'
console.log(result.isHosting); // true
console.log(result.hosting?.provider);Your own address
const result = await client.myIp();
console.log(result.ip); // the address we saw this call come fromYour plan and usage
const acct = await client.myAccount();
console.log(acct.plan.key); // max
console.log(acct.usage.requests); // 580
console.log(acct.usage.window_end); // when the allowance resetsUsage counts against the anniversary of your subscription, not the calendar month and not the billing period, and it is the same number a lookup is gated on. hard_limit is null on an uncapped plan, which is not the same as zero.
Batch lookup
You can do batch lookups with a list, which parallelizes requests for you efficiently:
const results = await client.lookupBatch(['45.83.91.1', '8.8.8.8', '1.1.1.1']);
for (const [ip, result] of results) {
if (result instanceof Error) {
console.error(`${ip}: ${result.message}`);
continue;
}
console.log(`${ip}: ${result.isVpn}`);
}Results are keyed by address, so duplicates in your list collapse into a single request and one address failing never loses the rest.
Concurrency and other variables are configurable per-call:
const results = await client.lookupBatch(manyIps, { concurrency: 32, retries: 4 });Caching
Answers are cached by default, so repeat lookups of the same address are free:
const client = new VPNDetection();
const result = await client.lookup('45.83.91.1');
console.log(result.isVpn); // true, API request
const result2 = await client.lookup('45.83.91.1');
console.log(result2.isVpn); // true, no API request, result was cachedYou can change the default cache variables (max size, TTL, etc) on initialization, or even disable it:
const client = new VPNDetection({ cache: { max: 50_000, ttlMs: 6 * 60 * 60 * 1000 } });
const clientNoCache = new VPNDetection({ cache: false });Private and reserved addresses
Private, loopback, link-local, documentation and multicast addresses (and their IPv6 equivalents, including the 6to4 and Teredo ranges) can never be VPN or proxy infrastructure. The library answers them locally, so they cost no request and no quota:
const result = await client.lookup('192.168.1.1');
result.isBogon; // true, this answer was computed rather than served
result.isVpn; // falseThe check is available on the client, which is handy when your inputs are addresses anyway:
client.isBogon('10.0.0.1'); // true
client.isBogon('8.8.8.8'); // falseIt is also importable on its own, if you want it without a client:
import { isBogon } from 'vpndetection';
isBogon('10.0.0.1'); // trueErrors
Failures throw a VPNDetectionError carrying a kind and a retryable flag:
import { VPNDetectionError } from 'vpndetection';
try {
await client.lookup('1.1.1.1');
} catch (err) {
if (err instanceof VPNDetectionError) {
console.error(err.kind, err.retryable);
}
}kind is one of bad_request, unauthorized, forbidden, rate_limited, quota_exceeded, server_error or network.
Note that rate_limited and quota_exceeded both arrive as HTTP 429 and are not the same thing. A rate limit is when the API faces extreme traffic bursts and so retrying later works; but a spent quota needs your allowance raised or the window to roll over. The library retries rate limits for you, but not if your quota is exceeded.
Database downloads
If your key carries the db.download scope, the licensed databases are available through client.database. download fetches one to a path, streaming it straight to disk so that nothing bigger than a chunk is ever held in memory:
const databases = await client.database.list();
const written = await client.database.download('vpn_ip_extended_v1', 'mmdb', './vpn_ip_extended_v1.mmdb');
console.log(`${written} bytes`);Or take the time-limited link and run the transfer yourself, or take a small database as bytes:
const url = await client.database.downloadUrl('vpn_ip_extended_v1', 'mmdb');
const bytes = await client.database.downloadBytes('cdn_ip_v1', 'csvgz');downloadBytes holds the whole file in memory, and the catalog runs from cdn_ip_v1 at 10 KB to resproxy_ip_90d_v1 at 1.79 GB, so use download for anything you have not measured.
Absent is not false
Only ip and isVpn come back on every plan. A field your plan does not include is undefined, which means "not in your plan" rather than "checked, and no".
result.isHosting ?? false // when you only want the flag
result.isHosting === undefined // not in your plan