Identity Verification

When your page says "this visitor is jane@acme.com", Simple Commenter needs proof that the claim came from you and not from someone typing in the browser console. The proof is userHash: an HMAC signature of the user's identity, computed with a secret only your backend knows.

This is the same pattern used by Intercom and similar embedded products. If you have set up identity verification anywhere before, this will look familiar.

The contract

  • With an externalId: userHash = HMAC_SHA256(secret, externalId + ":" + email)
  • Without one: userHash = HMAC_SHA256(secret, email)

Hex-encoded, lowercase. The strings must exactly match what the page sends, including case: sign the same values you put in the user object.

The signature covers the email on purpose. A signature over only a user id would let anyone who obtained one valid pair claim any email address they like. Simple Commenter rejects hashes that do not bind the email.

Where the secret lives

Project settings, Developers. Each project has its own secret. Store it like any other API credential: environment variable or secrets manager, never in client-side code, never in your repository.

Backend examples

Node.js

const crypto = require("crypto");

const userHash = crypto
  .createHmac("sha256", process.env.SC_PROJECT_SECRET)
  .update(`${user.id}:${user.email}`)
  .digest("hex");

PHP

$userHash = hash_hmac(
  'sha256',
  $user->id . ':' . $user->email,
  getenv('SC_PROJECT_SECRET')
);

Python

import hashlib, hmac, os

user_hash = hmac.new(
    os.environ["SC_PROJECT_SECRET"].encode(),
    f"{user.id}:{user.email}".encode(),
    hashlib.sha256,
).hexdigest()

Ruby

require "openssl"

user_hash = OpenSSL::HMAC.hexdigest(
  "sha256",
  ENV["SC_PROJECT_SECRET"],
  "#{user.id}:#{user.email}"
)

Render the result into your page (a template variable, a JSON endpoint your SPA calls after login, a meta tag: anything works) and pass it as userHash.

What happens on our side

A verified identify creates or reuses a client on your project, visible in Project settings, Clients with the channel "JS API". If the email matches one of your workspace members or the account owner, they keep their real role instead. When an externalId is present, it is the stable key: if that user later changes their email in your app, the same client record follows them.

Rotating the secret

Rotate from the Developers page. The old secret keeps verifying for 24 hours so you can deploy the new one without logging anyone out mid-session. Users already identified stay logged in through a rotation either way; rotation only affects new identify calls.

Testing without a backend (unverified mode)

For local development you can flip on Allow unverified identities on the Developers page. The widget then accepts user without a userHash.

Warning: Unverified mode means anyone can claim any email on that project. Use it on staging projects only, and turn it off before real users touch the site.

Troubleshooting

| Symptom | Likely cause | | --- | --- | | 401 from /api/js/identify | Hash mismatch: wrong secret, wrong field order, or email case differs between hash and user object | | 403 "JS API not enabled" | Enable it on the Developers page for this project | | 403 "pending approval" | The client exists but is not approved; approve them under Clients | | Works locally, fails in production | Different project (and secret) per environment; check which public key the script tag uses | | Widget never appears | The project is login-protected and no boot/identify happened, or the account is on the free plan |

Was this page helpful?