# WhiskerJS and fpscanner

> Both return an identifier in parts and support a nonce against replay. They differ in what they check, how they protect the payload and what they leave out. Compared from a code reading of fpscanner 1.0.8.

Source: https://whiskerjs.com/compare/fpscanner/ (last updated 2026-10-26)

> **In plain words:**
>
> 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 9, 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 9) | fpscanner 1.0.8 |
| --- | --- | --- |
| Canvas 2D image | Y (double render, known-answer probe) | Y (+ tamper check) |
| WebGL vendor and renderer (unmasked) | Y | Y |
| WebGL parameters and extensions | Y | - |
| WebGL rendered image | Y (double render, known-answer probe) | - |
| 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, Playwright binding, headless; no CDP) | b (21 rules, CDP, Playwright) |
| Lie or tamper detection | b (native source, descriptors, clean iframe, Proxy, cross-API consistency) | 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 (previous ids after a bump) | 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](https://whiskerjs.com/docs/identifiers/#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](https://whiskerjs.com/docs/integration/#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](https://whiskerjs.com/docs/how-it-works/#the-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](https://whiskerjs.com/docs/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`](https://whiskerjs.com/signals/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](https://whiskerjs.com/compare/).

## 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.

## Related pages

- [structuredId](https://whiskerjs.com/docs/identifiers/#structuredid): The seven parts and how to compare them.
- [Anti-replay binding](https://whiskerjs.com/docs/integration/#anti-replay-binding): Nonce, timestamp and digest, and what they do not prove.
- [media_codecs](https://whiskerjs.com/signals/media-codecs/): The MIME lists adapted from fpscanner.
- [WhiskerJS and BotD](https://whiskerjs.com/compare/botd/): Automation checks in a classifier.
- [Feature comparison matrix](https://whiskerjs.com/compare/): All twelve libraries on one table.
