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
</body> on the pages you want to protect. Detection runs on every
page view.
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>.
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_headers | HTTP headers reveal a forwarding proxy |
datacenter_ip | IP belongs to a hosting provider |
proxy_ip | IP is on a proxy list |
vpn_ip | IP is on a VPN list |
enumerated_vpn_ip | IP is a known VPN exit node |
tor_detection | IP is a Tor exit node |
tcpip_fp | TCP/IP fingerprint contradicts the browser |
timezone | Browser timezone contradicts the IP location |
net | Network behaviour (DNS resolution, blocked requests) |
webrtc | WebRTC leaks a different IP |
latency | WebSocket vs. TCP latency mismatch |
latency_vs_ping | Handshake latency vs. ping to the IP |
high_latencies | Latencies too high for the claimed location |
flow_pattern | Packet flow pattern of a tunnel |
proxy_ai / vpn_ai | Passive AI classifiers |
portscan | Open proxy ports on the IP |
browser_portscan | Local ports of the browser host |
invalid_url | Behaviour 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.