Skip to main content

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​

  1. Open Widgets in the SendSeven app and select your live chat widget.
  2. Go to the Advanced tab and scroll to Identity verification.
  3. 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.
  4. 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.

Keep the secret on the server

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:

RuleDetail
Line separatorA single line feed (\n), with no trailing newline
Missing fieldUse the empty string. The line stays in place, so the message always has 5 lines.
At least one identifierAt least one of user_id, email or phone must be non-empty
Exact valuesFields 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 valuesA value must not contain \n
issued_atUnix seconds as a plain integer, for example 1790000000, not milliseconds
nameNot part of the signature. It is only used as a display name when the contact has none.
EncodingUTF-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_at is at most 24 hours (86,400 seconds);
  • issued_at is 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:

InputValue
secrettest_secret_do_not_use
user_idcust-123
email[email protected]
phone(empty)
issued_at1790000000
messagev1\ncust-123\[email protected]\n\n1790000000
signature0a15d9d4883f9ff7eb08d59e3e0b1e9a90082a6a9d6239a61db2cb1cae135ff0

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
Send exactly what you signed

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​

You must call SendSeven.logout() when your user logs out

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

  1. Open Widgets → your live chat widget → Advanced → Identity verification and click Rotate secret (Owner or Admin).
  2. Copy the new secret and deploy it to your servers.
Rotation takes effect immediately

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_id until 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 passing identity still restores the stored chat. See Logout and shared devices.
  • SendSevenConfig.contactId is never trusted. Passing a SendSeven contact id stays an unverified claim, even when verification is enabled. Use identity instead.
  • Only verified values are trusted. Values from the pre-chat form or other SendSevenConfig fields 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.

MethodEndpointRequired scopeDescription
GET/api/v1/widgets/{widget_id}/identity-verificationwidgets:readStatus: 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-verificationwidgets:updateBody {"enabled": true} or {"enabled": false}. Enabling without a secret returns 409 with secret_required.
POST/api/v1/widgets/{widget_id}/identity-verification/rotatesettings:adminGenerates 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/revealsettings:adminReturns {secret}, or 404 with secret_not_found when none exists.
Rotate and reveal need a signed-in Owner or Admin

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​

SymptomCheck
Chats never show as verifiedIs Verify signed identities switched on? Is the widget on the page the same widget whose secret you use?
Works locally, fails in productionServer clock: issued_at must be unix seconds and within the 24-hour / 5-minute window. Check that NTP is running.
Fails for some users onlyThe 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 suddenlySomeone rotated the secret. Deploy the new one.