# Identifiers

> visitorId for exact matches, structuredId for similarity, and what makes them change.

Source: https://whiskerjs.com/docs/identifiers/ (last updated 2026-10-26)

> **In plain words:**
>
> The visitorId is one code made from all the stable signals: if two visits give the same code, the browser setup is the same. The structuredId is the same idea split into seven short codes, one per group of signals. When only one of the seven changes (say, graphics after a driver update), you can still see that it is very likely the same device, and you can see what changed.

Every result carries two identifiers derived from the same 37 stable components:

- **visitorId**: one 128-bit hash. Two results either match or they do not.
- **structuredId**: seven short hashes, one per signal group. Two results can match in some groups and differ in others, which tells you *what* changed.

Neither is stored by the library, and neither is a person: they describe a browser setup. Two identical devices with identical settings can share an id, and one person with two browsers has two.

## visitorId

Plainly: put every stable component on its own line, in alphabetical order, and hash the lot. Same environment, same id.

Exactly (idVersion 2):

```text
whiskerjs/id/2
apple_pay=v:none
audio=h:0a4d8e691d7e59afff0d9a1a9d405e3b
browser_class=v:chromium
canvas=!
...
webgpu=h:862da19ca084b185c1d8c76af676a08b
```

- the first line is `whiskerjs/id/<idVersion>`;
- then one line per stable component in alphabetical order: `name=v:value` for a plain value, `name=h:digest` for a hashed one, and `name=!` for every status other than `ok`;
- lines are joined with `\n`, UTF-8 encoded, hashed with SHA-256, and the first 128 bits are returned as 32 lowercase hex characters.

Because every non-ok status gives the same `!`, a component that is `blocked` in one run and `timed_out` in the next does not change the id. A component that is `ok` in one run and not in the next does.

It is `null` when `crypto.subtle` is missing or fails (for example on a page that is not a secure context).

## structuredId

Format:

```text
2_2ac06a6b0b67_63695d5858ec_9c9b9c3f5a2e_5e78835d0b60_77ab287db153_8ad7c1a2e06b_4880096d1ea0
| gpu          audio        fonts        device       browser      locale       prefs
idVersion
```

`<idVersion>_<gpu>_<audio>_<fonts>_<device>_<browser>_<locale>_<prefs>`. Each part is the first 48 bits (12 hex characters) of SHA-256 over the line `whiskerjs/id/2/<group>` followed by the group’s components in alphabetical order, one line each in the visitorId encoding above.

| Group | Components |
| --- | --- |
| `gpu` | canvas, webgl_image, webgl_params, webgl_renderer, webgl_vendor, webgpu |
| `audio` | audio |
| `fonts` | font_preferences, fonts, fonts_canvas |
| `device` | color_depth, cpu_architecture, device_memory, hardware_concurrency, heap_limit, screen_resolution, touch_support |
| `browser` | apple_pay, browser_class, math, media_codecs, pdf_viewer, platform, plugins, private_click, storage, ua_client_hints, vendor, vendor_flavors |
| `locale` | intl_locale, languages, time_zone |
| `prefs` | color_gamut, dynamic_range, forced_colors, prefers_contrast, reduced_motion |

The groups and their order are exported as `ID_GROUPS` and `ID_GROUP_ORDER`, the part length as `ID_PART_LENGTH` (12). The structuredId is `null` without a digest.

## Comparing two results

WhiskerJS only derives the parts. Deciding what counts as “the same device” is your server’s job. A simple, explainable approach:

```ts
import { ID_GROUP_ORDER } from 'whiskerjs';

/** Which groups match between two structuredIds of the same idVersion. */
function compareStructured(a: string, b: string): Record<string, boolean> | null {
  const pa = a.split('_');
  const pb = b.split('_');
  if (pa[0] !== pb[0]) return null;            // different idVersion: not comparable
  const out: Record<string, boolean> = {};
  ID_GROUP_ORDER.forEach((group, i) => {
    out[group] = pa[i + 1] === pb[i + 1];
  });
  return out;
}

// { gpu: false, audio: true, fonts: true, device: true, browser: true, locale: true, prefs: true }
// reads as: "same device and settings, the graphics changed" (for example a driver update)
```

Typical patterns, as hints and not rules:

| What differs | A likely cause |
| --- | --- |
| only `gpu` | GPU driver or browser graphics update, hardware acceleration switched |
| only `browser` | browser update (Client Hints brands, Math results, codecs) |
| only `locale` | the user changed language or time zone, or travelled |
| only `prefs` | an accessibility or display setting changed |
| only `device` | a different screen or a changed CPU or memory report |
| `fonts` | fonts installed or removed, a font setting changed |
| most groups | a different device, or a different browser on the same device |

When matching, weigh groups by how much they tell apart and how often they change on your own traffic. The components inside each group are in the result, so you can also compare them one by one.

## What changes the id

The visitorId changes when any stable component’s value or status (ok or not ok) changes. Common causes:

- browser or operating system updates (`math`, `media_codecs`, `ua_client_hints`, graphics);
- GPU driver updates, or turning hardware acceleration on or off;
- installing or removing fonts;
- changing language, time zone or accessibility settings;
- a different screen as the primary display;
- a private window or an extension that adds noise to canvas or WebGL (the component turns `noisy`);
- a component timing out on a slow or busy device in one run but not in another.

What does **not** change it: the pixel ratio (which follows page zoom), the taskbar or dock (`screen_frame`), permission grants, the keyboard layout, ad blockers, voices loading late, the audio output latency and exact browser version numbers (`ua_versions`). Those components are outside the id.

## Why versions bump

- **signalsVersion** goes up when a collector changes what it measures, an integrity check changes, or the exclusion table changes. Adding a stable component changes the visitorId of nearly every device; changing a check changes no id but changes which flags you see.
- **idVersion** goes up when the derivation itself changes: the line encoding, the prefix, the groups. The prefix contains the idVersion, so a new idVersion gives every device a new id.

Treat a visitorId as comparable only with ids of the same `idVersion` **and** `signalsVersion`. Across versions, re-link devices through something you already trust (a signed-in session, for example) and store the new id next to the old one. [What’s new](https://whiskerjs.com/whats-new/) lists what each version changed.
