Add live proxy & VPN detection to your site

One script tag on your pages, one JSON webhook to your server. About five minutes, no SDK and nothing to host. Free of charge for developers who help us improve detection quality.

Quick start

1 Get your API key Create a free account; the key is on your dashboard right away. It is public by design — it only tells the engine which site a visitor came from.
2 Add the script tag One line before </body> on the pages you want to protect. Detection runs on every page view.
3 Receive a webhook Enter your callback URL on the dashboard. A JSON verdict is POSTed to it a few seconds later; act on it server-side.
The snippet — replace YOUR_API_KEY
<script src="https://engine.proxydetect.live/pd-lib.js?pdKey=YOUR_API_KEY&pdVal=SESSION_ID" async></script>

That is the whole integration. Your dashboard shows the same snippet with your key filled in, lets you save the callback URL, send a test callback to your endpoint and run a live detection against your own connection to see exactly what your visitors will produce. The rest of this page is reference.

Loading the script

The script is served from engine.proxydetect.live, the geo-routed detection engine (Germany and the United States). The request itself already triggers the passive, server-side tests — TCP/IP fingerprint, IP intelligence, network flow analysis — so a visitor with JavaScript disabled is still classified. Once it runs, the script adds the browser tests (WebRTC, timezone, latency, network behaviour) and reports back.

Parameters

Parameter Meaning
pdKey Your API key. Required — it identifies your account, so results are delivered to your callback URL and measured against your settings.
pdVal Any string you choose, URL-encoded. It is echoed back untouched as pdVal in the callback — use a session id, order id or user id so the verdict can be tied to the visitor. Optional but strongly recommended.

Loading it from JavaScript

Useful when the session id is only known at runtime, or when you want detection at a specific moment (login, checkout) rather than on every page.

// Load the detection script once per visit - after login, on checkout, wherever it matters.
const pdKey = 'YOUR_API_KEY';
const pdVal = 'SESSION_ID'; // your own session, order or user id - echoed back in the callback

const s = document.createElement('script');
s.src = 'https://engine.proxydetect.live/pd-lib.js?pdKey=' + pdKey + '&pdVal=' + encodeURIComponent(pdVal);
s.async = true;
document.head.appendChild(s);

React, Next.js and other single-page apps

Client-side routing does not reload the page, so mount the loader once per session instead of relying on the HTML tag. Re-run it when the session changes, for instance after login.

import { useEffect } from 'react';

export function ProxyDetect({ sessionId }) {
  useEffect(() => {
    if (!sessionId) return;
    const s = document.createElement('script');
    s.src = 'https://engine.proxydetect.live/pd-lib.js?pdKey=YOUR_API_KEY&pdVal=' + encodeURIComponent(sessionId);
    s.async = true;
    document.head.appendChild(s);
    return () => s.remove();
  }, [sessionId]);
  return null;
}

Google Tag Manager, WordPress, Shopify

Any place that lets you add a "custom HTML" block or a script tag works: paste the snippet as a Custom HTML tag firing on all pages (GTM), into the theme's footer (WordPress), or into theme.liquid before </body> (Shopify). In GTM you can pass {{Client ID}} or a data-layer variable as pdVal.

Callbacks

Results are delivered by HTTP POST with a JSON body and Content-Type: application/json to the callback URL on your dashboard. Answer with any 2xx status; the body is ignored. There is no retry, so acknowledge first and process afterwards. Requests come with the header User-Agent: pdServer <version>.

Active callback DEFAULT Sent once every test has finished, including the browser-based ones — typically 3–8 seconds after the page view. Use this for decisions.
Passive callback Sent within about a second, from the server-side tests alone. Enable it next to the active one when you need a fast first answer; both carry the same JSON shape.

Two more switches live under Delivery settings on the dashboard: only deliver positive results (skip the callback for clean visitors — recommended in production, but switch it off while you test from your own connection) and verdict only (the default: just is_proxy and is_vpn; switch it off to also receive scores and the per-test evidence). Changes are picked up by the engines within three minutes.

The Send test callback button on the dashboard POSTs the payload below to your URL with "type": "test". Handle that value explicitly so a test never flags a real session.

