Account

SpoofLens API: a risk score for sign-ups, checkouts and promo codes

Ask one question when a form is sent: does this visitor look like an honest person in a normal browser? The answer is a score from 0 to 100, a verdict, the reasons in plain words, the network (VPN, data center, Tor, proxy) and a device ID that shows how many of your accounts were created on the same device. It is the same engine as the browser check: about 30 consistency rules plus the TLS handshake, DNS and WebRTC seen by our own server.

Get a free key 1,000 checks a month free, no card.

Quick start

  1. Add the collector and a hidden field to the page with your form. The script fills the field with the browser signals by itself.
  2. When the form is sent, your server passes the field, the visitor's IP address and request headers to POST /v1/assess with your key.
  3. Decide what to do with the answer: let the user in, ask for extra verification or block.
<script src="https://spooflens.com/v1/sl.js" data-field="sl_blob" async></script>
<input type="hidden" name="sl_blob">

If you build the request yourself, call SpoofLens.collect(): it resolves to the same string.

PHP

$res = file_get_contents('https://spooflens.com/v1/assess', false, stream_context_create(['http' => [
    'method'  => 'POST',
    'header'  => "Authorization: Bearer " . getenv('SPOOFLENS_KEY') . "\r\nContent-Type: application/json\r\n",
    'content' => json_encode([
        'blob' => $_POST['sl_blob'] ?? '',
        'ip' => $_SERVER['REMOTE_ADDR'],
        'headers' => [
            'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
            'accept_language' => $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '',
            'sec_ch_ua' => $_SERVER['HTTP_SEC_CH_UA'] ?? '',
            'sec_ch_ua_platform' => $_SERVER['HTTP_SEC_CH_UA_PLATFORM'] ?? '',
            'sec_ch_ua_mobile' => $_SERVER['HTTP_SEC_CH_UA_MOBILE'] ?? '',
        ],
        'user_ref' => (string)$userId, // optional: enables "seen"
    ]),
    'timeout' => 5,
]]));
$risk = json_decode($res, true);
if (($risk['verdict'] ?? '') === 'automation' || ($risk['score'] ?? 100) < 40) {
    // block or ask for verification
}

Node.js

const r = await fetch('https://spooflens.com/v1/assess', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.SPOOFLENS_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    blob: req.body.sl_blob,
    ip: req.ip,
    headers: {
      user_agent: req.get('user-agent') || '',
      accept_language: req.get('accept-language') || '',
      sec_ch_ua: req.get('sec-ch-ua') || '',
      sec_ch_ua_platform: req.get('sec-ch-ua-platform') || '',
      sec_ch_ua_mobile: req.get('sec-ch-ua-mobile') || '',
    },
    user_ref: String(user.id),
  }),
  signal: AbortSignal.timeout(5000),
});
const risk = await r.json();

Python

import os, requests

risk = requests.post("https://spooflens.com/v1/assess", timeout=5,
    headers={"Authorization": f"Bearer {os.environ['SPOOFLENS_KEY']}"},
    json={
        "blob": request.form.get("sl_blob", ""),
        "ip": request.remote_addr,
        "headers": {
            "user_agent": request.headers.get("User-Agent", ""),
            "accept_language": request.headers.get("Accept-Language", ""),
            "sec_ch_ua": request.headers.get("Sec-CH-UA", ""),
            "sec_ch_ua_platform": request.headers.get("Sec-CH-UA-Platform", ""),
            "sec_ch_ua_mobile": request.headers.get("Sec-CH-UA-Mobile", ""),
        },
        "user_ref": str(user.id),
    }).json()

Behind a proxy or load balancer, send the visitor's real IP (for example from X-Forwarded-For set by your own proxy), not the proxy's.

WordPress and WooCommerce

No code needed: the plugin adds the collector to registration and checkout, keeps the score with each user and order, shows a Risk column in the Users and Orders lists and a note on each order. It works with the WordPress registration form, WooCommerce My Account registration and both the classic and the block checkout, and supports High-Performance Order Storage. Blocking is off by default: you can block automated browsers and scores below a threshold. If our API cannot be reached, the order goes through and is marked "not checked".

Download the plugin (0.1.0, beta) WordPress 6.0+, PHP 7.4+, WooCommerce 7.0+ optional. Install: Plugins, Add New, Upload Plugin; then Settings, SpoofLens, paste your key.

Authentication

Create a key in your account and send it as Authorization: Bearer sl_live_… (or X-API-Key). Keep it on your server: never put it into page code. A key can be revoked at any time. Paid plans through RapidAPI are coming; they use RapidAPI's own key instead.

Machine-readable description for tools and code generators: OpenAPI 3 (openapi.json).

POST /v1/assess

FieldType
blobstringRequired. The value from sl.js.
ipstringRequired. The visitor's IP address (IPv4 or IPv6).
headersobjectThe visitor's user_agent, accept_language, sec_ch_ua, sec_ch_ua_platform, sec_ch_ua_mobile. Without them the header checks are skipped.
user_refstringOptional. Your own ID for the account (any string up to 200 characters, we store only its hash). Enables seen.
proxy_headersarrayOptional. Names (not values) of forwarding headers your server received, such as Via.

Response:

