Signed Identity

Verify logged-in users via HMAC-SHA256 to prevent impersonation and link conversations to real accounts.

Why Signed Identity?

Without identity verification, anyone could pass arbitrary user data to the widget — an attacker could impersonate your users by setting a fake id or email. Signed identity solves this by requiring a server-generated HMAC hash that proves the identity data hasn't been tampered with.

When signed identity is configured, conversations are permanently linked to your user accounts. Agents see verified user information in the inbox, and returning users see their full conversation history across devices and sessions.

How It Works

  1. Your backend computes an HMAC-SHA256 hash of the user's id using your workspace's Identity Secret Key.
  2. You pass the id, optional metadata (name, email), and the hash to the widget via the identity object.
  3. When the widget connects, it sends the identity object to the VerifyIdentity endpoint.
  4. The server recomputes the hash using the same secret key. If it matches, the identity is accepted. If not, the user is treated as anonymous.

Computing the HMAC Hash

The hash is an HMAC-SHA256 digest of the user's id (as a UTF-8 string), keyed with your workspace's Identity Secret Key. The output should be hex-encoded (lowercase).

🚨The Identity Secret Key is different from your public site key. It starts with zt_isk_ and must never be exposed to the browser. Find it in Settings → API Keys → Identity Secret.

Node.js

Node.js (server-side)
import crypto from "node:crypto";

function computeIdentityHash(userId: string, identitySecret: string): string {
  return crypto
    .createHmac("sha256", identitySecret)
    .update(userId, "utf8")
    .digest("hex");
}

// Usage
const hash = computeIdentityHash(user.id, process.env.ZAPTICKET_IDENTITY_SECRET);
// Pass this hash to your frontend alongside user.id

C# / .NET

.NET (server-side)
using System.Security.Cryptography;
using System.Text;

public static string ComputeIdentityHash(string userId, string identitySecret)
{
    var key = Encoding.UTF8.GetBytes(identitySecret);
    var message = Encoding.UTF8.GetBytes(userId);

    using var hmac = new HMACSHA256(key);
    var hash = hmac.ComputeHash(message);
    return Convert.ToHexString(hash).ToLowerInvariant();
}

Python

Python (server-side)
import hmac
import hashlib

def compute_identity_hash(user_id: str, identity_secret: str) -> str:
    return hmac.new(
        identity_secret.encode("utf-8"),
        user_id.encode("utf-8"),
        hashlib.sha256
    ).hexdigest()

Ruby

Ruby (server-side)
require 'openssl'

def compute_identity_hash(user_id, identity_secret)
  OpenSSL::HMAC.hexdigest('sha256', identity_secret, user_id)
end

PHP

PHP (server-side)
function computeIdentityHash(string $userId, string $identitySecret): string {
    return hash_hmac('sha256', $userId, $identitySecret);
}

Passing Identity to the Widget

Once you've computed the hash on your server, pass it to the client-side embed configuration:

Embed with identity
<script>
  window.ZapTicket = {
    siteKey: "zt_pub_abc123",
    identity: {
      id: "user_12345",         // Your internal user ID
      name: "Jane Doe",         // Optional: display name
      email: "[email protected]",   // Optional: email for agents
      hash: "a1b2c3d4e5f6..."   // HMAC-SHA256 hex digest
    }
  };
</script>
<script async src="https://cdn.zapticket.app/widget.js"></script>

The VerifyIdentity Endpoint

When the widget opens a connection with an identity object, it calls the following endpoint to validate the hash before establishing a verified session:

Endpoint
POST /widget/verify-identity
Content-Type: application/json

{
  "siteKey": "zt_pub_abc123",
  "id": "user_12345",
  "name": "Jane Doe",
  "email": "[email protected]",
  "hash": "a1b2c3d4e5f6..."
}

The server performs the following steps:

  1. Looks up the workspace by siteKey.
  2. Retrieves the workspace's Identity Secret Key.
  3. Computes HMAC-SHA256(identitySecret, id) and compares it to the provided hash.
  4. If the hashes match, returns a session token linked to the verified identity.
  5. If the hashes don't match, the identity is rejected.

What Happens on Invalid Hash

If the hash verification fails, the widget does not show an error to the visitor. Instead, it silently falls back to anonymous mode:

  • The conversation is created without a linked user identity.
  • Agents see the visitor as "Anonymous Visitor" with a generated session ID.
  • The conversation is not linked to any previous history for that user.
  • A warning is logged to the browser console: ZapTicket: identity verification failed, continuing as anonymous.
⚠️Common causes of hash mismatch: using the wrong secret key (site key vs. identity secret), encoding issues (ensure UTF-8), or passing a different value as id than what was used to compute the hash.

Security Considerations

  • The Identity Secret Key must only exist on your server. Never bundle it in client-side code, environment variables exposed to the browser, or mobile app bundles.
  • The hash is computed only over the id field. The name and email are informational and not verified cryptographically — they can be updated freely.
  • Rotate your Identity Secret Key from Settings → API Keys if you suspect it's compromised. Existing sessions remain valid until they expire.
  • Identity verification is optional. If you don't set the identity object, all visitors are treated as anonymous with session-based tracking.

Testing Identity Verification

To verify your implementation is working:

  1. Set the identity object with a valid hash.
  2. Open the widget and send a message.
  3. In the dashboard inbox, the conversation should show the user's name and email instead of "Anonymous Visitor".
  4. To test failure: change one character in the hash. The widget should fall back to anonymous mode (check the console for the warning).