- Docs
- Developers
Identity verification
Prove that the user your page says is logged in really is that user, so nobody can open the messenger as one of your customers and read their conversations.
How it works
When you identify a logged-in user to the messenger with a user_id or an email, that value comes from the browser, and anything in the browser can be edited. Identity verification adds a signature that only your server can produce:
user_hashis the HMAC-SHA256 of the user'suser_id(or of theiremailwhen you don't send auser_id), keyed with your workspace secret, written as lowercase hex.- Your server computes it and prints it into the page next to the user's details.
- TalkingDot recomputes the hash with the same secret. If they match, the contact is marked as verified (
"verified": truein the REST API).
Never put the workspace secret in browser code. Anyone who has it can sign any user ID and sign in as any of your users. Keep it in your server's configuration (for example an environment variable), compute the hash on the server and send only the hash to the page.
Set it up
- Copy your workspace secret. Open Settings → Security and copy the identity verification secret. Store it on your server, for example as the
TALKINGDOT_SECRETenvironment variable. - Compute the hash on your server. For each logged-in user, sign their
user_id(or their email if you don't have an ID) with the secret. Examples for each language are below. - Pass it to the messenger. Add
user_hashnext touser_idandemailinwindow.TalkingDotSettings, or in yourTalkingDot('boot')orTalkingDot('update')call. - Check it works. Log in to your site as a test user and open the page. The browser console shows a warning if the hash was rejected, and the contact's
verifiedfield istrueonce it was accepted. - Require it. Turn on Require identity verification in Settings → Security. From then on, identify calls without a valid hash are rejected.
Compute the hash
Sign exactly the value you pass as user_id, as a string. If you identify users by email only, sign the email instead, trimmed and in lowercase. Use the secret exactly as it's shown in Settings → Security: it's a text key, so don't hex-decode it.
PHP
<?php
function talkingdot_user_hash(string $value): string
{
$secret = (string) getenv('TALKINGDOT_SECRET'); // Settings → Security
return hash_hmac('sha256', $value, $secret);
}
$userHash = talkingdot_user_hash((string) $user->id);
// or, with no user ID: talkingdot_user_hash(strtolower(trim($user->email)))Node.js
const crypto = require('crypto');
function talkingdotUserHash(value) {
return crypto
.createHmac('sha256', process.env.TALKINGDOT_SECRET) // Settings → Security
.update(String(value))
.digest('hex');
}
const userHash = talkingdotUserHash(user.id);
// or, with no user ID: talkingdotUserHash(user.email.trim().toLowerCase())Python
import hashlib
import hmac
import os
def talkingdot_user_hash(value) -> str:
secret = os.environ["TALKINGDOT_SECRET"].encode() # Settings → Security
return hmac.new(secret, str(value).encode(), hashlib.sha256).hexdigest()
user_hash = talkingdot_user_hash(user.id)
# or, with no user ID: talkingdot_user_hash(user.email.strip().lower())Ruby
require "openssl"
def talkingdot_user_hash(value)
secret = ENV.fetch("TALKINGDOT_SECRET") # Settings → Security
OpenSSL::HMAC.hexdigest("SHA256", secret, value.to_s)
end
user_hash = talkingdot_user_hash(user.id)
# or, with no user ID: talkingdot_user_hash(user.email.strip.downcase)Check your code with known values
With the sample secret 3f8a1c0b9d2e4f6a, every example above must produce these hashes. (Your real secret is different and longer.)
| Signed value | Expected user_hash |
|---|---|
42 | 61cb1b052b30852a660424f8e2a1c4f90996a934e09cf1aafeb174b5bae47efe |
ada@example.com | 652d0e0439eb2bb5c664437cb035c9561fe969c91e217562adcb0f580d9e87e7 |
You can also compute a hash from the command line to compare with what your app prints:
printf '%s' '42' | openssl dgst -sha256 -hmac 'YOUR_WORKSPACE_SECRET'Pass the hash to the messenger
Print the hash into the page together with the user's details. In a server-rendered page, set window.TalkingDotSettings before the messenger script:
<?php
$secret = getenv('TALKINGDOT_SECRET');
$userId = (string) $user->id;
$chatSettings = [
'key' => 'WORKSPACE_KEY',
'user_id' => $userId,
'email' => $user->email,
'name' => $user->name,
'user_hash' => hash_hmac('sha256', $userId, $secret),
];
?>
<script>
window.TalkingDotSettings = <?= json_encode($chatSettings, JSON_HEX_TAG | JSON_HEX_AMP | JSON_UNESCAPED_SLASHES) ?>;
</script>
<script src="https://talkingdot.com/widget.js" data-key="WORKSPACE_KEY" async></script>The page the browser receives looks like this:
<script>
window.TalkingDotSettings = {
key: 'WORKSPACE_KEY',
user_id: '42',
email: 'ada@example.com',
name: 'Ada Lovelace',
user_hash: '61cb1b052b30852a660424f8e2a1c4f90996a934e09cf1aafeb174b5bae47efe'
};
</script>
<script src="https://talkingdot.com/widget.js" data-key="WORKSPACE_KEY" async></script>With TalkingDot('boot') or TalkingDot('update')
If you load the messenger with the async loader, you can pass the same keys to boot instead. Queued straight after the loader, it runs before the messenger starts:
<script>
(function(w,d){w.TalkingDot=w.TalkingDot||function(){(w.TalkingDot.q=w.TalkingDot.q||[]).push(arguments)};var s=d.createElement('script');s.async=1;s.src='https://talkingdot.com/widget.js';s.setAttribute('data-key','WORKSPACE_KEY');d.head.appendChild(s)})(window,document);
TalkingDot('boot', {
key: 'WORKSPACE_KEY',
user_id: '42',
email: 'ada@example.com',
name: 'Ada Lovelace',
user_hash: '61cb1b052b30852a660424f8e2a1c4f90996a934e09cf1aafeb174b5bae47efe'
});
</script>When the user logs in after the messenger is already running (common in single-page apps), fetch the signed identity from your server and send it with update:
// Your endpoint returns { user_id, email, name, user_hash } for the logged-in user,
// with user_hash computed on the server.
fetch('/account/chat-identity', { credentials: 'same-origin' })
.then(function (response) { return response.json(); })
.then(function (identity) {
TalkingDot('update', identity);
});When the user logs out, call TalkingDot('shutdown') so the next person on the same browser starts as a new visitor. See the JavaScript API.
Require identity verification
The Require identity verification switch in Settings → Security decides what happens to identify calls that have no valid user_hash.
| Situation | Switch off | Switch on |
|---|---|---|
user_id or email sent with a valid user_hash | Accepted; contact marked verified | Accepted; contact marked verified |
user_id or email sent without a hash, or with a wrong one | Name, email and attributes are saved, but the contact isn't verified. The user_id is only shown as an “unverified_user_id” attribute: it never becomes the contact's user ID, so it can't claim the real user's profile | Rejected: the visitor's profile isn't changed, so an anonymous visitor stays anonymous |
Switching into an existing profile (same user_id), e.g. on a second device | Needs a valid hash | Needs a valid hash |
Anonymous visitors (no user_id or email) | Unaffected | Unaffected |
So even with the switch off, nobody can take over a user's profile and conversations without a valid hash — not before their first verified visit, and not after it. Turn the switch on as soon as every page that identifies users sends a hash, so unverified identities are refused too.
Tip: roll out in this order: add user_hash everywhere, check that your users show as verified, then turn on Require identity verification. Turning it on first rejects every identify call that has no hash yet.
Troubleshooting
When the messenger starts with an identity whose hash doesn't match, the browser console shows a warning that says Identity verification failed: user_hash is missing or wrong. Check these causes in order:
The hash was computed on the wrong value
When you send a user_id, the hash must be of the user_id, even if you send an email too. Only sign the email when there's no user_id. Sign the value exactly as the page sends it: '42' and '0042' are different strings. Also make sure you used HMAC-SHA256 with the secret as the key, not a plain SHA-256 of the value plus the secret.
Email whitespace or capital letters
Emails are checked trimmed and in lowercase, so Ada@Example.com is checked as ada@example.com. Normalise the email the same way before you sign it: strtolower(trim($email)) in PHP, email.trim().toLowerCase() in JavaScript.
The hash isn't lowercase hex
The hash must be the 64-character hexadecimal digest. Base64 output (for example digest('base64')) or the raw binary digest won't match.
The secret is wrong or was rotated
Use the workspace secret from Settings → Security, not an API key or a webhook signing secret. If the secret has been regenerated, update it everywhere your servers compute hashes; hashes made with the old secret stop working straight away.
The page was cached
A page that contains a user_hash belongs to one user. If a full-page cache or CDN stores it, other visitors can be served someone else's identity, and old copies keep a stale hash after the secret changes. Exclude logged-in pages from caching, or load the identity from an uncached endpoint and pass it with TalkingDot('update', …).
Still stuck?
Compute the hash for one user from the command line with openssl (shown above), compare it with the user_hash in the page's HTML source, and check the console for the warning. If the two hashes match and the call is still rejected, contact us.