Result payload

By default the callback is as small as it gets: who it is about and whether the connection is a proxy or a VPN. This is the exact shape of both callback types:

{
  "type": "active",
  "uuid": "59d68ddfa135f3f0",
  "pdVal": "session-8f3a1c",
  "ip": "185.220.101.34",
  "is_proxy": true,
  "is_vpn": false
}

With verdict only switched off on the dashboard, the engine adds the scores, the client anomalies, every test's result and evidence, and timing:

{
  "type": "active",
  "uuid": "59d68ddfa135f3f0",
  "pdVal": "session-8f3a1c",
  "ip": "185.220.101.34",
  "is_proxy": true,
  "is_vpn": false,
  "proxy": { "score": 85, "informal": "85/100 - Very likely a Proxy", "positive": 4, "total": 9 },
  "vpn": { "score": 0, "informal": "0/100 - Very likely not a VPN", "positive": 0, "total": 6 },
  "client": { "threats": [] },
  "tests": {
    "datacenter_ip": { "is_proxy": true, "is_vpn": true, "ms": 120 },
    "tcpip_fp": { "is_proxy": true, "ms": 310, "info": { "tcpIpHighestOs": "Linux", "userAgentOs": "Windows" } },
    "timezone": { "is_proxy": true, "is_vpn": true, "ms": 1240, "info": { "browser": "Asia/Tehran", "ip": "Europe/Berlin", "delta": 5400, "reasons": ["timezone", "time"] } },
    "latency": { "is_proxy": true, "ms": 1690 },
    "webrtc": { "is_proxy": false, "ms": 1250 },
    "tor_detection": { "is_proxy": false, "is_vpn": false, "ms": 120 }
  },
  "servedAt": 1757680012053,
  "sentAt": 1757680015370
}
Field Meaning
type active, passive, or test for the dashboard button.
uuid Id of this detection session, unique per page view.
pdVal The value you passed in the snippet, unchanged. Your API key is never echoed.
ip The visitor's IP address as seen by the engine.
is_proxy / is_vpn The verdicts. A visitor can be both (a VPN provider's datacenter exit) or neither.
proxy / vpn Detailed only. score is 0–100, informal a sentence you can show, positive / total how many tests fired out of those that could be evaluated.
client.threats Detailed only. Names of client-side anomalies: missing standard headers, user agent mismatch between requests, blocked WebSockets, implausible timing. A bot signal, independent of proxies; empty when clean.
tests Detailed only. One entry per test: is_proxy and is_vpn are true, false, or null (inconclusive), ms is when the test finished after the page view, info its evidence.
servedAt / sentAt Detailed only. Unix timestamps in milliseconds: when the script was served and when this callback left.

Handling results

The simplest useful rule is is_proxy || is_vpn. For graded responses switch verdict only off and use the scores: above 60 is a solid signal for a CAPTCHA or extra verification, above 85 is worth blocking outright. Match the result to your visitor with pdVal; the callback arrives a few seconds after the page view, so store the verdict against the session and read it on the next request rather than blocking the current one.

Node.js / Express

const express = require('express');
const app = express();
app.use(express.json());

app.post('/proxydetect/callback', (req, res) => {
  const { type, pdVal, ip, is_proxy, is_vpn } = req.body;
  res.json({ msg: 'ok' }); // acknowledge first - a 2xx is all the engine needs

  if (type === 'test') return; // sent by the "Send test callback" button

  if (is_proxy || is_vpn) {
    // pdVal is whatever you put into the snippet: a session id, order id, user id ...
    flagSession(pdVal, { ip, is_proxy, is_vpn });
  }
});

app.listen(3000);

Python / Flask

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.post("/proxydetect/callback")
def proxydetect_callback():
    result = request.get_json(force=True)
    if result["type"] != "test":
        if result["is_proxy"] or result["is_vpn"]:
            flag_session(result["pdVal"], ip=result["ip"], proxy=result["is_proxy"], vpn=result["is_vpn"])
    return jsonify(msg="ok")

PHP

<?php
$result = json_decode(file_get_contents('php://input'), true);

if ($result['type'] !== 'test') {
    if ($result['is_proxy'] || $result['is_vpn']) {
        flag_session($result['pdVal'], $result['ip'], $result['is_proxy'], $result['is_vpn']);
    }
}

header('Content-Type: application/json');
echo json_encode(['msg' => 'ok']);

Reading results in the browser

The engine also exposes the current verdict of a session at GET https://engine.proxydetect.live/i?uuid=…. The script sets window.pds.config.uuid when it loads, so a page can poll until finished is true — this is what the demo on this site does. It is fine for showing a notice or deciding whether to render a CAPTCHA; it is not a substitute for the callback, because anything a browser reads a browser can also fake.

// Optional: read the verdict in the browser, e.g. to show a notice or a CAPTCHA.
const s = document.createElement('script');
s.src = 'https://engine.proxydetect.live/pd-lib.js?pdKey=YOUR_API_KEY&pdVal=SESSION_ID';
s.onload = async () => {
  const { uuid, endpoint } = window.pds.config;
  for (let i = 0; i < 30; i++) {
    const res = await fetch(endpoint + '/i?uuid=' + uuid).then(r => r.json());
    if (res.finished) {
      console.log('proxy', res.proxy.isProxy, res.proxy.score, 'vpn', res.vpn.isVpn, res.vpn.score);
      break;
    }
    await new Promise(r => setTimeout(r, 700));
  }
};
document.head.appendChild(s);

Detection tests

Every test contributes to the scores; all of them run unless you deselect some under Delivery settings on the dashboard. These are the ids you will see in the tests object:

Test What it looks at
http_headersHTTP headers reveal a forwarding proxy
datacenter_ipIP belongs to a hosting provider
proxy_ipIP is on a proxy list
vpn_ipIP is on a VPN list
enumerated_vpn_ipIP is a known VPN exit node
tor_detectionIP is a Tor exit node
tcpip_fpTCP/IP fingerprint contradicts the browser
timezoneBrowser timezone contradicts the IP location
netNetwork behaviour (DNS resolution, blocked requests)
webrtcWebRTC leaks a different IP
latencyWebSocket vs. TCP latency mismatch
latency_vs_pingHandshake latency vs. ping to the IP
high_latenciesLatencies too high for the claimed location
flow_patternPacket flow pattern of a tunnel
proxy_ai / vpn_aiPassive AI classifiers
portscanOpen proxy ports on the IP
browser_portscanLocal ports of the browser host
invalid_urlBehaviour on invalid URLs

FAQ

Does it slow my pages down?

No. The tag is async and under 30 KB (10 KB compressed); it never blocks rendering. All heavy lifting happens on the engine, and the browser tests run in the background after the page has loaded.

What if a visitor blocks JavaScript?

The request for the script alone already triggers the server-side tests, so a callback with a passive-quality verdict is still delivered. In a detailed callback client.threats contains POST_PAYLOAD_NEVER_RECEIVED when the browser part never arrived.

I use a Content-Security-Policy.

Allow script-src https://engine.proxydetect.live and connect-src https://engine.proxydetect.live https://engine.proxydetect.live:22379 wss://engine.proxydetect.live:7630, plus img-src https://engine.proxydetect.live for the latency probes. Tests that need to reach other hosts degrade to inconclusive under a strict policy; the verdict still works.

Can I use several websites with one key?

Yes. The key identifies your account, not a domain. Put the domain or product into pdVal if you need to tell them apart.

Where is the data processed, and how long is it kept?

On our own engines in Germany and the United States, geo-routed by proximity. Sessions are kept in memory for a few minutes to compute the verdict, then stored briefly for analysis and quality control before they are removed. No cookies are set and no visitor is tracked across sites.

Why do I get no callbacks on my own machine?

Check that only deliver positive results is off while testing — on a clean home connection that setting suppresses the callback by design. Then run the live test on the dashboard; it uses your key and delivers a real callback. Saved settings reach the engines within three minutes.

Ready to try it? Your key, the snippet with it filled in and a live test are on the dashboard.