Checks · Device fingerprint
Device fingerprint
Detect spoofed devices, incognito sessions, automation and impossible-travel velocity from a real browser fingerprint. Because it needs to run in the visitor's browser, the device check is done with an embedded scanner — not a server-side JSON call.
POST /api/v1/check/device returns
400 device_requires_browser — a fingerprint can only be collected from
the browser via the embed below.
- You add a one-tag scanner to the page you want to protect.
-
It collects a fingerprint and POSTs it to
/embed/collectwith yourX-Site-Key. - We charge a device check, analyze it in-process, and return the scored visit to the browser.
Integrate
Create a scanner site key and add your page origins to its allowlist in the Antifrauder
mini-app → Scanner. The scanner must be active, and the
calling page's Origin must be allow-listed (else
403 origin_not_allowed).
<script src="https://cdn.antifrauder.app/scripts/embed.min.js"></script>
<script>
// siteKey + secret come from the mini-app Scanner screen.
Antifrauder.init({ siteKey: 'sk_live_…', secret: '<base64 key>' });
// run a scan, e.g. on a login or checkout submit
Antifrauder.scan({ externalId: 'order_1042' })
.then(function (res) {
if (res.intel.device.verdict === 'fraudulent') blockSubmit();
sendToBackend(res.checkId); // your server can also re-fetch via API
})
.catch(function (err) {
console.warn('scan failed:', err.code); // insufficient_balance, origin_not_allowed, …
});
</script>
embed.js loads scanner.js from the same CDN, runs the
fingerprint, encrypts it with your scanner
secret (AES-256-GCM) and POSTs only the ciphertext — the raw fingerprint
never leaves the page in plaintext. scan() resolves with the analyzed
visit.
Collect endpoint
The endpoint the embed calls. You normally don't call it by hand — but this is the exact
contract. Note it lives at the root (not under /api/v1) and authenticates
with X-Site-Key, not your API key.
Headers
| Header | Required | Description |
|---|---|---|
X-Site-Key |
required |
Scanner site key (active). Bad/inactive →
401 invalid_site_key.
|
Content-Type |
required | application/json |
Origin |
required | Sent by the browser; must be in the site key's allowlist. |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
enc |
object | required |
The fingerprint, AES-256-GCM encrypted with your scanner
secret: { v, iv, data } (base64). The embed
builds this for you; plaintext is rejected with
400 invalid_scan.
|
extraSignals |
object | optional | Extra context to fold into the analysis. |
externalId |
string | optional | Your own correlation id (order, session, …). |
Response
The consolidated intel plus a fraud section (the same shape
as the other checks) — plus the transaction fields checkId,
charged and balance.
{
"checkId": "665f1a9c8e4b2a0012ab34cd",
"charged": 7,
"balance": 4769,
"intel": {
"device": { "id": "a1b2c3d4e5f6", "visitorId": "a1b2c3d4e5f6a7b8c9d0",
"confidence": "high", "verdict": "suspicious" },
"identity": {
"claimed": { "browser": "Safari 17", "os": "iOS 17" },
"real": { "browser": "Chrome 122", "os": "Windows 10" },
"flags": { "isOSMatched": false, "isBrowserMatched": false, "isTimezoneMatched": true }
},
"network": { "ip": { "address": "45.155.205.16" },
"location": { "city": "Amsterdam", "country": "NL" } },
"velocity": { "device": { "5m": 1, "1h": 2, "24h": 5 }, "ip": { "5m": 1, "1h": 3, "24h": 9 } },
"firstSeen": "2026-06-10T08:31:00.000Z",
"signals": { "isBot": false, "isDeviceSpoofed": true, "isLocationSpoofed": false, "isIncognito": true },
"meta": { "sources": { "ok": 1, "total": 1 } }
},
"fraud": {
"score": 62, "level": "high",
"negative": [
{ "code": "DV-R-26", "key": "real_os_vs_claimed_os_mismatch", "name": "Real OS vs Claimed OS Mismatch",
"description": "The detected OS contradicts the one claimed by the User-Agent.", "points": 15 }
],
"positive": []
}
}
Result fields
| Field | Type | Description |
|---|---|---|
intel.device |
object |
id (stable device id), visitorId,
confidence, and verdict (clean
· suspicious ·
fraudulent).
|
intel.identity |
object |
claimed vs real device attributes and the
per-attribute match flags (isOSMatched,
isTimezoneMatched, …) — divergence drives the risk signals.
|
intel.network |
object |
ip, location and webrtc of the visitor.
|
intel.velocity · intel.firstSeen
|
object · string |
Scan counts per device/IP over 5m/1h/24h; when first seen (null if
new).
|
intel.signals |
object |
Boolean summary flags: isBot, isDeviceSpoofed,
isLocationSpoofed, isIncognito, … .
|
fraud |
object |
{ score, level, negative[], positive[] } — each signal with a
stable code (DV-*), name,
description, points. Device signals are all risk
(negative).
|
Errors
All errors set the CORS header so the calling page can read them; the embed surfaces the
code on scan().catch(err => err.code).
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_scan |
Missing or malformed scan data. |
| 401 | invalid_site_key |
Unknown key or scanner not active. |
| 403 | origin_not_allowed |
Page origin not in the allowlist. |
| 402 | insufficient_balance |
Account balance too low for a device check. |
| 429 | rate_limited |
Per-site-key rate limit exceeded. |
| 502 | analysis_failed |
Engine error; the charge was refunded. |