Skip to content
Console

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”.

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.

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);

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.

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.

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.

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.

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 and GET /v1/tickets answers 401.
  • A domain with accents or another script (IDN) is signed in punycode, the xn-- form: ana@münchen.example is 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 None
hash = 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;

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-email="[email protected]"
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:

EspejoChat.identify("[email protected]", "3f5c...e91a", "Ana");
// 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.

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.

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.