fpscanner is an MIT-licensed library, rewritten as a new 1.x line, that collects device signals and runs a broad set of bot detections, then returns a fingerprint in sections and a payload your server checks. WhiskerJS shares two of its ideas (an id in parts and a nonce against replay) and differs in the rest: what it reads, what it refuses to do (console calls, Workers), and how the result is protected. Neither returns a final verdict.
At a glance
| WhiskerJS 0.1.0 | fpscanner 1.0.8 | |
|---|---|---|
| Licence | Apache-2.0 | MIT |
| Version read | main at signalsVersion 5, idVersion 2 | tag v1.0.8, commit c701505 |
| Last release | not yet published | 2026-08-05 |
| Status | in development | maintained |
| Kind | collector: signals, ids and integrity flags, no verdict | collector with detections (a fastBotDetection flag) |
| Identifier | 32-hex visitorId plus structuredId (2_ and seven 12-hex parts) |
fsid in sections (FS1_<det>_<auto>_<dev>_<brw>_<gfx>_<cod>_<loc>_<ctx>) |
| Anti-replay | your nonce and timestamp, echoed and bound by a digest | time, nonce and page URL in the payload |
| Payload protection | digest (not a signature) | XOR obfuscation with a build-time key (“NOT cryptographically secure”) |
| Default network behaviour | none | none found |
| Install | build from source; npm package coming | npm package fpscanner |
How this was made: fpscanner’s public repository was read at v1.0.8 on 2026-10-24 (code read, never installed or run). Its own documentation is the reference if anything here differs.
Signals
Y collected for the identifier · b used only as a bot, lie or consistency check · o opt-in, or collected but outside the hash · - not collected.
| Signal | WhiskerJS 0.1.0 (signalsVersion 5) | fpscanner 1.0.8 |
|---|---|---|
| Canvas 2D image | Y (double render, tamper probe) | Y (+ tamper check) |
| WebGL vendor and renderer (unmasked) | Y | Y |
| WebGL parameters and extensions | Y | - |
| WebGL rendered image | Y (double render) | - |
| WebGPU adapter info | Y | Y |
| Audio (OfflineAudioContext) | Y | - |
| Audio base latency | o (Safari and Android only) | - |
| Fonts (how probed) | Y (54, span width and canvas measureText) | - |
| Font preferences (default font sizes) | Y | - |
| Screen size and colour depth | Y | Y |
| Screen frame or available area | o | Y |
| Time zone | Y | Y |
| Intl locale and calendars | Y | Y |
| Languages | Y | Y |
| User-Agent and platform | Y (platform; the UA string only for the class and checks) | Y |
| UA Client Hints (high entropy) | Y (versions outside the id) | Y |
| Hardware concurrency | Y | Y |
| Device memory | Y | Y |
| JS heap limit | Y | - |
| CPU architecture (NaN trick or CH) | Y (NaN) | Y (CH) |
| Touch support | Y | - |
| Plugins and mimeTypes | Y (+ consistency check) | Y |
| Math precision | Y | - |
| CSS media features | Y (5) | Y (9) |
| Storage availability | Y | - |
| Cookies enabled | Y | - |
| Media codecs | Y | Y |
| Speech voices | o | - |
| Permissions states | o (24) | - |
| Media device counts | - | Y |
| WebRTC | - | - |
| Network information | - | - |
| Battery | - | - |
| Keyboard layout | o | Y |
| Content blockers or extensions | o (8 bait elements) | Y (8 extensions) |
| Vendor-specific globals | Y | Y |
| Apple Pay, Private Click Measurement | Y | Y (ApplePaySetup) |
| PDF viewer | Y | - |
| Automation markers (webdriver, Selenium, Playwright, CDP, PhantomJS) | b (webdriver, 32 globals, headless; no CDP) | b (21 rules, CDP, Playwright) |
| Lie or tamper detection | b (native source, descriptors, clean iframe, Proxy) | b (descriptors, canvas tamper, context mismatch) |
| iframe and Worker cross-check | b (iframe; no Worker) | b (iframe and Worker) |
| Anti-replay (nonce, time) | Y | Y |
| Identifier format | 32-hex visitorId plus 7-part structuredId | structured fsid (JA4-like) |
In short: both read WebGL identity, WebGPU adapter details, media codecs, Client Hints, Intl, keyboard layout, screen and a canvas with a tamper check. fpscanner also reads media device counts, browser extensions and the Summarizer API, and cross-checks values in a Web Worker. WhiskerJS also reads audio, fonts (two methods), font preferences, Math, WebGL parameters and a WebGL image, permissions, speech voices and storage, and leaves out media devices by rule.
How they differ in approach
The id in parts. fpscanner’s fsid is, in its README’s words, “a JA4-inspired, locality-preserving fingerprint identifier”, with sections that include detections and automation. WhiskerJS’s structuredId covers only the 37 stable components, one 48-bit part per group (gpu, audio, fonts, device, browser, locale, prefs); its flags are a separate list and never change an id. See comparing two results.
Anti-replay and protection. fpscanner puts time and a nonce in an obfuscated payload and recommends rejecting payloads older than about 60 seconds; the obfuscation key is set at build time (the default build uses a placeholder). WhiskerJS takes a nonce and timestamp from your server, echoes them and computes a SHA-256 digest over the whole result; your server recomputes it with bindingInput. Both projects say plainly that this is not cryptographic protection: the digest stops replay and catches edits, it does not prove the browser told the truth. See Anti-replay binding.
Automation checks. Both check navigator.webdriver, a writable webdriver, Selenium and Playwright globals, impossible CPU or memory values, eval length against the engine, a software renderer and iframe mismatches. fpscanner also checks Chrome DevTools Protocol use, the 800x600 headless screen, a UTC time zone, language and platform mismatches and Worker mismatches. WhiskerJS also checks native function tampering and Proxy lies against a clean iframe realm, and plugin and mimeType consistency. It does not flag a UTC time zone, which real users in that zone also have.
What is left out on purpose. WhiskerJS has no console output, so no CDP detection; no Worker, because a blob: Worker can be blocked by a page CSP that then reports over the network; no media device counts, because that API is coupled to camera and microphone permissions. See the FAQ.
Statuses. WhiskerJS gives every component a status and reason code, so a missing value is explained rather than empty.
Their claims, in their words
README.md(1.0.8): “Fingerprints are representations, not verdicts.”README.md(1.0.8), Limits: “All client-side fingerprinting and bot detection techniques can be spoofed or emulated”.src/crypto-helpers.ts: “Simple and fast XOR-based encryption/decryption. Note: This is NOT cryptographically secure - use only for obfuscation”.- Stability and uniqueness figures: not published.
WhiskerJS publishes no accuracy, stability or uniqueness figure yet.
Code credits
WhiskerJS adapts one part from fpscanner 1.0.8 under its MIT licence: the audio and video MIME type strings used by media_codecs (the probing code is original), with the notice kept in the source file, NOTICE and THIRD_PARTY_NOTICES.md. WhiskerJS’s roadmap names fpscanner’s fsid and its timestamp-plus-nonce check as the ideas behind the structuredId and the binding; no code was taken for them.
When you might pick either
- You want detections such as CDP and Worker cross-checks run in the browser and accept a console call and a
blob:Worker: fpscanner includes them. - You need no console output, no Worker and no request of any kind from the library, with audio, fonts and Math signals as well: that is WhiskerJS’s design.
- You want an obfuscated payload by default: fpscanner; you want a plain JSON result with a verifiable digest: WhiskerJS. Neither replaces a server-side signature.
- Either way, measure on your own traffic.
No head-to-head measurement has run. This page compares features and designs. It does not say that either library is more accurate, more stable or more unique. A side-by-side measurement on the same devices is planned and will be published on the comparison page.
Questions and answers
Is the fpscanner fsid the same as the WhiskerJS structuredId?
They share an idea: an identifier made of sections so two results can be compared part by part. The sections differ. fpscanner's fsid covers detections, automation, device, browser, graphics, codecs, locale and contexts. WhiskerJS's structuredId has seven parts for gpu, audio, fonts, device, browser, locale and prefs, computed only from its 37 stable components.
Does either library sign the result?
No. fpscanner obfuscates its payload with a build-time XOR key and says itself that this is not cryptographically secure. WhiskerJS computes a digest over the result and your nonce, and says itself that this is not a signature. Signing belongs on your server.
Why does WhiskerJS leave out CDP detection and the Worker cross-check?
CDP detection works by logging an object to the console, and WhiskerJS writes nothing to the console. A Worker needs a blob: script that a page Content Security Policy may block and report over the network, which would be a request caused by the library.