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
- Add the collector and a hidden field to the page with your form. The script fills the field with the browser signals by itself.
- When the form is sent, your server passes the field, the visitor's IP address and request headers to
POST /v1/assesswith your key. - 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
| Field | Type | |
|---|---|---|
blob | string | Required. The value from sl.js. |
ip | string | Required. The visitor's IP address (IPv4 or IPv6). |
headers | object | The 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_ref | string | Optional. Your own ID for the account (any string up to 200 characters, we store only its hash). Enables seen. |
proxy_headers | array | Optional. 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"
} score | 100 means nothing contradicts. Each finding lowers it: high by 30, medium by 12, low by 4; privacy and info by 0. |
verdict | honest (85 and above), contradictions (60 to 84), spoofed (below 60), automation (a controlled or headless browser, whatever the score). |
reasons | Findings, strongest first. severity: high, medium, low, privacy (Brave, Tor Browser, Firefox resistFingerprinting: protection, not a lie), info. |
device.id | A 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. |
seen | Only 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. |
edge | Whether 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).
| Code | Meaning |
|---|---|
audio_noise | Noise in the audio fingerprint |
canvas_noise | Noise in canvas drawing |
ch_header | Client Hints header contradicts JavaScript |
dns_leak | DNS requests bypass the VPN |
exotic_hw | Impossible device values |
headless_outer | Zero-size window |
headless_ua | Headless browser |
hosting_ip | Data center or VPN IP |
iframe_mismatch | An embedded frame sees a different browser |
lang_header | Different language in the header and in JavaScript |
lang_nav | Two different primary languages |
mobile_no_touch | A phone without a touch screen |
no_chrome_obj | Chrome without the chrome object |
os_fonts | Fonts from another system |
os_gpu | Graphics card from another system |
platform_ua | Platform does not match the User-Agent |
privacy_brave | Brave: fingerprint protection |
privacy_rfp | Firefox: fingerprint resistance mode |
privacy_torbrowser | Tor Browser |
proxy_headers | Proxy headers |
rtc_local_ip | WebRTC exposes a local IP |
soft_gpu | Software graphics |
tamper | Tampered built-in functions |
tls_library | TLS handshake not from a browser |
tls_ua | TLS handshake from another browser |
tor_exit | IP belongs to the Tor network |
tz_intl_date | Two different time zones |
tz_ip | Time zone does not match the IP country |
ua_header | Different User-Agent in the header and in JavaScript |
uad_mobile | Mobile or not: conflicting answers |
uad_platform | Client Hints and the User-Agent name different systems |
uad_version | Two different browser versions |
webdriver | The browser is controlled by a program |
webgpu_webgl | WebGPU and WebGL see different graphics cards |
webrtc_ip | WebRTC shows another public IP address |
window_screen | Window larger than the screen |
worker_mismatch | A background thread sees a different browser |
Errors and limits
401 unauthorized | No key, a wrong key or a revoked key. |
429 rate_limited | Too many requests a second from one account: 5 without a package, 10 with 20,000 a month, 20 with 100,000. |
429 quota_exceeded | The 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_json | The request is malformed. |
500 server | Our 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
- You decide what happens with your visitors' data; we process it on your behalf. Mention fraud prevention in your privacy policy.
- We do not store the signals, the result or the full IP address. With
user_refwe keep a hash of the device and a hash of the account reference for 30 days, nothing else. - Device IDs are keyed per client: the same person gets unrelated IDs on different sites.
- sl.js talks only to
edge.spooflens.comand*.l.spooflens.com(the DNS test). If your site has a Content Security Policy, allowscript-src https://spooflens.comandconnect-src https://edge.spooflens.com https://*.l.spooflens.com. - The API must not be used to track people across sites or to get around other services' fraud protection; keys used that way are revoked.
Terms of use (beta)
- The API is provided as is during the beta. We aim for high availability but do not guarantee it, so never make our answer the only gate: on errors and timeouts, let the user through.
- Without a package: 1,000 checks a month per account (all keys together), up to 3 keys. Paid packages are listed on the pricing page. Limits may change; changes are announced on this page.
- You need a lawful basis to process your visitors' data and should mention fraud prevention in your privacy policy. You decide about the processing; we process the data on your behalf as described in our privacy policy.
- Not allowed: tracking or profiling people across sites, getting around other services' fraud protection, reselling raw access to the API, load testing without asking us. Keys used this way are revoked.
- Within v1 the response only grows: new fields may appear, existing ones keep their meaning. Breaking changes would go to /v2.
- Questions: hello@spooflens.com.
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/assessanswers400 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
seenand 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.