VPN detection API

VPN detection API that names the VPN when it’s known

Maskbreak’s Free plan includes 10,000 visitor checks a month with no card; paid plans start at €29 a month. 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.

One check on the live visit, one decision to route on

A VPN detection API tells your backend whether a visitor is coming through a VPN, a proxy or Tor, so you can decide what happens before a signup, login or payment goes through. Maskbreak checks the live visit, not only the address: a script in the page collects a short-lived token, and your server swaps it for a verdict using your secret key. Each check returns:

  • decision: allow, review or block. A VPN on its own comes back review. Proxies, Tor exits and automated or tampered browsers come back block.
  • network: separate flags for vpn, proxy, tor, datacenter (a cloud server), anonymous and residential.
  • network.service: the VPN or proxy service, such as PROTON_VPN, when it’s known, and null when it isn’t.
  • reasons: short codes such as vpn_detected and datacenter_asn that you can log and show to whoever reviews the case.
  • risk_score: 0 to 100. It’s a signal score for sorting and thresholds, not the probability that someone is a fraudster.
  • device: fake-browser and automation signals, present when the device check completes.
  • evaluated_in_ms: how long the check took on our server.

An IP address on its own won’t tell you it’s a VPN

Many VPN exits sit in the same hosting ranges as ordinary servers, and the addresses change. So Maskbreak keeps the two inputs apart. A live visit gets the full network and device check. A bare IP address gets only the checks an address can support.

What a live visit check and an IP lookup can each detect
 Live visit: POST /v1/evaluateIP lookup: GET /v1/lookup/{ip}
What you sendThe browser token from our script, plus your keyAn IP address, plus your key
VPNYes, with the service name when it’s knownNo
Residential proxyYes, when the address is known as a proxy exitNo
Tor exitYesYes
Cloud server rangeYesYes, with the cloud provider
Fake browser, automationWhen the device check completesNo, there is no browser to check
What comes backdecision: allow, review or blockverdict and risk_score; known: false means nothing was found, not that the address is safe
Use it forSignup, login, checkout, password resetServer logs, allowlists, webhooks, batch screening

The free IP lookup tool runs the address-only checks in your browser, without a key. If all you have is addresses from old logs and you need VPN flags on them, an IP-based service is a better fit. Our free VPN detection API comparison lists several and what each one needs as input.

What a VPN visit looks like in the response

This is the public sample for a VPN visit. You can fetch it yourself without an account:

curl 'https://maskbreak.com/v1/evaluate/sample?scenario=vpn'

These are the fields you route on. The full response also carries evaluated_in_ms and the older status, isSuspicious and details fields for existing integrations.

{
  "sample": true,
  "decision": "review",
  "risk_score": 65,
  "ip": "198.51.100.18",
  "country": "NL",
  "network": {
    "vpn": true,
    "proxy": false,
    "datacenter": true,
    "anonymous": true,
    "tor": false,
    "residential": false,
    "service": "PROTON_VPN"
  },
  "reasons": ["vpn_detected", "datacenter_asn"]
}

The decision is review, not block, because a VPN on its own doesn’t prove fraud. network.vpn is true and network.service names the service. datacenter is also true because this exit sits in a hosting range, and both findings appear in reasons. The address is reserved for documentation, and the sample’s evaluated_in_ms is a fixed value, not a measurement.

Add it in three steps

The check sits in front of your existing handler. Your own sign-in, permission checks and rate limits keep running; Maskbreak only adds a verdict before them.

1Load the script on the page with the form

Add the script to the page and the class monocle-enriched to the form. The script adds three hidden fields: monocle (the network token), sentinel_fp (the device check) and sentinel_tz (the browser’s time zone).

<script async src="https://maskbreak.com/assets/sentinel.js"></script>

<form class="monocle-enriched" method="POST" action="/login">
  <!-- your existing fields stay as they are -->
</form>

2Send the token to your server

A normal form submit already carries those fields. If you submit with fetch, collect them first and send them along with the rest of the form. The token goes to your server, never straight to Maskbreak from the browser, because the call needs your secret key.

const { token, fingerprintEventId, tz } = await window.Sentinel.collect();

await fetch('/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email, password, token, fingerprintEventId, tz })
});

3Ask for a verdict before the action runs

Call /v1/evaluate from your backend and route on decision. Each example starts in watch mode: it logs what it would do and always lets the visitor through. After a few days, compare that log with Events in the dashboard, then set MASKBREAK_MODE=enforce. The Python and PHP examples use the official SDKs. The Node example calls the API directly so it can also pass on the time zone, which the SDKs don’t send yet.

Node.js
// 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);
Python
# 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
<?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 here

Treat a timeout, an error or degraded: true (there is no network verdict, usually because the browser script was blocked) as missing evidence, never 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. Every field is listed in the /v1/evaluate reference. To see what browser JavaScript can and can’t find out on its own, such as time zone and WebRTC checks, read the JavaScript VPN detection guide.

What to do with a VPN verdict

Plenty of real customers use a VPN: at work, on hotel Wi-Fi, or out of habit. That’s why the default for a VPN on its own is review, and why review should mean an extra step the customer can finish, not a dead end.

Default decision and a suggested response for each finding
What the check foundDefaultA sensible response
A VPN on its ownreviewAsk for an email code or a one-time passcode, then let them in. Hold payouts or large orders for a person to check.
A VPN plus a fake browser or automationblockStop the action. The browser signals carry this verdict, not the VPN.
A proxy, including residentialblockStop it, or send it to manual review if each signup is worth a lot to you.
A Tor exitblockStop it, or allow read-only access if Tor users matter to your product.
A cloud server on its ownallowLet it through and log the datacenter_asn reason. Office gateways and hosted browsers look like this.
Nothing flaggedallowCarry on as normal.

