Saltar al contenido principal

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', …)). The recipient_code is never sent to Percus over the network, never logged, and never written to S3 — only the resulting hash travels with events.
  • org_salt is a per-organization value delivered to the player in the embed manifest (see below). Because each organization has a different salt, the same recipient_code produces different hashes across organizations — hashes cannot be correlated cross-org.
  • organization_id is 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 new session_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_salt lives 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_id deterministically 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

ValueStored by Percus?
recipient_code (CRM id)Never — client-side hash input only
master_saltSecrets Manager only; never sent to clients
org_salt (derived)Delivered to the client in the manifest; not persisted with events
viewer_hashYes — anonymous, per-org, non-reversible without the salt
session_id / playback_idYes — anonymous