Skip to content

Identity Verification (HMAC) ​

Priya logs into Acme Diagnostics, opens the chat widget, and asks about her recent test results. Without identity verification, anyone who knows Priya's customer ID could type it into the browser console and impersonate her. HMAC verification closes that gap — your server signs the identity so Perfox can trust it, not just accept it.

This page is for developers integrating the widget on a site where your users are logged in. You'll come away with a working signing implementation and an understanding of how Perfox uses the signature on every request. If you haven't embedded the widget yet, start with the window.Perfox SDK first.

Why signing matters ​

Anyone can run Perfox('identify', { external_id: 'someone_else' }) from a browser console — the widget has no way to know whether the call came from your app or from a curious visitor. Without a signature, that claim is self_asserted, and unless you require verification Perfox takes it at face value: it resolves to that customer and can resume their open conversation. To reach verified, you compute a user_hash on your server and pass it with the identity. Because the computation requires your site secret — which never leaves your server — the browser receives only the resulting hex digest, which is useless to forge.

What gets signed ​

The canonical string is:

<site_id>.<external_id>

You join your Site key and the visitor's external_id with a dot, then HMAC-SHA256 that string using your site secret (sa_secret_live_…) as the key. The hex digest is passed as user_hash in identify.

You get the site secret once, when you click Generate Credentials (or Rotate) on the Install tab of your agent's Web Chat trigger — store it in your server's secret manager straight away. The Studio snippet's Server (Node.js) tab shows the same signing helper with your Site key filled in.

For example, with site sa_site_live_EW607… and external_id cust_abc123, you sign:

sa_site_live_EW607….cust_abc123

The visitor's browser never sees the site secret — only the resulting hex digest.

Server-side signing — code samples ​

Node.js / TypeScript ​

ts
import { createHmac } from 'node:crypto';

const SITE_ID     = process.env.PERFOX_SITE_ID!;     // sa_site_live_…
const SITE_SECRET = process.env.PERFOX_SITE_SECRET!; // sa_secret_live_…

export function compute_user_hash(external_id: string): string {
  return createHmac('sha256', SITE_SECRET)
    .update(`${SITE_ID}.${external_id}`)
    .digest('hex');
}

Python ​

python
import hmac, hashlib, os

SITE_ID     = os.environ["PERFOX_SITE_ID"]
SITE_SECRET = os.environ["PERFOX_SITE_SECRET"].encode()

def compute_user_hash(external_id: str) -> str:
    canonical = f"{SITE_ID}.{external_id}".encode()
    return hmac.new(SITE_SECRET, canonical, hashlib.sha256).hexdigest()

Go ​

go
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "os"
)

func ComputeUserHash(externalID string) string {
    h := hmac.New(sha256.New, []byte(os.Getenv("PERFOX_SITE_SECRET")))
    h.Write([]byte(os.Getenv("PERFOX_SITE_ID") + "." + externalID))
    return hex.EncodeToString(h.Sum(nil))
}

Ruby ​

ruby
require 'openssl'

def compute_user_hash(external_id)
  OpenSSL::HMAC.hexdigest(
    'sha256',
    ENV['PERFOX_SITE_SECRET'],
    "#{ENV['PERFOX_SITE_ID']}.#{external_id}"
  )
end

PHP ​

php
function compute_user_hash(string $external_id): string {
    return hash_hmac(
        'sha256',
        getenv('PERFOX_SITE_ID') . '.' . $external_id,
        getenv('PERFOX_SITE_SECRET')
    );
}

Wiring the hash into the widget ​

Ship the computed hash to the page alongside your user record, then pass it to identify:

js
fetch('/api/me')
  .then(r => r.json())
  .then(me => {
    Perfox('identify', {
      external_id: me.id,
      name:        me.name,
      email:       me.email,
      user_hash:   me.perfox_user_hash, // computed server-side
    });
  });

