How to detect VPN users in your app with one API call
Your checkout page gets 50 orders from the same IP range in 12 hours, all using prepaid cards. Your free trial shows 200 signups from datacenter IPs in a week. Your login endpoint sees credential-stuffing attempts from rotating proxy IPs.
You need to know when traffic comes from a VPN, proxy, or Tor exit node. Not to block it outright, but to adjust your risk scoring. One API call gives you that signal.
The API call
Send the user's IP to POST /v1/vpn-detect. No dependencies, no SDK required.
curl -X POST https://api.botoi.com/v1/vpn-detect \
-H "Content-Type: application/json" \
-d '{"ip": "185.220.101.1"}' Response for a known Tor exit node (risk score 90):
{
"success": true,
"data": {
"ip": "185.220.101.1",
"is_vpn": true,
"is_proxy": false,
"is_tor": true,
"is_datacenter": false,
"provider": null,
"risk_score": 90,
"checks": {
"tor": true,
"datacenter": false,
"suspicious_hostname": false
}
}
} Response for a clean residential IP (risk score 0):
{
"success": true,
"data": {
"ip": "73.162.45.118",
"is_vpn": false,
"is_proxy": false,
"is_tor": false,
"is_datacenter": false,
"provider": null,
"risk_score": 0,
"checks": {
"tor": false,
"datacenter": false,
"suspicious_hostname": false
}
}
}
The response gives you five boolean flags (is_vpn, is_proxy,
is_tor, is_datacenter) plus a numeric risk_score
from 0 to 100. Tor connections score 90. Datacenter IPs score 60. Suspicious hostnames
score 40. Clean residential IPs score 0.
Integration 1: Express middleware
This middleware calls the VPN detection API and attaches the result to req.vpnRisk.
Every downstream route handler can check req.vpnRisk.isVpn to make decisions
without repeating the API call.
import type { Request, Response, NextFunction } from 'express';
const VPN_DETECT_URL = 'https://api.botoi.com/v1/vpn-detect';
const BOTOI_API_KEY = process.env.BOTOI_API_KEY;
interface VpnRisk {
isVpn: boolean;
isProxy: boolean;
isTor: boolean;
isDatacenter: boolean;
riskScore: number;
}
declare global {
namespace Express {
interface Request {
vpnRisk?: VpnRisk;
}
}
}
export async function vpnDetectMiddleware(
req: Request,
_res: Response,
next: NextFunction
) {
const ip = req.headers['x-forwarded-for']?.toString().split(',')[0]?.trim()
|| req.socket.remoteAddress
|| 'unknown';
try {
const res = await fetch(VPN_DETECT_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${BOTOI_API_KEY}`,
},
body: JSON.stringify({ ip }),
signal: AbortSignal.timeout(3000),
});
const { data } = await res.json();
req.vpnRisk = {
isVpn: data.is_vpn,
isProxy: data.is_proxy,
isTor: data.is_tor,
isDatacenter: data.is_datacenter,
riskScore: data.risk_score,
};
} catch {
// Fail open: if detection fails, treat as clean
req.vpnRisk = {
isVpn: false,
isProxy: false,
isTor: false,
isDatacenter: false,
riskScore: 0,
};
}
next();
}
// Usage in your route:
// app.use(vpnDetectMiddleware);
//
// app.post('/api/signup', (req, res) => {
// if (req.vpnRisk?.isVpn) {
// // require email verification or flag for review
// }
// });
The middleware fails open. If the API is unreachable or times out after 3 seconds, it sets
all flags to false and lets the request continue. Your users never see an error
caused by a third-party outage.
Integration 2: Next.js checkout protection
Checkout fraud follows a pattern: new account, prepaid card, VPN connection. This Next.js App Router route handler checks all three signals and routes suspicious orders to manual review instead of auto-approving them.
import { NextRequest, NextResponse } from 'next/server';
const VPN_DETECT_URL = 'https://api.botoi.com/v1/vpn-detect';
const BOTOI_API_KEY = process.env.BOTOI_API_KEY!;
interface CheckoutBody {
cardType: 'credit' | 'debit' | 'prepaid';
accountCreatedAt: string;
amount: number;
currency: string;
}
export async function POST(req: NextRequest) {
const body: CheckoutBody = await req.json();
const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim()
|| '127.0.0.1';
// Check VPN status
const vpnRes = await fetch(VPN_DETECT_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${BOTOI_API_KEY}`,
},
body: JSON.stringify({ ip }),
signal: AbortSignal.timeout(3000),
});
const { data: vpnData } = await vpnRes.json();
const accountAge = Date.now() - new Date(body.accountCreatedAt).getTime();
const isNewAccount = accountAge < 24 * 60 * 60 * 1000; // less than 24 hours
const isPrepaid = body.cardType === 'prepaid';
const isVpn = vpnData.is_vpn || vpnData.is_tor || vpnData.is_proxy;
// VPN + new account + prepaid card = manual review
if (isVpn && isNewAccount && isPrepaid) {
return NextResponse.json({
status: 'review',
orderId: crypto.randomUUID(),
message: 'Order placed. We will confirm within 30 minutes.',
}, { status: 202 });
}
// VPN + new account (no prepaid) = proceed with logging
if (isVpn && isNewAccount) {
console.log(JSON.stringify({
event: 'checkout_vpn_new_account',
ip,
riskScore: vpnData.risk_score,
amount: body.amount,
}));
}
// Process the order normally
return NextResponse.json({
status: 'approved',
orderId: crypto.randomUUID(),
});
} The handler doesn't reject the order. It returns a 202 with a "we'll confirm within 30 minutes" message. This buys your team time to review without tipping off a bad actor that they've been flagged. Legitimate customers on VPNs still get their order processed after a short delay.
Integration 3: Login rate limiting
Credential stuffing attacks often come from VPN or proxy IPs to avoid IP-based blocking. Apply stricter rate limits to those connections: 3 login attempts per 15 minutes for VPN users versus 10 for regular users.
import type { Request, Response, NextFunction } from 'express';
const VPN_DETECT_URL = 'https://api.botoi.com/v1/vpn-detect';
const BOTOI_API_KEY = process.env.BOTOI_API_KEY;
// VPN users: 3 attempts per 15 minutes
// Regular users: 10 attempts per 15 minutes
const VPN_LOGIN_LIMIT = 3;
const STANDARD_LOGIN_LIMIT = 10;
const WINDOW_MS = 15 * 60 * 1000;
const loginAttempts = new Map<string, { count: number; resetAt: number }>();
export async function loginRateLimit(
req: Request,
res: Response,
next: NextFunction
) {
const ip = req.headers['x-forwarded-for']?.toString().split(',')[0]?.trim()
|| req.socket.remoteAddress
|| 'unknown';
// Check VPN status
let isVpn = false;
try {
const vpnRes = await fetch(VPN_DETECT_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${BOTOI_API_KEY}`,
},
body: JSON.stringify({ ip }),
signal: AbortSignal.timeout(2000),
});
const { data } = await vpnRes.json();
isVpn = data.is_vpn || data.is_tor || data.is_proxy;
} catch {
// Fail open: use standard limits
}
const limit = isVpn ? VPN_LOGIN_LIMIT : STANDARD_LOGIN_LIMIT;
const now = Date.now();
const entry = loginAttempts.get(ip);
if (!entry || entry.resetAt < now) {
loginAttempts.set(ip, { count: 1, resetAt: now + WINDOW_MS });
return next();
}
entry.count++;
if (entry.count > limit) {
const retryAfter = Math.ceil((entry.resetAt - now) / 1000);
return res.status(429).json({
error: 'Too many login attempts. Try again later.',
retryAfter,
});
}
next();
}
// Mount on your login route:
// app.post('/api/auth/login', loginRateLimit, loginHandler); The 3:10 ratio slows down automated attacks from VPN IPs without affecting most real users. A legitimate user on a corporate VPN who mistypes their password three times still gets a clear error message and a retry window, not a permanent ban.
Building a compound risk score
VPN detection alone is a weak fraud signal. Many legitimate users run VPNs. The signal gets stronger when you combine it with other checks. Call three botoi endpoints in parallel:
/v1/vpn-detectfor connection type (VPN, proxy, Tor, datacenter)/v1/disposable-email/checkfor email quality (throwaway vs permanent)/v1/ip/lookupfor geo mismatch (IP country vs billing country)
This function calls all three and produces a 0-100 risk score:
interface RiskResult {
score: number;
action: 'allow' | 'review' | 'block';
signals: {
vpn: boolean;
tor: boolean;
proxy: boolean;
vpnRiskScore: number;
disposableEmail: boolean;
geoMismatch: boolean;
ipCountry: string;
billingCountry: string;
};
}
async function calculateRisk(
ip: string,
email: string,
billingCountry: string
): Promise<RiskResult> {
const BOTOI_KEY = process.env.BOTOI_API_KEY!;
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${BOTOI_KEY}`,
};
// Run all three checks in parallel
const [vpnRes, emailRes, geoRes] = await Promise.all([
fetch('https://api.botoi.com/v1/vpn-detect', {
method: 'POST',
headers,
body: JSON.stringify({ ip }),
}),
fetch('https://api.botoi.com/v1/disposable-email/check', {
method: 'POST',
headers,
body: JSON.stringify({ email }),
}),
fetch('https://api.botoi.com/v1/ip/lookup', {
method: 'POST',
headers,
body: JSON.stringify({ ip }),
}),
]);
const vpnData = (await vpnRes.json()).data;
const emailData = (await emailRes.json()).data;
const geoData = (await geoRes.json()).data;
const ipCountry = geoData.country_code || '';
const geoMismatch = ipCountry !== '' && billingCountry !== ''
&& ipCountry.toUpperCase() !== billingCountry.toUpperCase();
// Weight each signal
let score = 0;
// VPN/proxy/Tor: up to 30 points
if (vpnData.is_vpn || vpnData.is_tor || vpnData.is_proxy) {
score += Math.round(vpnData.risk_score * 0.3);
}
// Disposable email: 25 points
if (emailData.is_disposable) {
score += 25;
}
// Geo mismatch (IP country != billing country): 20 points
if (geoMismatch) {
score += 20;
}
// VPN + disposable email combo: extra 15 points
if ((vpnData.is_vpn || vpnData.is_tor) && emailData.is_disposable) {
score += 15;
}
score = Math.min(score, 100);
return {
score,
action: score > 70 ? 'block' : score > 30 ? 'review' : 'allow',
signals: {
vpn: vpnData.is_vpn,
tor: vpnData.is_tor,
proxy: vpnData.is_proxy,
vpnRiskScore: vpnData.risk_score,
disposableEmail: emailData.is_disposable,
geoMismatch,
ipCountry,
billingCountry,
},
};
}
// Example usage:
// const risk = await calculateRisk('185.220.101.1', 'user@tempmail.com', 'US');
// if (risk.action === 'review') { queueForManualReview(orderId); }
// if (risk.action === 'block') { rejectTransaction(orderId); }
All three API calls run in parallel with Promise.all, so total latency equals
the slowest call (typically under 100ms). The scoring weights are a starting point. Tune them
based on your fraud data. If disposable emails are your biggest source of chargebacks, increase
that weight. If geo mismatches are rare and usually benign for your user base, decrease it.
When NOT to block VPN users
Hard-blocking VPN traffic is a mistake for most applications. Here are the common reasons people connect through a VPN:
- Corporate policy. Companies route employee traffic through a VPN by default. Blocking these connections means your B2B customers can't use your product during work hours.
- Privacy. Privacy-conscious users run VPNs on every connection as a baseline security measure. They're paying customers, not fraudsters.
- Restricted internet access. Users in certain countries rely on VPNs to reach your product at all. Blocking VPNs locks them out completely.
- Public Wi-Fi. Anyone on a coffee shop or airport network should be using a VPN. Penalizing them for good security hygiene creates the wrong incentive.
- Journalists and researchers. People in these roles use Tor and VPNs for source protection and operational security. Blocking them can have outsized consequences.
The right approach: flag and score, don't hard-block. Use VPN detection as one input to a risk function that considers multiple signals. Route high-risk transactions to a review queue. Let humans make the final call on ambiguous cases.
Key points
-
POST /v1/vpn-detectreturnsis_vpn,is_proxy,is_tor,is_datacenter, andrisk_scorefor any IP. - No API key required for testing (5 requests per minute). Free keys unlock higher limits for production.
- Attach the VPN check to Express middleware so every route has access to the risk data without repeating the call.
- Combine VPN detection with disposable email checks and IP geolocation for a compound risk score that's strong enough to act on.
- Flag VPN connections for review. Don't block them. Legitimate users run VPNs for privacy, corporate policy, and restricted internet access.
Frequently asked questions
- How do I detect VPN users in my app?
- Send the user's IP address in a POST request to the botoi /v1/vpn-detect endpoint. The response includes boolean flags for is_vpn, is_proxy, is_tor, and is_datacenter, plus a 0-100 risk_score. Call this endpoint during signup, login, or checkout to flag connections from anonymizing services.
- Which VPN detection API is best for production apps?
- Look for an API that returns separate flags for VPN, proxy, Tor, and datacenter connections rather than a single boolean. The botoi /v1/vpn-detect endpoint returns all four flags plus a numeric risk score, and it works with no API key at 5 requests per minute. For production workloads, API keys unlock higher rate limits starting at the free tier.
- Should I block all VPN users from my app?
- No. Many legitimate users run VPNs for privacy, corporate policy, or because they live in regions with restricted internet access. Blocking all VPN traffic locks out paying customers. Instead, use VPN detection as one signal in a compound risk score and flag suspicious connections for review.
- Can I detect proxy and Tor connections with the same API call?
- Yes. The botoi /v1/vpn-detect endpoint returns separate boolean flags for is_vpn, is_proxy, and is_tor in a single response. You don't need separate API calls for each connection type. The endpoint also returns is_datacenter to identify traffic from cloud providers like AWS or Google Cloud.
- How do I combine VPN detection with other fraud signals?
- Call multiple botoi endpoints in parallel: /v1/vpn-detect for connection type, /v1/disposable-email/check for email quality, and /v1/ip/lookup for geo mismatch between the IP country and billing country. Weight each signal and sum them into a 0-100 risk score. Scores above 70 go to manual review; scores below 30 pass through.
Try this API
IP Intelligence API — interactive playground and code examples
More integration posts
Start building with botoi
150+ API endpoints for lookup, text processing, image generation, and developer utilities. Free tier, no credit card.