Skip to content
guide

VPN and proxy detection API: flag abuse without blocking users

| 7 min read
Digital shield icon with network connections
Photo by Privecstasy on Unsplash

Your SaaS app offers a free trial per user. Users sign up, activate a VPN, and sign up again. Promo abuse costs you revenue and skews your conversion metrics. You need to flag VPN and proxy connections at the point of signup.

This guide covers the botoi POST /v1/vpn-detect endpoint: what it returns, how to integrate it into Next.js and Express apps, how to combine it with other fraud signals, and where it falls short.

The endpoint

One POST request with an IP address in the body. No special headers, no API key required for anonymous access.

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:

{
  "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:

{
  "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
    }
  }
}

Response fields

  • is_vpn (boolean): True if the IP belongs to a known datacenter range or has a suspicious reverse DNS hostname containing VPN-related keywords.
  • is_proxy (boolean): True if the reverse DNS hostname suggests a proxy server.
  • is_tor (boolean): True if the IP matches a known Tor exit node.
  • is_datacenter (boolean): True if the IP falls within AWS, Google Cloud, Azure, DigitalOcean, or Linode CIDR ranges.
  • provider (string or null): The cloud provider name when is_datacenter is true. Null for residential and Tor IPs.
  • risk_score (number, 0-100): Tor connections score 90, datacenter IPs score 60, suspicious hostnames score 40. Clean residential IPs score 0.
  • checks (object): Breakdown of which detection methods triggered, for debugging.
Network cables connected to a server switch
VPN detection checks whether an IP routes through a VPN, proxy, or Tor node Photo by Jordan Harrison on Unsplash

Flag VPN users at signup with Next.js middleware

This middleware intercepts POST requests to your signup route, checks the caller's IP against the VPN detection API, and attaches a header to the request when a VPN is detected. Your signup handler reads the header and decides what to do: require email verification, add a manual review flag, or reduce the trial period.

import { NextRequest, NextResponse } from 'next/server';

const VPN_DETECT_URL = 'https://api.botoi.com/v1/vpn-detect';

export async function middleware(req: NextRequest) {
  if (req.method !== 'POST') {
    return NextResponse.next();
  }

  const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim()
    || req.ip
    || '127.0.0.1';

  try {
    const res = await fetch(VPN_DETECT_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ ip }),
      signal: AbortSignal.timeout(3000),
    });

    const { data } = await res.json();

    if (data.is_vpn || data.is_tor || data.is_proxy) {
      // Add a header so your signup handler can flag the account
      const response = NextResponse.next();
      response.headers.set('x-vpn-detected', 'true');
      response.headers.set('x-vpn-risk-score', String(data.risk_score));
      return response;
    }
  } catch {
    // Fail open: if the API is unreachable, let the request through
    console.warn('VPN detection check failed, allowing request');
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/api/auth/signup', '/api/auth/register'],
};

The middleware doesn't block the signup. It passes a signal to your handler. This is the right approach because VPN traffic is not proof of fraud. A user on a corporate VPN or traveling through a restrictive network is a legitimate customer.

Stricter rate limits for VPN IPs in Express

If you run an API, you can apply tighter rate limits to VPN and proxy connections without blocking them outright. This middleware gives standard users 100 requests per hour and VPN users 20.

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;

// Standard limits
const STANDARD_LIMIT = 100; // requests per hour
const VPN_LIMIT = 20;       // requests per hour for VPN users

const requestCounts = new Map<string, { count: number; resetAt: number }>();

export async function vpnAwareRateLimit(
  req: Request,
  res: Response,
  next: NextFunction
) {
  const ip = req.headers['x-forwarded-for']?.toString().split(',')[0]?.trim()
    || req.socket.remoteAddress
    || 'unknown';

  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 if detection fails
  }

  const limit = isVpn ? VPN_LIMIT : STANDARD_LIMIT;
  const now = Date.now();
  const entry = requestCounts.get(ip);

  if (!entry || entry.resetAt < now) {
    requestCounts.set(ip, { count: 1, resetAt: now + 3600_000 });
    res.setHeader('X-RateLimit-Limit', limit);
    return next();
  }

  entry.count++;

  if (entry.count > limit) {
    return res.status(429).json({
      error: 'Rate limit exceeded',
      retryAfter: Math.ceil((entry.resetAt - now) / 1000),
    });
  }

  res.setHeader('X-RateLimit-Limit', limit);
  res.setHeader('X-RateLimit-Remaining', limit - entry.count);
  next();
}

