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 |