VPN detection API that names the VPN when it’s known
Our script runs on your signup or login page, your server sends the visit’s token to /v1/evaluate, and you get back allow, review or block, with the VPN service in network.service when it’s known.
- Free plan: 10,000 visitor checks a month
- No card
- Paid plans from €29 a month
- IP address
198.51.100.18- VPN
- Proton VPN
- Proxy
- Not detected
- Tor exit
- Not detected
- Cloud server
- Detected
- Country
- Netherlands
"decision": "review""reasons": ["vpn_detected", "datacenter_asn"]What a check tells your server
decision- Allow, review or block. A VPN on its own is review. A proxy, a Tor exit or an automated or fake browser is block; Apple’s Private Relay is review. A cloud server on its own is allow.
network.vpn- True when the visit comes through a VPN the check knows.
network.proxy,network.torandnetwork.datacentersit beside it. network.service- The VPN service, such as
PROTON_VPN, when it’s known, andnullwhen it isn’t. reasons- Short codes such as
vpn_detectedanddatacenter_asnto log and route on. risk_score- 0 to 100, for sorting and thresholds. It isn’t the probability that someone is a fraudster.
You can change any default per signal under Rules in the dashboard. A rule changes the answer only for the signal it names, so allowing VPNs never lets a proxy or a fake browser through.
Add it in front of your login
Add <script async src="https://maskbreak.com/assets/sentinel.js"></script> to the page and the class monocle-enriched to the form: the form then carries the visit’s token (monocle), its browser check (sentinel_fp) and its time zone (sentinel_tz). Your server sends them to /v1/evaluate before your own login handler runs. The Python and PHP examples use the official SDKs.
// Server-side only (Node 18+, Express). Runs before your existing login handler.
// 'watch' (default): log what Maskbreak would do, always continue.
// 'enforce': stop block; hold review, missing evidence and errors
// (a used-up month excepted: logged, then the login continues).
const MASKBREAK_MODE = process.env.MASKBREAK_MODE || 'watch';
app.post('/login', async (req, res, next) => {
const token = req.body.token || req.body.monocle;
const fingerprintEventId = req.body.fingerprintEventId || req.body.sentinel_fp;
let result = null, hold = null, monthUsedUp = false;
if (!token) {
hold = [409, 'Verification required.'];
} else {
try {
const response = await fetch('https://maskbreak.com/v1/evaluate', {
method: 'POST',
signal: AbortSignal.timeout(5000),
headers: {
Authorization: 'Bearer ' + process.env.MASKBREAK_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({ token, fingerprintEventId, tz: req.body.tz || req.body.sentinel_tz })
});
if (!response.ok) {
const refusal = await response.json().catch(() => null);
// The month's checks are used up: let the login through, logged, until the 1st.
if (response.status === 429 && refusal?.code === 'monthly_quota_exceeded') monthUsedUp = true;
else throw new Error('Evaluation unavailable');
} else {
result = await response.json();
if (!['allow', 'review', 'block'].includes(result.decision) ||
result.test || result.sample || result.sandbox) throw new Error('Not a live verdict');
if (result.decision === 'block') hold = [403, 'Request declined.'];
else if (result.decision === 'review' || result.degraded) {
hold = [409, 'Verification required.']; // e.g. send an email code
}
}
} catch {
hold = [503, 'Verification unavailable. Try again.'];
}
}
console.log('[maskbreak]', JSON.stringify({
mode: MASKBREAK_MODE, decision: result?.decision ?? null, reasons: result?.reasons ?? null,
enforce_would: hold ? 'return ' + hold[0] : monthUsedUp ? 'continue (month used up)' : 'continue'
}));
if (MASKBREAK_MODE === 'enforce' && hold) return res.status(hold[0]).json({ error: hold[1] });
return next(); // watch mode, or allow: your normal login runs
}, existingLoginHandler);# pip install sentinelsup (Flask shown; any framework works)
import json
import os
from flask import request, abort
from sentinel import Sentinel, SentinelError
maskbreak = Sentinel(api_key=os.environ["MASKBREAK_API_KEY"])
MODE = os.environ.get("MASKBREAK_MODE", "watch") # watch: log only; enforce: act on it
@app.post("/login")
def login():
result, hold = None, None
try:
result = maskbreak.evaluate(
token=request.form.get("monocle", ""),
fingerprint_event_id=request.form.get("sentinel_fp"),
)
if result.test:
hold = 400 # a test token must never pass a live action
elif result.decision == "block":
hold = 403
elif result.decision == "review" or result.raw.get("degraded"):
hold = "verify"
except SentinelError as e: # missing token, timeout or API error
# The month's checks are used up: let the login through, logged, until the 1st.
if e.status == 429 and e.body.get("code") == "monthly_quota_exceeded":
hold = None
else:
hold = 503
print("[maskbreak]", json.dumps({ # one line per check, whatever your log level
"mode": MODE, "decision": getattr(result, "decision", None),
"reasons": getattr(result, "reasons", None), "enforce_would": hold or "continue",
}), flush=True)
if MODE == "enforce" and hold == "verify":
return send_email_code(request.form["email"]) # your extra step
if MODE == "enforce" and hold:
abort(hold)
return existing_login() # watch mode, or allow: your normal login runs<?php
// composer require sentinelsup/sdk
$mode = getenv('MASKBREAK_MODE') ?: 'watch'; // watch: log only; enforce: act on it
$maskbreak = new \Sentinel\Client(getenv('MASKBREAK_API_KEY') ?: null);
$result = null;
$hold = null;
try {
$result = $maskbreak->evaluate([
'token' => $_POST['monocle'] ?? '',
'fingerprintEventId' => $_POST['sentinel_fp'] ?? '',
]);
if ($result->test) { $hold = 400; } // never on a live action
elseif ($result->decision === 'block') { $hold = 403; }
elseif ($result->decision === 'review' || !empty($result->raw['degraded'])) { $hold = 'verify'; }
} catch (\Sentinel\SentinelException $e) { // missing token, timeout or API error
// The month's checks are used up: let the login through, logged, until the 1st.
$monthUsedUp = $e->getStatus() === 429 && ($e->getBody()['code'] ?? null) === 'monthly_quota_exceeded';
$hold = $monthUsedUp ? null : 503;
}
error_log('[maskbreak] ' . json_encode([
'mode' => $mode, 'decision' => $result->decision ?? null,
'reasons' => $result->reasons ?? null, 'enforce_would' => $hold ?? 'continue',
]));
if ($mode === 'enforce' && $hold === 'verify') {
send_email_code($_POST['email']); // your extra step
exit;
}
if ($mode === 'enforce' && $hold) { http_response_code($hold); exit; }
// watch mode, or allow: your normal login continues hereEach example starts in watch mode: it logs what it would do and always lets the visitor through. Once the log matches Events in your dashboard, set MASKBREAK_MODE=enforce. In enforce mode a timeout or an error holds the login, never counts as a clean visit. The one exception is a used-up month (HTTP 429 with the code monthly_quota_exceeded): the examples log it and let the login through until the allowance resets on the 1st, in enforce mode too, so running out of checks never locks your users out.
With the public key sk_test_sandbox and no account, the token test_vpn answers review, test_clean allow, and test_proxy and test_tor block, so you can run each path before real traffic. The VPN detection API test cases guide turns them into a test for CI.
What it can’t see
- VPNs it doesn’t know yet
- A server that came online recently, or a service that sends traffic through home and mobile addresses, may not be known as an exit yet. The flags then stay false.
- The name
network.vpncan be true whilenetwork.serviceis null. Base your rules on the flags and treat the name as extra context.- A blocked script
- Ad blockers and hardened browsers can stop the script. The answer then says
degraded: true: ask for verification instead of counting the visit as clean.
VPN detection API questions
Is the VPN detection API free?
Yes, up to the Free plan: 10,000 visitor checks and 100,000 IP lookups a month, with no card. Paid plans start at €29 a month for 50,000 checks; the pricing page has the rest.
Can you detect a VPN from an IP address alone?
Not with Maskbreak. A bare IP lookup (/v1/lookup and the free IP lookup tool) checks only Tor exits and cloud-server ranges; a VPN verdict needs a live visit, the browser script’s token sent to /v1/evaluate. If all you have is addresses from old logs, the free VPN detection API comparison lists IP-based services.
Should I block visitors who use a VPN?
Not for the VPN alone: plenty of real customers use one at work or on hotel Wi-Fi. A VPN on its own comes back review, and the usual response is an extra step such as an email code; VPN detection without blocking real customers covers how.
How accurate is it?
We haven’t published a detection rate, because we haven’t run a controlled benchmark we’d stand behind. Test it on your own traffic, and count real customers who were flagged as well as VPNs that got through.
Check your signups and logins for VPNs
Setup starts in watch mode, so nobody is blocked until you turn blocking on.