Guide · fraud prevention

Detect VPN, proxy and Tor users

VPN detection is a signal, not a verdict. How it works, what an IP security API returns, and how to use it in sign-up flows without locking out privacy-conscious users.

Updated October 29, 2026 · 8 min read

VPN detection means checking whether a visitor’s IP address belongs to a VPN server, an open proxy, a Tor exit node, a privacy relay or a data center, and using the answer as one signal in a risk decision. You don’t need to maintain the lists yourself: an IP security API returns all flags and a threat score in one request. This guide explains how detection works, what the flags mean, and how to add it to sign-up and checkout in Node.js and Python without locking out people who use a VPN for good reasons.

How VPN detection works

An address is flagged when one or more of these sources list it:

SignalWhat it catchesReliability
VPN server listsExit servers of commercial and consumer VPN servicesHigh for commercial VPNs; they run on known server ranges
Open proxy listsPublic HTTP and SOCKS proxiesHigh while the proxy is listed; proxies come and go quickly
Tor exit listsTor exit nodes, published by the Tor network itselfVery high
Privacy relaysiCloud Private Relay egress addresses, published by AppleVery high, and the users are ordinary Safari users
Data center rangesCloud and hosting providers (see the cloud IP ranges)High; real users rarely browse from AWS or DigitalOcean
Threat feedsAddresses seen in attacks, spam, brute force or botnetsGood for recent activity; addresses get reassigned

What detection cannot see well: residential proxies, which route traffic through real home connections (what a residential proxy is), self-hosted VPNs on a home or office line, and mobile networks where thousands of users share one address behind carrier-grade NAT. Treat a clean result as “no known signal”, not as proof.

What an IP security API returns

The ipbase info endpoint returns a security object next to the location. These are the fields to build rules on:

FieldMeaning
is_vpnServer of a commercial or consumer VPN service
is_proxyPublic or open proxy (HTTP, SOCKS)
is_torTor exit node
is_icloud_relayiCloud Private Relay egress address
is_datacenterAddress of a data center, cloud, hosting or CDN network
is_anonymousAny of Tor, VPN, open proxy or privacy relay
proxy_typetor, vpn, proxy, relay or an empty string
is_abuser, is_known_attacker, is_spamRecently reported for abuse, attacks or spam
is_botVerified crawler or monitor (for example Googlebot) or a research scanner
threat_score0 (no known threat) to 100
is_corporate_gatewayEgress of a corporate security gateway such as Zscaler, which VPN lists often list too
vpn.provider, vpn.last_seen, vpn.confidenceWhich VPN service, when the lists last saw the address, how strongly they back the flag
reasonsOne short sentence per flag, such as Tor exit node, seen 2026-10-16

The documentation gives examples for the threat score: a data center address alone scores 10, a VPN server 30, a VPN server in a data center 40, a Tor exit node 60, and a Tor exit node that is also reported for attacks 90. Verified crawlers and allowlisted infrastructure such as public DNS resolvers score 0.

The security data is part of the Medium plan and above. With a free key the security object is still in the response, but every field in it is null. Code that only checks if (security.is_vpn) then lets everyone through without noticing, so the examples below check for null explicitly.

Step 1: check an address

Try a single address with the free VPN and proxy checker, or with curl and your key:

curl -G https://api.ipbase.com/v2/info \
  -d ip=203.0.113.50 \
  -d fields=ip,security \
  -H "apikey: $IPBASE_KEY"

For a Tor exit node the answer looks like this (abridged):

{
  "data": {
    "ip": "203.0.113.50",
    "security": {
      "is_anonymous": true,
      "is_datacenter": false,
      "is_vpn": false,
      "is_proxy": false,
      "is_tor": true,
      "proxy_type": "tor",
      "is_icloud_relay": false,
      "threat_score": 60,
      "tor": { "last_seen": "2026-10-16", "confidence": 0.95 },
      "is_corporate_gateway": false,
      "reasons": ["Tor exit node, seen 2026-10-16"]
    }
  }
}

