1. Docs
  2. 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_hash is the HMAC-SHA256 of the user's user_id (or of their email when you don't send a user_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": true in 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

  1. Copy your workspace secret. Open Settings → Security and copy the identity verification secret. Store it on your server, for example as the TALKINGDOT_SECRET environment variable.
  2. 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.
  3. Pass it to the messenger. Add user_hash next to user_id and email in window.TalkingDotSettings, or in your TalkingDot('boot') or TalkingDot('update') call.
  4. 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 verified field is true once it was accepted.
  5. 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
<?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

JavaScript
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

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

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 valueExpected user_hash
4261cb1b052b30852a660424f8e2a1c4f90996a934e09cf1aafeb174b5bae47efe
ada@example.com652d0e0439eb2bb5c664437cb035c9561fe969c91e217562adcb0f580d9e87e7

You can also compute a hash from the command line to compare with what your app prints:

Shell
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:

layout.php
<?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:

HTML
<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:

HTML
<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:

JavaScript
// 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.

SituationSwitch offSwitch on
user_id or email sent with a valid user_hashAccepted; contact marked verifiedAccepted; contact marked verified
user_id or email sent without a hash, or with a wrong oneName, 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 profileRejected: 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 deviceNeeds a valid hashNeeds a valid hash
Anonymous visitors (no user_id or email)UnaffectedUnaffected

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.

Last updated October 4, 2026 Something unclear? Tell us