The 5:1 ratio between standard and VPN limits is a starting point. Tune it based on your abuse patterns. If your API handles payments or account modifications, tighter VPN limits make sense. For read-only endpoints, you might not need differential limits at all.

Fraud scoring: combine VPN detection with other signals

VPN detection alone is a weak fraud signal. A VPN + disposable email + new account + failed payment attempts is a strong signal. This function combines multiple inputs into a single fraud score.

interface FraudSignals {
  vpnDetected: boolean;
  riskScore: number;
  disposableEmail: boolean;
  accountAge: number; // days
  failedPayments: number;
}

function calculateFraudScore(signals: FraudSignals): number {
  let score = 0;

  // VPN/proxy risk contributes up to 30 points
  if (signals.vpnDetected) {
    score += Math.round(signals.riskScore * 0.3);
  }

  // Disposable email: strong signal
  if (signals.disposableEmail) {
    score += 35;
  }

  // New account + VPN is a red flag
  if (signals.accountAge < 1 && signals.vpnDetected) {
    score += 20;
  }

  // Failed payment history
  score += Math.min(signals.failedPayments * 10, 30);

  return Math.min(score, 100);
}

async function assessSignupRisk(ip: string, email: string) {
  const [vpnRes, emailRes] = await Promise.all([
    fetch('https://api.botoi.com/v1/vpn-detect', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ ip }),
    }),
    fetch('https://api.botoi.com/v1/disposable-email/check', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email }),
    }),
  ]);

  const vpnData = (await vpnRes.json()).data;
  const emailData = (await emailRes.json()).data;

  const fraudScore = calculateFraudScore({
    vpnDetected: vpnData.is_vpn || vpnData.is_tor,
    riskScore: vpnData.risk_score,
    disposableEmail: emailData.is_disposable,
    accountAge: 0, // new signup
    failedPayments: 0,
  });

  return {
    fraudScore,
    action: fraudScore > 70 ? 'block' : fraudScore > 40 ? 'review' : 'allow',
    details: {
      vpn: vpnData.is_vpn,
      tor: vpnData.is_tor,
      proxy: vpnData.is_proxy,
      disposableEmail: emailData.is_disposable,
    },
  };
}

Both API calls run in parallel, so the total latency is the slower of the two (typically under 100ms). The action field gives you three tiers: allow, review, or block. For scores between 40 and 70, route the signup to a manual review queue rather than auto-rejecting it.

Caching to reduce API calls

An IP address doesn't change its VPN status every request. Cache the result for 10 minutes to cut your API usage without missing status changes.

const vpnCache = new Map<string, { result: VpnResult; expiresAt: number }>();
const CACHE_TTL = 10 * 60 * 1000; // 10 minutes

interface VpnResult {
  isVpn: boolean;
  isTor: boolean;
  isProxy: boolean;
  riskScore: number;
}

async function checkVpn(ip: string): Promise<VpnResult> {
  const cached = vpnCache.get(ip);
  if (cached && cached.expiresAt > Date.now()) {
    return cached.result;
  }

  try {
    const res = await fetch('https://api.botoi.com/v1/vpn-detect', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ ip }),
      signal: AbortSignal.timeout(3000),
    });

    const { data } = await res.json();
    const result: VpnResult = {
      isVpn: data.is_vpn,
      isTor: data.is_tor,
      isProxy: data.is_proxy,
      riskScore: data.risk_score,
    };

    vpnCache.set(ip, { result, expiresAt: Date.now() + CACHE_TTL });
    return result;
  } catch {
    return { isVpn: false, isTor: false, isProxy: false, riskScore: 0 };
  }
}

For multi-server deployments, swap the in-memory Map for Redis or Upstash. The same cache key (IP address) and TTL pattern apply.