The confidence values rank how strongly the lists back a flag; the documentation says they are hand-weighted and not a calibrated probability.

Step 2: decide what each signal means for you

A flag says something about the network, not about the person. Match the response to the risk of the action: a newsletter sign-up needs less than a payout or a free trial with credits.

SignalTypical response
Tor exit nodeBlock or challenge sensitive actions (payments, payouts, trials)
Known attackerBlock, or require strong verification
Commercial VPN or open proxyAsk for email or phone verification; allow browsing
Data centerLikely automated: CAPTCHA, rate limits, verification
Abuse or spam reports, threat score 50 and aboveVerification, closer review
iCloud Private RelayAllow; it is a mainstream Apple feature
Corporate security gatewayAllow; it is office traffic, even when VPN lists include it
No flagsAllow; it is not proof of a residential user

If your product is licensed by country, combine the flags with the country: a streaming or betting service blocks VPN traffic because the location cannot be trusted, not because the user is malicious. The geo blocking guide shows the country side.

Step 3: add VPN detection to sign-up

The function asks only for the security object, turns the flags into one of four actions and keeps the reason for your logs. It fails open: if the lookup fails or times out, the user can sign up, so an outage on either side never blocks your funnel.

// risk.mjs – Node.js 18 or later
const API = "https://api.ipbase.com/v2/info";

export async function riskCheck(ip) {
  let s;
  try {
    const res = await fetch(`${API}?${new URLSearchParams({ ip, fields: "security" })}`, {
      headers: { apikey: process.env.IPBASE_KEY },
      signal: AbortSignal.timeout(3000),
    });
    if (!res.ok) return { action: "allow", reason: `lookup failed (${res.status})` };
    s = (await res.json()).data.security;
  } catch (err) {
    return { action: "allow", reason: `lookup failed (${err.name})` }; // fail open
  }

  // A plan without security data gets every flag as null: that is not "clean"
  if (s.threat_score === null) return { action: "unknown", reason: "no security data in this plan" };

  if (s.is_tor) return { action: "block", reason: "Tor exit node" };
  if (s.is_known_attacker) return { action: "block", reason: "known attacker" };
  if (s.is_corporate_gateway) return { action: "allow", reason: "corporate security gateway" };
  if (s.is_icloud_relay) return { action: "allow", reason: "iCloud Private Relay" };
  if (s.is_vpn || s.is_proxy) return { action: "verify", reason: s.is_vpn ? "VPN" : "proxy" };
  if (s.is_datacenter) return { action: "verify", reason: "data center" };
  if (s.is_abuser || s.is_spam || s.threat_score >= 50) return { action: "verify", reason: `threat score ${s.threat_score}` };
  return { action: "allow", reason: "no flags" };
}

The order matters: Tor and known attackers first, then the two allow cases (a corporate gateway can also carry is_vpn), then the signals that call for verification. In an Express route behind one proxy:

// signup.mjs – Express 5
import express from "express";
import { riskCheck } from "./risk.mjs";

const app = express();
app.set("trust proxy", 1);
app.use(express.json());

app.post("/signup", async (req, res) => {
  const risk = await riskCheck(req.ip);
  console.log("signup risk", req.ip, risk); // keep the reason for support and tuning

  if (risk.action === "block") {
    return res.status(403).json({ error: "Sign-ups from this network are not possible. Please contact support." });
  }
  // "verify" and "unknown": create the account, but confirm email or phone first
  const needsVerification = risk.action !== "allow";
  res.status(201).json({ email: req.body.email, needsVerification });
});

app.listen(3000);

We ran both files with Node.js 26 and Express 5 against a local stub of the API: a Tor exit got a 403, a VPN address an account with needsVerification: true, a clean address one without, a free key unknown, and an invalid key or an unreachable API allow with the reason logged. Read req.ip only after you set trust proxy for your setup; the IP geolocation tutorial explains how to find the real client IP.

