Integration guide
One script tag scores every visitor. One server-side call decides what to do about it.
1. Register the property
Add a site key to api/config.php. One key per property.
$BS_SITES = array(
'eNHF2jKHdRQPvyJZ' => 'browserscan.in',
'k7Qm2xR9pLvW' => 'yourshop.com',
);
Remove the 'demo' and '*' entries before going live.
A wildcard key lets anyone write telemetry into your dashboard.
2. Install the tag
Put this before </body> on every page you want scored.
<script src="https://browserscan.in/t.js" data-key="YOUR_SITE_KEY" async></script>
Attributes
| data-key | Required. Identifies the property. |
|---|---|
| data-manual | Do not scan automatically. Call BrowserScan.scan() yourself, for example only on the checkout page. |
| data-callback | Name of a global function to receive the verdict. |
| data-no-stun | Skip STUN discovery. You lose the browser-versus-network address comparison, which is the strongest proxy signal available. |
| data-endpoint | Override the API origin if you are self-hosting. |
Reading the verdict in the page
BrowserScan.ready(function (result) {
console.log(result.score); // 0-100, higher is riskier
console.log(result.verdict); // human | likely-human | suspicious |
// likely-automated | automated | verified-bot
console.log(result.flags); // { bot, headless, stealth, spoofed, proxy, ... }
console.log(result.fingerprint) // stable device id
console.log(result.session); // pass this to your backend
});
A browserscan:result event fires on window with the same payload in event.detail.
3. Verify server-side
Never trust the verdict the browser reports. Anything JavaScript hands you can be rewritten by whoever controls that browser. Send the session id to your backend and ask our server what it recorded.
Send the session id with your form, then verify it before you act:
GET https://browserscan.in/api/verify.php?key=YOUR_SITE_KEY&session=SESSION_ID
{
"ok": true,
"found": true,
"session": "a1b2c3...",
"visitor": "9f3e1c...",
"seen_at": "2026-08-08T11:42:07+00:00",
"age_seconds": 12,
"stale": false,
"score": 88,
"verdict": "automated",
"allow": false,
"flags": {
"bot": true, "headless": true, "stealth": false,
"spoofed": true, "proxy": true, "datacenter": true,
"verified_bot": false
},
"fingerprint": "6d21f0ab9c74e3d5",
"device": { "browser": "Chrome 126", "os": "Windows 10/11", "type": "desktop" },
"network": { "country": "DE", "rdns": "ec2-...compute.amazonaws.com", "kind": "datacenter" },
"reasons": [ { "id": "webdriver", "weight": 90, "text": "...", "source": "client" } ]
}
PHP
function browserscan_check($session) {
$url = 'https://browserscan.in/api/verify.php?key=' . BS_KEY
. '&session=' . urlencode($session);
$raw = @file_get_contents($url);
$r = $raw ? json_decode($raw, true) : null;
// Fail open: if the check is unavailable, do not lock out real customers.
if (!$r || empty($r['found'])) { return array('allow' => true, 'score' => null); }
// A verdict older than half an hour describes a different visit.
if (!empty($r['stale'])) { return array('allow' => true, 'score' => $r['score']); }
return $r;
}
$check = browserscan_check($_POST['bs_session']);
if ($check['score'] !== null && $check['score'] >= 80) {
// Automated. Reject silently rather than explaining why.
http_response_code(204);
exit;
}
if ($check['score'] !== null && $check['score'] >= 55) {
require_captcha();
}
Node
const check = await fetch(
`https://browserscan.in/api/verify.php?key=${KEY}&session=${session}`
).then(r => r.json());
if (check.found && !check.stale && !check.allow) {
return res.status(403).json({ error: 'blocked' });
}
4. Choose your thresholds
| 0–17 | human Let it through. |
|---|---|
| 18–37 | likely human Let it through. Log it if you are tuning. |
| 38–59 | suspicious Challenge: CAPTCHA, email confirmation, step-up auth. |
| 60–79 | likely automated Challenge hard, or refuse anything irreversible. |
| 80–100 | automated Refuse. |
| verified-bot | crawler A search engine confirmed by reverse DNS. Serve it normally. |
Start by logging only. Watch a week of real traffic on the dashboard, find where your genuine customers actually sit, and set the threshold above them. A fraud filter that blocks paying customers costs more than the fraud does.
API reference
POST /api/collect.php
Telemetry ingest. Called by the tag; you should not need to call it yourself. Requires a valid site key in the body. Returns the fused verdict.
GET /api/verify.php
Server-to-server verdict lookup by session or visitor. Requires the site key.
GET /api/ip.php
Public. Address intelligence for the caller, or for ?ip=. Returns location, timezone, network operator and proxy/hosting reputation.
curl https://browserscan.in/api/ip.php?ip=8.8.8.8
GET /api/stats.php
Dashboard feed. Requires the admin key in an X-BS-Admin header.
| ?do=summary | Aggregates for &window= seconds. |
|---|---|
| ?do=live | Recent hits, newest first. &since= takes a row id. |
| ?do=detail&id= | Every signal recorded for one hit, plus related activity. |
| ?do=clusters | Rotating proxy pools and device farms. |
Self-hosting
The whole system is flat files plus SQLite. Copy public_html to any
Apache host with PHP 7.2 or newer, set your keys in api/config.php, and
point the tag at your own domain with data-endpoint.
The datastore is created automatically, preferring a directory above the web root.
If pdo_sqlite is missing it falls back to append-only JSON, which keeps
ingest working but disables the velocity and clustering analysis.
Performance and privacy
- The tag is one request, no dependencies, and runs after
DOMContentLoaded. - Collection is budgeted at three seconds and every probe has its own timeout, so a slow API cannot hang the page.
- Every call is wrapped so the tag cannot throw into your page.
- Addresses are stored alongside a salted hash. Set
BS_STORE_RAW_IPto false to keep hashes only. - Telemetry is pruned after
BS_RETENTION_DAYS, thirty days by default. - If you operate in the EU or UK, fingerprinting for fraud prevention still needs a lawful basis and a mention in your privacy notice. Say what you collect and why.