user_hash is one of the user context fields. On every request that carries an identity (/init, /send, voice, dial-out, uploads), Perfox checks it against each verifying site secret; on a match the request is verified, and /init reports it back as auth_level.

Only external_id is covered by the signature. phone and email travel as unverified hints, so on a site that relies on verification, identify signed-in visitors by external_id plus user_hash.

Worked example — Priya opens the Acme Diagnostics widget ​

Setup: Asha, an admin at Acme Diagnostics, has turned on Require HMAC identity verification on the Install tab and stored PERFOX_SITE_ID and PERFOX_SITE_SECRET as environment variables in her backend.

Action: Priya logs in. The Acme Diagnostics server calls compute_user_hash('cust_priya_7') and includes the result in the /api/me response as perfox_user_hash. The browser receives this value and calls:

js
Perfox('identify', {
  external_id: 'cust_priya_7',
  name:        'Priya Sharma',
  email:       'priya@example.com',
  user_hash:   '3a9f…c12e', // the hex digest returned by the server
});

Result: Perfox recomputes the HMAC on its side using the stored site secret. The digests match, so Priya's session is verified: her external_id is accepted as genuine, the widget resumes her own conversation and can list her past ones, and a visitor who merely types cust_priya_7 into the console is turned away.

What just happened: the secret never left Acme's server. The browser only carried the digest — a value that can't be reversed to find the key or forged to create a different identity.

Turning verification on ​

On the Install tab, tick Require HMAC identity verification (you can also tick it while generating credentials). Anonymous visitors are always allowed; what changes is how a claimed identity is treated. Turn it on whenever your agent handles per-customer data: your Site key ships in the page source, so without it any page holding the key can claim to be one of your signed-in customers.

The identity + origin gate ​

Every request passes a site check, an origin allowlist check (case-insensitive exact match; a missing Origin is rejected), and then resolves the identity path:

  • No external_id → anonymous.
  • external_id + valid user_hash → verified.
  • external_id + a user_hash that matches no verifying secret → rejected.
  • external_id without user_hash → self_asserted, unless Require HMAC identity verification is on, in which case the request is rejected.

Verification error codes ​

HTTPError codeCause
400missing_site_idX-Perfox-Site header absent.
404unknown_siteNo site with that key exists (for example, its credentials were deleted).
403site_suspendedEmbedding is suspended for the site (Suspend embedding on the Install tab).
403origin_not_allowedRequest Origin isn't in the site's allowed origins.
403identity_verification_requiredVerification is required and external_id was sent without user_hash.
403identity_verification_faileduser_hash didn't match any active or expiring secret.

Most identity_verification_failed cases are a server signing with the wrong secret — often a mix-up between a _test_ and a _live_ credential, or a secret that has already been revoked.

Zero-downtime secret rotation ​

Site secrets are HMAC keys, so a leak compromises every future verification. Rotate without downtime from the Secrets table on the Install tab — click Rotate and pick a grace window:

  1. A new active secret is minted; the raw value is shown once — store it immediately.
  2. The old secret flips to expiring, set to expire once the grace window ends.
  3. Both active and expiring secrets verify inbound user_hash values during the grace window.
  4. Update your server to sign with the new secret.
  5. When the grace window ends, expiring secrets are automatically flipped to revoked and stop verifying.

The grace window defaults to 24 hours; the choices are 5 minutes (emergency rotation), 1 hour, 4 hours, 24 hours, 3 days or 7 days.

For an immediate revoke, use Revoke now on a secret's row — but the last verifying secret cannot be revoked (rotate first).


You can now embed verified identity into every widget session. Here's where to go next:

  • User Context & Identity — see the full set of fields you can pass alongside user_hash, and how they shape the conversation.
  • window.Perfox SDK — the complete widget API reference, including identify, open, close, and event listeners.
  • Site Management — the site object behind your embed credentials: allowed origins, secrets and the verification flag.