{
  "ok": true,
  "score": 70,
  "verdict": "contradictions",
  "reasons": [
    {"code": "tls_ua", "severity": "high",
     "title": "TLS handshake from another browser",
     "text": "The User-Agent names Firefox, but the TLS handshake the server sees is typical of Chrome / Chromium. ..."}
  ],
  "device": {"id": "d_3f9c0a71b2e4d85c6a1f0e93", "browser": "Firefox", "os": "Windows",
             "tls": "chromium", "ja4": "t13d1516h2_8daaf6152771_e2d80978ab2e"},
  "network": {"ip": "203.0.113.42", "asn": 64500, "org": "Example Broadband", "country": "RO",
              "type": "regular", "tor": false, "dns_leak": false, "webrtc_ip_mismatch": false},
  "seen": {"accounts_on_device_30d": 3},
  "edge": true,
  "request_id": "r_5c2e8f1a9b0d3e47",
  "ts": "2026-10-21T21:30:00+00:00"
}
score100 means nothing contradicts. Each finding lowers it: high by 30, medium by 12, low by 4; privacy and info by 0.
verdicthonest (85 and above), contradictions (60 to 84), spoofed (below 60), automation (a controlled or headless browser, whatever the score).
reasonsFindings, strongest first. severity: high, medium, low, privacy (Brave, Tor Browser, Firefox resistFingerprinting: protection, not a lie), info.
device.idA hash of stable traits (canvas, WebGL, audio, fonts, screen, hardware) keyed with your own salt. Stable across visits of the same browser profile; different on every other client's site, so it cannot be used to follow people between sites.
seenOnly with user_ref: how many of your accounts used this device in the last 30 days, the current one included. Several accounts on one device is the classic sign of promo and trial abuse.
edgeWhether the TLS, DNS and WebRTC checks of our server reached the visitor. If not (blocked, slow network), the other checks still run.

Suggested use: automation or a score below 40: block or require extra verification; 40 to 84: allow, but watch (limit promo codes, delay payouts); seen above 2 on sign-up or trial: treat as the same person. Tune the thresholds on your own traffic.

POST /v1/check

The same as /v1/assess, but takes signals (the raw JSON object from the open-source spooflens-collector) instead of blob.

GET /v1/ip/{ip}

Network facts about one address, no browser needed.

curl https://spooflens.com/v1/ip/8.8.8.8 -H "Authorization: Bearer sl_live_…"

{"ok": true, "ip": "8.8.8.8", "version": 4, "ptr": "dns.google", "asn": 15169, "org": "GOOGLE",
 "country": "US", "type": "hosting", "tor": false}

Reason codes

The code values you can receive. The severity of a few of them depends on context (for example, canvas noise is privacy in Brave and high elsewhere).

CodeMeaning
audio_noiseNoise in the audio fingerprint
canvas_noiseNoise in canvas drawing
ch_headerClient Hints header contradicts JavaScript
dns_leakDNS requests bypass the VPN
exotic_hwImpossible device values
headless_outerZero-size window
headless_uaHeadless browser
hosting_ipData center or VPN IP
iframe_mismatchAn embedded frame sees a different browser
lang_headerDifferent language in the header and in JavaScript
lang_navTwo different primary languages
mobile_no_touchA phone without a touch screen
no_chrome_objChrome without the chrome object
os_fontsFonts from another system
os_gpuGraphics card from another system
platform_uaPlatform does not match the User-Agent
privacy_braveBrave: fingerprint protection
privacy_rfpFirefox: fingerprint resistance mode
privacy_torbrowserTor Browser
proxy_headersProxy headers
rtc_local_ipWebRTC exposes a local IP
soft_gpuSoftware graphics
tamperTampered built-in functions
tls_libraryTLS handshake not from a browser
tls_uaTLS handshake from another browser
tor_exitIP belongs to the Tor network
tz_intl_dateTwo different time zones
tz_ipTime zone does not match the IP country
ua_headerDifferent User-Agent in the header and in JavaScript
uad_mobileMobile or not: conflicting answers
uad_platformClient Hints and the User-Agent name different systems
uad_versionTwo different browser versions
webdriverThe browser is controlled by a program
webgpu_webglWebGPU and WebGL see different graphics cards
webrtc_ipWebRTC shows another public IP address
window_screenWindow larger than the screen
worker_mismatchA background thread sees a different browser

Errors and limits

401 unauthorizedNo key, a wrong key or a revoked key.
429 rate_limitedToo many requests a second from one account: 5 without a package, 10 with 20,000 a month, 20 with 100,000.
429 quota_exceededThe monthly limit of the account is used up (1,000 without a package, all keys together). It resets on the 1st, UTC. Bigger packages: pricing.
400 bad_blob, bad_signals, bad_ip, bad_jsonThe request is malformed.
500 serverOur side failed. Let the user in and retry later; never block on our errors.

Typical latency is 50 to 300 ms. Use a timeout of a few seconds and treat a timeout as "unknown".

Privacy and your site

Terms of use (beta)

FAQ

Does SpoofLens block anyone by itself?
No. It returns a score and reasons; your code decides.
What if an ad blocker stops sl.js?
The field stays empty and /v1/assess answers 400 bad_blob. Treat that as "unknown", not as fraud: many honest people use blockers.
Can a good antidetect browser pass?
Sometimes. The checks look for contradictions; a carefully built profile hides most of them. Combine the score with seen and your own signals.
Is there a WordPress / WooCommerce plugin?
Yes, in beta: download spooflens-0.1.0.zip, then Plugins, Add New, Upload Plugin. See WordPress and WooCommerce above. The WordPress.org directory listing comes later.