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-keyRequired. Identifies the property.
data-manualDo not scan automatically. Call BrowserScan.scan() yourself, for example only on the checkout page.
data-callbackName of a global function to receive the verdict.
data-no-stunSkip STUN discovery. You lose the browser-versus-network address comparison, which is the strongest proxy signal available.
data-endpointOverride 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–17human Let it through.
18–37likely human Let it through. Log it if you are tuning.
38–59suspicious Challenge: CAPTCHA, email confirmation, step-up auth.
60–79likely automated Challenge hard, or refuse anything irreversible.
80–100automated Refuse.
verified-botcrawler 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=summaryAggregates for &window= seconds.
?do=liveRecent hits, newest first. &since= takes a row id.
?do=detail&id=Every signal recorded for one hit, plus related activity.
?do=clustersRotating 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