- Guides
- Identify the user
Identify the user
Attach an opaque user id (never PII) to recordings and votes to read feedback and replays per user, from the script tag or at runtime. Viewing is Pro. For tickets, sign the visitor's email with the identity secret.
By default every vote and recording is Anonymous, and everything works. Identify lets you attach your id for a user, so a vote or a replay can be traced back to the person who made it — and so you can pivot from a satisfaction score straight to that user’s recordings.
It is entirely optional. Without it, nothing breaks; you just see “Anonymous”.
The golden rule
Section titled “The golden rule”user-ref is an opaque id — the id your own database already has for that user
(user-123, a1b2c3). Never an email, never a name, never any PII.
The reason is physical: the id travels inside the bundle, in plain sight in the browser. Anything you put there is visible to anyone who opens dev tools. If you want real names or emails in the dashboard, that is a server-side v2 and does not go through here.
Two ways to set it
Section titled “Two ways to set it”Both are optional; use whichever fits. Pick one.
On the script, when the id is known at page render:
<script src="https://app.espejo.dev/sdk/espejo.dom.js" data-key="pk_live_..." data-user-ref="user-123"></script>At runtime, the moment your user logs in (best for SPAs, where the page never reloads):
window.espejo.identify("user-123");
// On logout, clear it — subsequent votes and recordings go back to Anonymous:window.espejo.identify(null);Where it shows up
Section titled “Where it shows up”In the console, Satisfaction → Recent votes shows the user_ref next to each
vote. From there you can filter by user, and jump from a user straight to their
recordings. One opaque id ties the two views together.
Verified identity for tickets
Section titled “Verified identity for tickets”The Tickets section of the chat widget and the
agent’s ticket tools show a visitor their own tickets. That needs more than
an opaque id: Espejo has to know the person’s email, and has to be sure the
email is theirs. It is a different thing from user-ref, and the two live side
by side.
Your backend proves the email by signing it with the identity secret of the project. The widget sends the email and the signature with each tickets request and each chat message. Without a pair that verifies there are no tickets: the widget hides the section and the agent never gets the ticket tools. An email alone is never accepted.
Get the identity secret
Section titled “Get the identity secret”In the console, open Integrations. With Soporte connected, the Tickets
card has a Visitor identity block. Generate secret shows the secret
once: copy it into your server’s environment (for example
ESPEJO_IDENTITY_SECRET). Afterwards the console only shows its last four
characters.
Rotate replaces it. Signatures made with the old secret stop working on the next request, so visitors lose their tickets until your backend signs with the new one.
Turn on Tickets in the widget
Section titled “Turn on Tickets in the widget”In the console, open the Widget tab and turn on Tickets in the
Menu block. It can be turned on once Soporte is connected with Show
tickets in the widget on. Without it, the widget shows no Tickets section,
GET /v1/tickets answers 404 and the agent does not get lookup_tickets.
While there is no identity secret, the row warns that nobody will see
Tickets.
Sign the email on your server
Section titled “Sign the email on your server”The signature is hex(HMAC-SHA256(secret, email)), with the email trimmed and
in lowercase. The result is 64 hexadecimal characters.
- Only sign an email your site has verified (a confirmed address, or one that comes from your identity provider). The signature tells Espejo the email is this person’s, so a signed address that nobody confirmed opens the tickets of whoever owns it.
- Only ASCII emails can be signed. Trim only the ASCII blanks at the ends
(space, tab,
\n,\v,\f,\r), check that what is left is printable ASCII without spaces, and then lowercase it. An email with anything else gets no identity: the widget ignores it andGET /v1/ticketsanswers401. - A domain with accents or another script (IDN) is signed in punycode, the
xn--form:ana@münchen.exampleis signed as[email protected]. Pass that same form to the widget.
The three examples below do exactly that, so they give the same signature that Espejo checks.
Node
import { createHmac } from "node:crypto";
const trimmed = user.email.replace(/^[\t\n\v\f\r ]+|[\t\n\v\f\r ]+$/g, "");// Anything that is not ASCII: no identity.const email = /^[\x21-\x7e]+$/.test(trimmed) ? trimmed.toLowerCase() : null;const hash = email && createHmac("sha256", process.env.ESPEJO_IDENTITY_SECRET).update(email).digest("hex");Python
import hashlib, hmac, os, re
trimmed = user.email.strip(" \t\n\v\f\r")# Anything that is not ASCII: no identity.email = trimmed.lower() if re.fullmatch(r"[\x21-\x7e]+", trimmed) else Nonehash = email and hmac.new(os.environ["ESPEJO_IDENTITY_SECRET"].encode(), email.encode(), hashlib.sha256).hexdigest()PHP
$trimmed = trim($user->email, " \t\n\v\f\r");// Anything that is not ASCII: no identity. strtr lowercases ASCII only, whatever the locale.$email = preg_match('/^[\x21-\x7e]+$/', $trimmed) ? strtr($trimmed, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : null;$hash = $email ? hash_hmac('sha256', $email, getenv('ESPEJO_IDENTITY_SECRET')) : null;Pass it to the widget
Section titled “Pass it to the widget”On the script, when the page is rendered for a signed in user:
<script src="https://app.espejo.dev/sdk/espejo.chat.js" data-key="pk_live_..." data-user-hash="3f5c...e91a" data-user-name="Ana" defer></script>Escape the email when you write it into the attribute, as you escape any
value you put in HTML (&, <, >, " and '). Most template engines do it
on their own; if you build the tag by concatenating strings, do it yourself.
The same goes for the name.
data-user-name is optional and only greets the visitor on Home (“Hi Ana.”).
It is used only while the email and the hash are valid, is cut at 40
characters, and is never sent to the API or stored. It is not signed: it does
not change what the visitor can see.
In a single page app, after sign in and on sign out. The third argument, the name, is optional:
// On sign out: Tickets disappears and the chat starts a new conversation.EspejoChat.identify(null);The widget sends them in the x-espejo-user-email and x-espejo-user-hash
headers. GET /v1/tickets answers 401 when they are missing or do not
verify, and 404 when the Tickets section is not on for the project.
Why the secret never goes to the browser
Section titled “Why the secret never goes to the browser”Anyone who has the secret can sign any email and read that person’s tickets. The signature has to be computed where the secret lives, on your server, and only for the user that is signed in. Never put the secret in the page, in a bundle or in a mobile app. If it leaks, rotate it.
The email and its signature do reach the browser, and that is fine: they only open the tickets of that one person, the same person that is already signed in. They are not attached to recordings or votes, and Espejo does not store the email.
One identity per conversation
Section titled “One identity per conversation”The first chat message that arrives with a valid pair ties that identity to
the conversation. A later request in the same conversation with another
identity, or with none, gets 409 with the code identity_changed, and
nothing of that conversation: the widget starts a new one and sends the
message again, once.
The widget starts a new conversation when identify changes the user, and
also when the page loads with someone else. Next to the conversation id it
keeps, in sessionStorage, a fingerprint of who opened it (never the email or
the hash). If the page loads with another identity, or without one after a
conversation that had one, it drops the saved conversation and the
Continue card in Home.