What this API can't detect

Honesty about limitations matters more than a polished sales pitch. Here's where VPN detection falls short.

  • Residential VPNs. Services like iCloud Private Relay and some configurations of Mullvad route traffic through residential IP addresses. These IPs look identical to regular home internet connections. No reverse DNS or CIDR-based detection catches them.
  • Mobile carrier NAT. Mobile carriers use carrier-grade NAT, which means thousands of users share a single IP address. Flagging these IPs affects legitimate users. The API may return is_datacenter: false and risk_score: 0 for these IPs even when a VPN user is behind them.
  • Private SOCKS5 proxies. Proxies hosted on personal servers with residential ISPs don't appear in datacenter CIDR ranges and don't have suspicious hostnames. They're invisible to automated detection.
  • IPv6-only connections. The current endpoint supports IPv4 addresses only. IPv6 VPN detection isn't available.
  • False positives on shared hosting. A developer running a side project on a DigitalOcean droplet will trigger is_datacenter: true. That doesn't mean they're hiding their identity.

The detection works well for commercial VPN providers (NordVPN, ExpressVPN, Surfshark, CyberGhost) and Tor traffic. It catches most datacenter-hosted proxies. It doesn't catch residential VPNs or private proxies. Expect 80-90% detection for the common cases and near-zero for edge cases.

Flag, don't block

Blocking VPN users outright is a mistake for most applications. Here's why:

  • Privacy-conscious users with no intent to abuse your service run VPNs as a default.
  • Employees on corporate VPNs route all their traffic through company infrastructure.
  • Users in countries with internet restrictions rely on VPNs to access your product at all.
  • Journalists, activists, and security researchers use Tor for legitimate reasons.

The right approach: add a risk flag to the account, require additional verification (email confirmation, phone number, payment method), or route the signup 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, provider, and risk_score for any IPv4 address.
  • No API key required for anonymous access (5 requests per minute). Free keys unlock 1,000 requests per day.
  • Detection covers commercial VPN providers, known Tor exit nodes, and major cloud provider IP ranges. Residential VPNs and private proxies slip through.
  • Combine VPN detection with disposable email checks, account age, and payment history for a meaningful fraud score. VPN status alone isn't enough to act on.
  • Flag VPN connections for review. Don't block them. Legitimate users run VPNs for privacy, corporate policy, and access in restricted regions.

Frequently asked questions

How do I detect VPN users in my app?
Send the user's IP address to a VPN detection API (like POST /v1/vpn-detect) and check the is_vpn boolean in the response. The API also returns is_proxy, is_tor, and is_datacenter flags so you can distinguish between different anonymization methods. Call this endpoint during signup, login, or checkout to flag suspicious connections for review.
Can a VPN detection API catch every VPN connection?
No. VPN detection works by checking known datacenter IP ranges, reverse DNS hostnames, and Tor exit node lists. Residential VPN services route traffic through home ISP addresses, which look identical to regular residential traffic. Expect 80-90% detection of commercial VPNs (NordVPN, ExpressVPN, Surfshark) but lower rates for residential VPNs and private proxies.
Should I block all VPN users from my application?
No. Many legitimate users run VPNs for privacy, corporate policy, or because they live in regions with restricted internet access. Blocking VPN traffic outright will lock out paying customers. Instead, flag VPN connections as higher risk and combine them with other signals (email domain, payment method, account age) to make a decision.
What is the difference between VPN, proxy, and Tor detection?
A VPN encrypts all traffic through a tunnel to a server, usually run by a commercial provider. A proxy routes traffic through an intermediary server without full encryption. Tor routes traffic through multiple volunteer-operated relays for maximum anonymity. The botoi API returns separate boolean flags for each type so you can apply different policies to each.
Does the botoi VPN detection API require an API key?
No. Anonymous access allows 5 requests per minute with no API key. For production workloads, a free API key raises the limit to 1,000 requests per day. Paid plans start at $9/month for higher rate limits.

Try this API

IP Intelligence API — interactive playground and code examples

More guide posts

Start building with botoi

150+ API endpoints for lookup, text processing, image generation, and developer utilities. Free tier, no credit card.