iplogs.com

How to detect VPN users on your website

A practical, vendor-neutral guide for fraud teams, ad-ops, support, and platform engineers. Copy-paste examples in Node.js, Python, Go, PHP, and Cloudflare Workers. No SDK required — the underlying API is free and unauthenticated.

What VPN detection actually means

"Is this IP a VPN?" sounds like a yes/no question, but in practice you'll get back a bucket: clean, suspicious (datacenter), vpn_likely, or vpn_detected. The right product action depends on the bucket, not on a single boolean.

  • vpn_detected — confirmed VPN exit (multi-source agreement). Block writes, step up auth.
  • vpn_likely — strong signals but no ground-truth confirmation. Score as elevated risk.
  • suspicious — datacenter / hosting IP. Almost never a real residential consumer; could be a scraper, automated browser, or self-hosted VPN.
  • clean — residential or trusted IP. No detection signals matched.

Server-side detection (recommended)

Call POST /v1/check from your backend with the visitor IP from your reverse-proxy headers. Cache the verdict for 5 minutes per IP.

Node.js (TypeScript)

async function checkVpn(ip: string) {
  const r = await fetch("https://iplogs.com/v1/check", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ ip }),
  });
  const data = await r.json();
  return {
    verdict: data.verdict,        // clean | suspicious | vpn_likely | vpn_detected
    isVpn: data.is_vpn,
    score: data.score,            // 0–1
    provider: data.ip_info?.vpn_provider ?? null,
    sources: data.ip_info?.vpn_provider_sources ?? [],
  };
}

Python

import httpx

def check_vpn(ip: str) -> dict:
    r = httpx.post(
        "https://iplogs.com/v1/check",
        json={"ip": ip},
        timeout=5.0,
    )
    r.raise_for_status()
    return r.json()  # verdict, score, is_vpn, ip_info, signals[]

Go

type Verdict struct {
    Verdict string  `json:"verdict"`
    Score   float64 `json:"score"`
    IsVPN   bool    `json:"is_vpn"`
}

func CheckVPN(ctx context.Context, ip string) (*Verdict, error) {
    body, _ := json.Marshal(map[string]string{"ip": ip})
    req, _ := http.NewRequestWithContext(ctx, "POST",
        "https://iplogs.com/v1/check", bytes.NewReader(body))
    req.Header.Set("content-type", "application/json")
    resp, err := http.DefaultClient.Do(req)
    if err != nil { return nil, err }
    defer resp.Body.Close()
    var v Verdict
    return &v, json.NewDecoder(resp.Body).Decode(&v)
}

PHP

function checkVpn(string $ip): array {
    $ctx = stream_context_create([
        "http" => [
            "method"  => "POST",
            "header"  => "content-type: application/json",
            "content" => json_encode(["ip" => $ip]),
            "timeout" => 5,
        ],
    ]);
    $body = file_get_contents("https://iplogs.com/v1/check", false, $ctx);
    return json_decode($body, true);
}

Edge detection (Cloudflare Worker)

Run the check at the edge before requests hit your origin. Useful for blocking VPN traffic on signup or login routes without modifying application code.

export default {
  async fetch(req: Request) {
    const ip = req.headers.get("cf-connecting-ip") ?? "";
    const r = await fetch("https://iplogs.com/v1/check", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ ip }),
    });
    const { verdict } = await r.json();
    if (verdict === "vpn_detected" && req.method !== "GET") {
      return new Response("VPN traffic blocked on this route", { status: 403 });
    }
    return fetch(req);
  },
};

What signals to weight in your own risk model

Don't just trust a single boolean. The IPLogs response includes a signals[] array — every signal that fired with its weight. Roll-your-own scoring example:

function customRisk(detection) {
  let risk = detection.score;
  // Boost for confirmed commercial VPN
  if (detection.ip_info?.vpn_provider) risk += 0.1;
  // Boost for any threat-intel hit
  if (detection.signals?.some(s => s.type === "threat_intel_listed" && s.matched))
    risk += 0.2;
  // Discount for trusted-infra IPs (DNS resolvers, CDN edge)
  if (detection.signals?.some(s => s.type === "asn_trusted_infra" && s.matched))
    risk = 0.0;
  return Math.min(1, risk);
}

Common pitfalls

  • Don't trust client-reported IP. Always read from your reverse-proxy header (X-Forwarded-For, CF-Connecting-IP, X-Real-IP) — never from JavaScript or request body.
  • Cache aggressively. Per-IP 5-minute cache cuts API load 100x and shaves ~150ms off the critical path.
  • Treat Apple Private Relay separately. Many of your real customers are on it by default. Hard-blocking it costs you signups.
  • Don't show the verdict to the user. "We blocked you because you're on a VPN" tells the attacker exactly which signal to evade. Use a generic message.

FAQ

+What's the most reliable way to detect VPN users?

Cross-source confirmation. Single feeds drift; ground-truth ingestion from the VPN provider's own API plus active protocol probing eliminates ~99% of false positives. Aggregator-only solutions (a single CIDR feed) miss new exits and falsely flag CGNAT.

+Should I block VPN users entirely?

Almost never. Most consumer VPN traffic is legitimate (privacy, geoblocked content, untrusted networks). Block at high-friction surfaces (signup, checkout, password reset) and step up auth on payments. For read traffic, a hard block punishes loyal users.

+Will VPN detection slow down my requests?

Not if you do it right. Cache verdicts per IP for 5 minutes (Redis or in-memory), and the median request adds <1ms. The IPLogs API itself returns sub-2-second on a cold IP and <1ms on a warm one.

+Do I need to handle iCloud Private Relay and Cloudflare WARP separately?

Yes. Both are "anonymizing" but neither is a commercial VPN — and many of your most loyal users will be on Apple Private Relay by default. Treat them as a separate signal type ("private_relay") and apply step-up only when combined with other risk factors.

Once you've integrated detection, see copy-paste blocking recipes for Cloudflare WAF, Nginx, and Stripe Radar. Or read the full API reference.