You want to know whether a request is coming from a VPN, a proxy, or a datacenter IP — and respond appropriately. The good news: it's one API call. The important news: a VPN flag is a signal, not a verdict, and treating it like one will hurt real users.
This guide builds a small, production-shaped Express integration that checks each request, exposes the risk on req.ipRisk, and steps up verification for high-risk signups — without ever silently blocking someone for using a VPN.
1. The raw call
Every lookup is a single authenticated GET:
$ curl "https://api.geoq.io/v1/check?ip=45.83.220.5" \ -H "Authorization: Bearer $GEOQ_API_KEY"
That returns the full payload: signals.is_vpn, is_proxy, is_datacenter, geo, ASN and a risk object. See the response schema.
2. A tiny wrapper
Wrap the call so the rest of your app never touches fetch directly:
// geoq.js — a tiny wrapper
export async function checkIp(ip) {
const url = new URL('https://api.geoq.io/v1/check');
if (ip) url.searchParams.set('ip', ip);
const res = await fetch(url, {
headers: { Authorization: 'Bearer ' + process.env.GEOQ_API_KEY },
});
if (!res.ok) throw new Error('geoq ' + res.status);
return res.json();
} 3. Middleware that fails open
Attach the result to the request. Critically, if the API has a blip we fail open — a missed risk check is far better than locking every user out:
import express from 'express';
import { checkIp } from './geoq.js';
const app = express();
app.set('trust proxy', true); // so req.ip is the real client IP
// Flag risky IPs, but step up — never silently block.
app.use(async (req, res, next) => {
try {
const r = await checkIp(req.ip);
req.ipRisk = r;
if (r.signals.is_vpn || r.signals.is_proxy) {
res.set('X-IP-Note', 'vpn-or-proxy');
}
} catch (e) {
req.ipRisk = null; // fail open — don't lock users out on an API blip
}
next();
}); Settingtrust proxymatters: behind a load balancer,req.ipis otherwise your proxy's IP, not the user's. Use the leftmost untrusted value fromX-Forwarded-For.
4. Step up, don't block
Now use the risk level in a route. A VPN user is usually a normal user who cares about privacy. Add friction proportional to risk instead of denying access:
app.post('/signup', async (req, res) => {
const risk = req.ipRisk?.risk;
if (risk && risk.level === 'high') {
// require email verification before creating the account
return res.status(202).json({ next: 'verify-email', reasons: risk.reasons });
}
// ...create the account normally
res.json({ ok: true });
}); What the VPN signal can and can't tell you
- Can: tell you an IP belongs to a known commercial VPN range — useful context, worth +30 in the risk score.
- Can't: tell you the person is malicious. Journalists, remote workers and the privacy-conscious all use VPNs.
- Won't be perfect: VPN providers rotate ranges constantly, so detection is probabilistic.
That's why we combine is_vpn with the overall risk score and only escalate to step-up auth, never to a hard block — and why GeoQ must not be the sole basis of an automated decision about a person (AUP).
Next steps
Read the VPN detection API page, try the IP lookup tool, or get a free API key (no card) and ship this today.