Open source · In development · Apache-2.0

Sense every device.
Keep the decision yours.

WhiskerJS is an open-source browser collector for device signals: 47 signals in 7 groups, 19 integrity checks and two versioned identifiers, hashed in the page and handed to your server to judge. No storage, no verdicts, and the result goes only where you send it.

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 }) })
47signals
7signal groups
19integrity checks
0values or ids sent by the library

Features · signalsVersion 11 · idVersion 2

What WhiskerJS does today

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

47 signals in 7 groups

Graphics, audio, fonts, device, browser, locale and preferences, plus 10 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, and up to twice more when the first two differ; canvas, WebGL and audio are checked against known answers; noise is reported as noisy instead of trusted. Signals that Brave, Firefox and Safari fingerprinting protection, resistFingerprinting and Tor randomise are left out by an exclusion table.

Read more

19 integrity checks

Automation globals, webdriver, headless traits, engine, user agent, time zone and screen mismatches, impossible hardware, virtual machines, 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 never sends the result itself; its only request is a sampled diagnostics report of statuses, which one setting turns off.

Read more

How it works

WhiskerJS collects. Your server decides.

The library never scores or blocks, and never sends the result anywhere. Every decision stays in your code. The pipeline in detail.

Step 1

Collect in the browser

47 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, WebGL and audio are checked twice and against known answers to catch noise.

Step 2

Hash, check and bind

Large values become SHA-256 digests in the page. 19 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 and Safari fingerprinting protection add noise, resistFingerprinting and Tor fake others, and private windows can add canvas noise. WhiskerJS knows where, draws twice (and again when the two differ) and checks known answers 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
  • 19 automation and tamper checks
  • Versioned visitorId and structuredId
  • Anti-replay binding to your nonce
  • Self-hosted, zero dependencies

All 47 signals · All 19 checks

result.json (shortened)
{
  "visitorId": "97b14fef4feb8caae6190337f45fe6f5",
  "signalsVersion": "9", "idVersion": "2",
  "components": {
    "canvas": { "status": "ok", "value": "e6e7e5...8ade", "hashed": true },
    "device_memory": { "status": "unavailable", "ruleId": "no_api" },
    "time_zone": { "status": "ok", "value": "Europe/Berlin", "hashed": false }
  },
  "structuredId": "2_ee36f3ff7067_6369...a0",
  "previousVisitorId": "3feb3ead31e6b520459ad33ebc58f2d7",
  "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 44441d3, signalsVersion 11, idVersion 2) in your browser: the visitorId, the structuredId group by group, every component with its status, the integrity flags and, after a library update, the id under the previous signal version for linking. Run it again later to see whether your id holds, and if not, which group moved. The demo turns the diagnostics report off, so 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

Explore

Everything on this site

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.

Diagnostics: statuses only, easy to turn off

By default about 1 in 100 page loads send the WhiskerJS project one report of component statuses, never values or ids. diagnostics: false, a meta tag, Do Not Track or Global Privacy Control turns it off. What is sent.

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