Open source · In development · Apache-2.0

Sense every device.
Keep the decision yours.

WhiskerJS is an open-source browser collector for device signals: 46 signals in 7 groups, 14 integrity checks and two versioned identifiers, hashed in the page and handed to your server to judge. No network calls, no storage, no verdicts.

Try the live demoGet started
canvasaudiofontsscreenwebgllocalebotsmath

A few lines to start

Collect a result and send it with the request you already make. No extra endpoint, no third-party calls.

Not on npm yet: version 0.1.0 is built from source. The API may change before 1.0. Getting started

$ pnpm install && pnpm build   # in the WhiskerJS source folder
import { load } from 'whiskerjs'

const agent = await load()
const device = await agent.collect({ binding: { nonce, timestamp } })
await fetch('/login', { method: 'POST', body: JSON.stringify({ email, password, device }) })
46signals
7signal groups
14integrity checks
0network requests

Features · signalsVersion 5 · idVersion 2

What WhiskerJS does today

One call, one plain JSON result. What changed in each version.

46 signals in 7 groups

Graphics, audio, fonts, device, browser, locale and preferences, plus 9 volatile signals reported outside the ids. Each with a status and a reason code.

Read more

Noise-aware by design

Canvas and WebGL are drawn twice; a mismatch is reported as noisy instead of trusted. Brave farbling and Firefox resistFingerprinting signals are left out by an exclusion table.

Read more

14 integrity checks

Automation globals, webdriver, headless traits, engine mismatches, impossible hardware, and lie and Proxy detection against a clean, hidden iframe. Reported as flags, never as a verdict.

Read more

An id you can compare

visitorId for an exact match, and structuredId with one short hash per group, so "same device, new graphics driver" is visible instead of a new id.

Read more

Anti-replay binding

Pass your server's single-use nonce and timestamp; the result carries a digest over everything, so an old result cannot be replayed. Signing stays on your server.

Read more

An optional hook, your endpoint

onResult hands your code a copy of every result. Off by default. The library itself never sends anything.

Read more

How it works

WhiskerJS collects. Your server decides.

The library never scores, blocks or phones home. Every decision stays in your code. The pipeline in detail.

Step 1

Collect in the browser

46 signals run in parallel, each with one second and the whole call with a budget. Signals a browser is known to randomise are left out instead of trusted, and canvas and WebGL are drawn twice to catch noise.

Step 2

Hash, check and bind

Large values become SHA-256 digests in the page. 14 checks look for automation and tampering. The result is versioned and, if you pass your server's nonce, bound to it with a digest so old results cannot be replayed.

Step 3

Decide on your server

Send the result with a request you already make. Match visitorIds, compare structuredIds group by group, weigh the flags and set thresholds in your own code. Your rules stay private.

Inside a result

Built for browsers that fight back

Brave randomises some signals, Firefox's resistFingerprinting mode fakes others, and private windows can add canvas noise. WhiskerJS knows where, draws twice to catch the rest, and gives every signal a status and a reason code, so your server knows what it could not trust.

  • Noise-aware canvas and WebGL
  • Per-browser exclusion rules
  • 14 automation and tamper checks
  • Versioned visitorId and structuredId
  • Anti-replay binding to your nonce
  • Self-hosted, zero dependencies

All 46 signals · All 14 checks

result.json (shortened)
{
  "visitorId": "3feb3ead31e6b520459ad33ebc58f2d7",
  "signalsVersion": "5", "idVersion": "2",
  "components": {
    "canvas": { "status": "ok", "value": "fa6b47...43ec", "hashed": true },
    "device_memory": { "status": "unavailable", "ruleId": "no_api" },
    "time_zone": { "status": "ok", "value": "Europe/Berlin", "hashed": false }
  },
  "structuredId": "2_2ac06a6b0b67_6369...a0",
  "integrityFlags": [],
  "binding": { "nonce": "[your nonce]", "timestamp": 1767000000000, "digest": "9b16...7457" }
}

Live demo

See what it returns on your device

The demo runs the real WhiskerJS build (commit 1100e48, signalsVersion 5, idVersion 2) in your browser: the visitorId, the structuredId group by group, every component with its status, and the integrity flags. Run it again later to see whether your id holds, and if not, which group moved. Nothing is sent anywhere.

Open the live demoHow to read the results

  • visitorId: the same on every run means a stable setup.
  • structuredId: seven parts; a highlighted part is the group that changed.
  • Components: 46 rows; 32 hex characters are a digest, not raw data.
  • Lab mode: copy one line per run to measure your own devices.

Comparison

Feature by feature, with eleven other libraries

Which signals each open-source library collects at a named version, its licence, its own stability and uniqueness claims, and which ones send data home by default. The head-to-head measurement has not run yet, so the page makes no accuracy claim; it is coming.

See the comparison

Privacy

Small footprint, on purpose

Device signals can be personal data. The lawful basis and the notice belong to the site that uses WhiskerJS; the library keeps what it touches to a minimum. What is and is not collected, and a notice template.

No telemetry

No network requests of any kind: no usage sampling, no remote scripts.

No prompts, no WebRTC

No permission prompts. No WebRTC addresses, battery, location or media device labels.

Hashed in the browser

Images, audio, font lists and other large values leave only as SHA-256 digests.

Stores nothing

No cookies, localStorage or IndexedDB. Each call starts from nothing.

Standing on good shoulders

WhiskerJS learns from the open-source projects that came before it. Two small parts are adapted with their MIT notices kept (Math operations from FingerprintJS 5.3.0, media MIME lists from fpscanner 1.0.8); the rest is original code built from published ideas, credited in the library's third-party notices.

FingerprintJSThumbmarkJSBotDfpscannerBowserClientJS