Step 4: check existing accounts in bulk

To review accounts you already have, send their last login addresses to the batch endpoint: up to 1,000 addresses per POST with an API key, each valid address counting as one request.

# audit.py – flag existing accounts by their last login IP, 1,000 addresses per request
import os

import requests

FIELDS = "ip,security.is_vpn,security.is_proxy,security.is_tor,security.is_datacenter,security.threat_score"


def flagged(ips: list[str]) -> dict[str, dict]:
    unique = list(dict.fromkeys(ips))
    found: dict[str, dict] = {}
    with requests.Session() as session:
        session.headers["apikey"] = os.environ["IPBASE_KEY"]
        for start in range(0, len(unique), 1000):
            res = session.post(
                "https://api.ipbase.com/v2/batch",
                params={"fields": FIELDS},
                json={"ips": unique[start : start + 1000]},
                timeout=60,
            )
            res.raise_for_status()
            for ip, row in res.json()["data"].items():
                s = row.get("security") or {}
                if s.get("threat_score") is None:
                    raise RuntimeError("this API key's plan has no security data")
                if s["is_vpn"] or s["is_proxy"] or s["is_tor"] or s["is_datacenter"]:
                    found[ip] = s
    return found

With a free key the function stops with an error instead of reporting that no account is flagged. We ran it with Python 3.15 and requests against the stub.

Avoiding false positives

Privacy

IP addresses are personal data under the GDPR. Fraud prevention is a common reason to process them, but say in your privacy policy that you check sign-up and payment addresses with an external service, and keep only the decision and the reason, not the full lookup. ipbase’s sub-processors and trust center list how the data is handled on our side.

Frequently asked questions

How does VPN detection work?

The visitor's IP address is checked against lists of VPN servers, open proxies, Tor exit nodes and privacy relays, against the address ranges of data centers and cloud providers, and against threat feeds. An IP security API does all of this in one request and returns flags such as is_vpn, is_tor and is_datacenter plus a threat score.

Can you detect every VPN?

No. Commercial VPNs run on known data-center addresses and are detected reliably; residential proxies and self-hosted VPNs on home connections are much harder, because their traffic comes from ordinary consumer addresses.

Should I block all VPN users?

Usually not. Many people use a VPN for privacy, at work or on public Wi-Fi. Block Tor exit nodes and known attackers on sensitive actions, ask VPN, proxy and data-center users for an extra verification step, and allow iCloud Private Relay and corporate security gateways.

Is it legal to block VPN users?

Generally yes, websites may decide whom to serve. Mention the IP lookup in your privacy policy, and where you can, offer people who rely on a VPN for their safety another way to verify instead of blocking them.

Does the free plan include VPN detection?

No. A free key returns the security object with every field set to null; the VPN, proxy, Tor, data center and threat score data start with the Medium plan. The free VPN and proxy checker shows the flags for single addresses.

IP geolocation API

Get this data in your app

ipbase returns location, ISP, ASN and time zone for any IPv4 or IPv6 address, up to 1,000 addresses per POST request. Security flags (VPN, proxy, Tor, data center) are on the Medium plan and above.

Read the documentation

1,000 free requests every month. No credit card required.

GET https://api.ipbase.com/v2/info?ip=1.1.1.1

{
  "data": {
    "ip": "1.1.1.1",
    "type": "v4",
    "connection": {
      "asn": 13335,
      "organization": "APNIC and Cloudflare DNS Resolver project",
      "isp": "Cloudflare, Inc",
      "range": "1.1.1.0/24"
    },
    "location": {
      "country": { "alpha2": "AU", "name": "Australia" },
      "zip": "4000"
    },
    "security": { "is_vpn": null, "is_tor": null, "threat_score": null }
  }
}

Keep reading

Get your free IP geolocation API key

Start with 1,000 free requests a month