You can change any default per signal under Rules in the dashboard, for example allowing VPNs if many of your customers use one, or blocking them if your product pays out money. Rules apply to your whole account; to treat a newsletter form differently from payouts, route on decision differently in each handler. A rule changes the answer only for the signal it names, so with VPNs allowed, a VPN visit that also shows a proxy or a fake browser is still blocked. When a rule changes the answer, the response keeps the engine’s own verdict in engine_decision. Rules apply to test tokens too, so one command in a terminal shows whether a rule works before real visitors reach it.

Don’t block on a VPN alone. VPN detection without blocking real customers covers review flows and how to measure the friction they add.

What it can’t see, and what we don’t claim

Residential VPNs and proxies. Some services send traffic through home broadband and mobile addresses. If an address isn’t known as an exit when the visit happens, the network flags stay false. The device check sometimes still spots the automation behind it, but not always.

Brand-new servers. A VPN server that came online recently may not be known yet. You may see only the cloud-server flag, or nothing at all, and network.service will be null.

The name isn’t always known. network.vpn can be true while network.service is null. Base your rules on the flags and treat the name as extra context.

A blocked script means no network verdict. Ad blockers and hardened browsers can stop the script from running. The response then says degraded: true. Ask for verification in that case instead of counting the visit as clean.

No accuracy percentage. We haven’t published a detection rate, because we haven’t run a controlled benchmark we’d stand behind. Test on your own traffic, and count real customers who were flagged as well as VPNs that got through.

Test it before real traffic reaches it

You can run every decision path without an account, then again on your own key before any real visitor reaches it. The VPN detection API test cases guide turns the steps below into a contract test you can run in CI.

curl -X POST https://maskbreak.com/v1/evaluate \
  -H 'Authorization: Bearer sk_test_sandbox' \
  -H 'Content-Type: application/json' \
  -d '{"token":"test_vpn"}'
Test tokens and the decision each one returns
Test tokenDecisionWhat it proves
test_cleanallowYour normal path still runs
test_vpnreviewReview pauses for your extra step
test_datacenterallowA cloud-server flag doesn’t block on its own
test_proxyblockBlock never reaches your handler
test_torblockThe same, for a Tor exit

The public key sk_test_sandbox answers only these tokens, needs no signup and is rate-limited per address. With an account, the same tokens work on your live key and never reach your webhook, and your personal sk_test_ key runs the full live pipeline with its events marked as tests. All of these responses carry test: true, which is why the handler above refuses them in enforce mode. These are checks on your code, not a measure of how well detection works.

To see a real verdict, turn your own VPN on and off and run the live check on the homepage. It shows the result for your own visit.

VPN detection API questions

Which service flags VPN, Tor and datacenter traffic?

Maskbreak flags all three with one API call. On a live visit, POST /v1/evaluate reports network.vpn (with the VPN service's name when it is known), network.tor for a listed Tor exit and network.datacenter for a cloud or hosting address, and answers allow, review or block: a VPN alone is review, a Tor exit is block, and a datacenter address on its own is context, not a block. A bare IP lookup (GET /v1/lookup) checks only the Tor exit list and published cloud ranges, so it cannot tell a VPN from a home connection. The Free plan includes 10,000 visitor checks a month with no card; paid plans start at €29 a month.

Is the VPN detection API free?

Yes, up to the Free plan’s allowance: 10,000 visitor checks a month and 100,000 IP lookups a month, and you don’t need a card. Paid plans raise the allowance, from €29 a month for 50,000 checks; larger volumes are on Enterprise. The pricing page has the current terms.

Can you detect a VPN from an IP address alone?

Not with Maskbreak. The IP lookup (/v1/lookup and the free IP lookup tool) checks for Tor exits and cloud-server ranges. VPN and residential proxy verdicts need a live visit: the browser script’s token, sent to /v1/evaluate.

Which VPN services can it name?

When the service is known, network.service carries an identifier such as PROTON_VPN, and proxy networks get one too, such as BRIGHT_DATA. We don’t promise a name on every visit: the field is null when the service isn’t known, even if network.vpn is true.

Should I block visitors who use a VPN?

Not for the VPN alone. A VPN on its own comes back review, and the usual response is an extra step such as an email code. Proxies, Tor exits and automated or tampered browsers come back block. You can change any of these 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.

Does it detect residential proxies?

On a live visit, yes, when the address is known as a proxy exit. A proxy comes back block by default, with the network named when it’s known. A residential proxy that isn’t known yet can look like an ordinary home connection, which is why the browser is checked as well. See proxy detection for more.

How fast is a check?

Every /v1/evaluate response carries evaluated_in_ms, the time the check took on our server. The browser script needs about one to two seconds after the page loads to produce its token, usually while the visitor is still filling in the form, and the trip between your server and ours adds to that. Measure the full time on your own setup.

How accurate is it?

We haven’t published an accuracy or detection-rate figure, because we haven’t run a controlled benchmark we’d stand behind. The test tokens prove your code handles each decision; they don’t measure detection. Try it with a few of your own connections, and count real customers who were flagged as well as VPNs that got through.

Do I need to set anything up per country?

No. There’s no country list to configure: each visit is checked by how it connects and what its browser shows. How much is known about a given address still varies, so look at your own results market by market.

One check before every signup, login and checkout

Free plan: 10,000 visitor checks a month, no card. Paid plans from €29 a month.

Stop fraud before it hides — try Maskbreak free. Free plan: 10,000 checks a month, no card.