Skip to content
integration

How to detect VPN users in your app with one API call

| 7 min read
Lock icon on a digital network background representing VPN security
Photo by Towfiqu barbhuiya on Unsplash

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.

Network cables connected to a server switch in a datacenter
VPN detection identifies traffic routed through VPN servers, proxies, Tor exit nodes, and cloud datacenters Photo by Jordan Harrison on Unsplash

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-detect for connection type (VPN, proxy, Tor, datacenter)
  • /v1/disposable-email/check for email quality (throwaway vs permanent)
  • /v1/ip/lookup for 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-detect returns is_vpn, is_proxy, is_tor, is_datacenter, and risk_score for 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.