Live Chat Identity Verification
By default, anything a visitor tells the live chat widget, whether in the pre-chat form or through window.SendSevenConfig, is treated as a claim. SendSeven keeps the conversation separate from existing contacts until an agent confirms the match. That is safe, but it means your agents cannot tell whether "[email protected]" in the chat really is Ada.
With identity verification, your server signs the logged-in user's identity using a secret that only you and SendSeven know. When the widget sends a valid signature, SendSeven:
- links the chat directly to the matching contact, found by your user id, email or phone;
- creates the contact if it does not exist yet, with the signed email and phone marked as verified;
- remembers your
user_id, so the same user always lands on the same contact, even if their email changes later; - marks the conversation as identity-verified for your agents.
If the signature is missing, invalid or expired, or verification is switched off, the widget keeps working exactly as before: the values are treated as an unverified claim. The widget shows no error, so the response does not tell an attacker whether a signature was right.
How it works
Your server Browser (your page) SendSeven
----------- ------------------- ---------
1. User logs in
2. Build message, HMAC-SHA256
with the widget secret ──────► 3. window.SendSevenConfig.identity
= { user_id, email, ..., signature }
4. Widget starts a chat ─────────► 5. Recompute signature,
check issued_at window,
bind to the contact
The secret never leaves your server. The browser only ever sees the signature, which is valid for one set of values and for a limited time.
1. Enable verification and get the secret
- Open Widgets in the SendSeven app and select your live chat widget.
- Go to the Advanced tab and scroll to Identity verification.
- Click Generate secret. The secret is shown once. Copy it straight into your server's secret store or environment variables, for example
SENDSEVEN_WIDGET_SECRET. - Switch Verify signed identities on. You cannot enable verification until a secret exists.
Generating, rotating and revealing the secret requires the Owner or Admin role, signed in to the SendSeven app. An API key or an OAuth app cannot generate or reveal the secret. Switching verification on or off requires permission to edit widgets.
Anyone who has the secret can chat as any of your users. Never put it in JavaScript, HTML, a mobile app bundle or a public repository. Only the signature goes to the browser.
2. Token format (v1)
Your page passes the identity to the widget like this:
window.SendSevenConfig = {
identity: {
user_id: "cust-123", // optional: your own stable user id
email: "[email protected]", // optional
phone: "+4915112345678", // optional, E.164 recommended
name: "Ada Lovelace", // optional: display name, NOT signed
issued_at: 1790000000, // required: unix time in seconds, when you signed
signature: "0a15d9d4..." // required: lowercase hex HMAC-SHA256
}
};
The signed message
The signature is the lowercase hex HMAC-SHA256 of this exact string, keyed with your widget secret:
"v1" + "\n" + user_id + "\n" + email + "\n" + phone + "\n" + issued_at
Rules:
| Rule | Detail |
|---|---|
| Line separator | A single line feed (\n), with no trailing newline |
| Missing field | Use the empty string. The line stays in place, so the message always has 5 lines. |
| At least one identifier | At least one of user_id, email or phone must be non-empty |
| Exact values | Fields are signed exactly as you send them. SendSeven does not trim, lowercase or reformat anything before checking the signature. Normalise the values on your server first, then sign and send the same strings. |
| No newlines inside values | A value must not contain \n |
issued_at | Unix seconds as a plain integer, for example 1790000000, not milliseconds |
name | Not part of the signature. It is only used as a display name when the contact has none. |
| Encoding | UTF-8 for both the secret and the message |
Validity window
A signature is accepted when all of these hold:
- verification is enabled for the widget and the signature matches the current secret;
now - issued_atis at most 24 hours (86,400 seconds);issued_atis at most 5 minutes in the future, which allows for clock skew.
Sign on every page load or server render so the token is always fresh. Do not cache a signed identity for longer than the page lives.
Test vector
Use this to check your implementation:
| Input | Value |
|---|---|
| secret | test_secret_do_not_use |
| user_id | cust-123 |
[email protected] | |
| phone | (empty) |
| issued_at | 1790000000 |
| message | v1\ncust-123\[email protected]\n\n1790000000 |
| signature | 0a15d9d4883f9ff7eb08d59e3e0b1e9a90082a6a9d6239a61db2cb1cae135ff0 |
3. Sign on your server
Each snippet returns the object you put into window.SendSevenConfig.identity.
Node.js
const crypto = require("crypto");
function signSendSevenIdentity({ userId = "", email = "", phone = "", name } = {}) {
const issuedAt = Math.floor(Date.now() / 1000);
const message = ["v1", userId, email, phone, String(issuedAt)].join("\n");
const signature = crypto
.createHmac("sha256", process.env.SENDSEVEN_WIDGET_SECRET)
.update(message, "utf8")
.digest("hex");
return {
user_id: userId || undefined,
email: email || undefined,
phone: phone || undefined,
name,
issued_at: issuedAt,
signature,
};
}
Python
import hashlib
import hmac
import os
import time
def sign_sendseven_identity(user_id: str = "", email: str = "", phone: str = "", name: str | None = None) -> dict:
issued_at = int(time.time())
message = "\n".join(["v1", user_id, email, phone, str(issued_at)])
signature = hmac.new(
os.environ["SENDSEVEN_WIDGET_SECRET"].encode("utf-8"),
message.encode("utf-8"),
hashlib.sha256,
).hexdigest()
identity = {"issued_at": issued_at, "signature": signature}
if user_id:
identity["user_id"] = user_id
if email:
identity["email"] = email
if phone:
identity["phone"] = phone
if name:
identity["name"] = name
return identity
PHP
<?php
function sign_sendseven_identity(string $userId = '', string $email = '', string $phone = '', ?string $name = null): array
{
$issuedAt = time();
$message = implode("\n", ['v1', $userId, $email, $phone, (string) $issuedAt]);
$signature = hash_hmac('sha256', $message, getenv('SENDSEVEN_WIDGET_SECRET')); // lowercase hex
return array_filter([
'user_id' => $userId,
'email' => $email,
'phone' => $phone,
'name' => $name,
'issued_at' => $issuedAt,
'signature' => $signature,
], fn ($v) => $v !== '' && $v !== null);
}
Ruby
require "openssl"
def sign_sendseven_identity(user_id: "", email: "", phone: "", name: nil)
issued_at = Time.now.to_i
message = ["v1", user_id, email, phone, issued_at.to_s].join("\n")
signature = OpenSSL::HMAC.hexdigest("SHA256", ENV.fetch("SENDSEVEN_WIDGET_SECRET"), message)
{
user_id: user_id,
email: email,
phone: phone,
name: name,
issued_at: issued_at,
signature: signature
}.reject { |_, v| v.nil? || v == "" }
end
If you sign [email protected], send [email protected]. If the page sends [email protected] instead, the signature will not match and the chat falls back to an unverified claim.
4. Add the identity to your page
Render the signed identity into the page before the widget script tag. For example, in a server-rendered template:
<script>
window.SendSevenConfig = {
identity: {{ signed_identity | tojson }}
};
</script>
<script
src="https://widget.sendseven.com/widget-support.js"
data-widget-id="YOUR_WIDGET_ID"
data-tenant-id="YOUR_TENANT_ID"
async
></script>
In a single-page app, fetch the signed identity from your own backend after login, for example from GET /me/sendseven-identity, set window.SendSevenConfig.identity, and only then load the widget script. You can combine identity with the other SendSevenConfig fields such as language or custom parameters.
Only set identity for logged-in users. For anonymous visitors, leave it out and the widget behaves as usual.
5. Logout and shared devices
SendSeven.logout() when your user logs outThe widget remembers the visitor and their open chat in the browser. When a user logs out of your site, and ideally also when they switch accounts, you must call window.SendSeven.logout(). If you do not, the next person on that browser can resume the previous user's chat, including its message history.
Simply no longer passing identity does not end the stored chat. A page that loads without SendSevenConfig.identity, for example after a server-side logout, still restores the chat saved in the browser until logout() runs.
Call it in your sign-out handler, before you redirect:
async function signOut() {
// End the SendSeven chat stored in this browser first
if (window.SendSeven && typeof window.SendSeven.logout === "function") {
window.SendSeven.logout();
}
await fetch("/logout", { method: "POST" }); // your own logout
window.location.href = "/";
}
logout() forgets the visitor on this browser, closes the chat connection and removes SendSevenConfig.identity. The next person on the same device starts as a new, anonymous visitor.
Two things the widget does on its own, which are not a replacement for logout():
- If a different user signs in on the same browser, the widget detects the new signed identity and starts a new visitor for them.
- When a new chat is started without a valid signature, a browser that was linked through a signed identity is not reconnected to that user's contact. This does not affect a chat that is still stored in the browser, which is why
logout()is required.
6. Rotating the secret
Rotate the secret whenever it may have leaked, or when someone with access leaves your team:
- Open Widgets → your live chat widget → Advanced → Identity verification and click Rotate secret (Owner or Admin).
- Copy the new secret and deploy it to your servers.
The old secret stops working the moment you rotate, with no grace period. Until your servers sign with the new secret, chats fall back to unverified claims. They keep working, but they are not linked automatically. Deploy the new secret promptly, ideally during a quiet period.
Show secret displays the current secret again, for example to configure an additional server. Every rotation and reveal is recorded with the acting user.
Security notes
- Never expose the secret in the browser. Only the signature and the signed values go to the page.
- Sign per page load. A signed identity is valid for up to 24 hours. Anyone who copies a token within that window can start chats as that user, so keep tokens short-lived and never put them in URLs, logs or analytics.
- Only sign emails and phone numbers you have verified yourself. A signed email or phone links the chat to the existing contact that holds it, including that contact's conversation history. If your sign-up flow does not confirm an address, sign only
user_iduntil it is confirmed. - Use a stable
user_id. Your own immutable user id, such as a database id, is the most reliable key. Emails and phone numbers can change or be reused. - Call
window.SendSeven.logout()on sign-out and account switch. This is required, not optional: a page that simply stops passingidentitystill restores the stored chat. See Logout and shared devices. SendSevenConfig.contactIdis never trusted. Passing a SendSeven contact id stays an unverified claim, even when verification is enabled. Useidentityinstead.- Only verified values are trusted. Values from the pre-chat form or other
SendSevenConfigfields remain claims, and agents see them as unverified.
API reference
You can read and switch the setting through the REST API as well. None of these endpoints is needed for signing itself.
| Method | Endpoint | Required scope | Description |
|---|---|---|---|
GET | /api/v1/widgets/{widget_id}/identity-verification | widgets:read | Status: enabled, has_secret, secret_hint (last 4 characters), secret_rotated_at, max_age_seconds. Never returns the secret. |
PUT | /api/v1/widgets/{widget_id}/identity-verification | widgets:update | Body {"enabled": true} or {"enabled": false}. Enabling without a secret returns 409 with secret_required. |
POST | /api/v1/widgets/{widget_id}/identity-verification/rotate | settings:admin | Generates a new secret and returns {secret, secret_hint, secret_rotated_at}. The old secret is invalid immediately. |
POST | /api/v1/widgets/{widget_id}/identity-verification/reveal | settings:admin | Returns {secret}, or 404 with secret_not_found when none exists. |
rotate and reveal only accept a signed-in Owner or Admin session from the SendSeven app. API keys, OAuth apps (including MCP clients) and embed tokens get 403 with {"detail": {"error_code": "interactive_session_required"}}, even when they carry the settings:admin scope. This keeps the secret out of automated integrations. To rotate the secret, use Rotate secret in the app. GET and PUT work with an API key that has the listed scope.
Responses are sent with Cache-Control: no-store. Error bodies look like {"detail": {"error_code": "secret_required", "message": "..."}}.
Troubleshooting
| Symptom | Check |
|---|---|
| Chats never show as verified | Is Verify signed identities switched on? Is the widget on the page the same widget whose secret you use? |
| Works locally, fails in production | Server clock: issued_at must be unix seconds and within the 24-hour / 5-minute window. Check that NTP is running. |
| Fails for some users only | The values sent differ from the values signed, for example a different case, extra whitespace or phone formatting. Or a value contains a newline. |
| Stopped working suddenly | Someone rotated the secret. Deploy the new one. |
Related
- Live Chat - live chat channel overview
- Widget Appearance & Channels - widget configuration options
- Webhook Signature Verification - the same HMAC-SHA256 approach for webhooks