Identity Model
Percus analytics never stores viewer PII. Viewers are identified by an
anonymous, per-organization hash computed in the browser, plus a
per-load session id. The host page's recipient identifier
(recipient_code — typically a CRM id) is only ever a hash input and never
leaves the page.
viewer_hash
viewer_hash = SHA-256( organization_id : recipient_code : org_salt ) // lowercase hex
- Computed client-side, in the embed SDK, via the Web Crypto API
(
crypto.subtle.digest('SHA-256', …)). Therecipient_codeis never sent to Percus over the network, never logged, and never written to S3 — only the resulting hash travels with events. org_saltis a per-organization value delivered to the player in the embed manifest (see below). Because each organization has a different salt, the samerecipient_codeproduces different hashes across organizations — hashes cannot be correlated cross-org.organization_idis part of the input and is independently bound on the server side from the signed share token at ingest, so a tampered client cannot attribute events to another organization.
session_id and playback_id
session_id— a UUID v4 generated once per iframe load. Refreshing the iframe yields a newsession_id.playback_id— an optional integer that increments per replay within a session (1 on the first playback, 2 on the next, …). It lets a single viewing session contain multiple distinct playbacks.
Per-organization salt: derivation and rotation
org_salt is derived, not stored per organization:
org_salt = HMAC-SHA256( master_salt, organization_id )
- A single high-entropy
master_saltlives in AWS Secrets Manager. The campaign service reads it once at startup and derives each organization's salt on demand when resolving the embed manifest. The SDK only ever receives the derived per-org salt — the master never leaves the server. - This is zero-touch: onboarding a new organization needs no salt
provisioning step; any
organization_iddeterministically maps to a salt.
Annual hard rotation
Rotate the single master_salt annually. Because every organization's salt
is derived from it, rotation is a hard rotation: all derived salts change at
once, so every viewer's viewer_hash changes.
- Cohort analysis does not span a rotation. A viewer seen before and after a rotation appears as two different hashes; cross-rotation cohort continuity is intentionally not preserved.
- This trade-off is accepted in exchange for a simpler, stronger-privacy model: one secret to manage, no long-lived per-viewer identifiers, and no way to re-link viewers across a rotation boundary.
To rotate: generate a new high-entropy value for the master_salt secret and
redeploy/refresh the campaign service so it picks up the new value. No data
migration is required — historical events keep their old hashes; new events use
the new ones.
What Percus stores vs. never sees
| Value | Stored by Percus? |
|---|---|
recipient_code (CRM id) | Never — client-side hash input only |
master_salt | Secrets Manager only; never sent to clients |
org_salt (derived) | Delivered to the client in the manifest; not persisted with events |
viewer_hash | Yes — anonymous, per-org, non-reversible without the salt |
session_id / playback_id | Yes — anonymous |