Skip to content

Identifying visitors

When a visitor is signed in to your site, you can tell the widget who they are so the conversation links to a real contact in your workspace. There are two ways to attach visitor information, and they are not the same.

  • Zev.set attaches display details. They are convenient, but unverified. Anyone can set them from the browser.
  • Zev.identify attaches a verified identity. It proves the visitor is who your site says they are, using a signature only your backend can produce.

Use identify for signed-in customers. Use set for casual context you do not need to trust.

Verification uses an HMAC signature.

  1. Your backend has a secret identify key for the brand.
  2. Your backend signs the visitor’s id with that secret and produces a signature.
  3. Your page calls Zev.identify with the visitor’s id and the signature.
  4. The server recomputes the signature with its copy of the secret. If they match, the visitor is linked to a contact.

The secret stays on your backend. Only the signature crosses into the browser, and a signature cannot be reversed into the secret.

  1. Get your identify secret. In the dashboard, go to Settings → Brands and open your brand. Under Verified identity secret, click Reveal, then copy the value. Keep it on your server and never ship it to the browser. You can Rotate the secret at any time; the previous one keeps verifying for 30 days, so you can update your backend without a gap.

  2. Compute the signature on your backend. Sign the visitor’s stable id, for example their user id, with HMAC SHA-256.

    server.js
    import crypto from 'node:crypto';
    const externalId = 'user_12345'; // your stable user id
    const hmac = crypto
    .createHmac('sha256', process.env.ZEV_IDENTIFY_SECRET)
    .update(externalId)
    .digest('hex');
    // Send `externalId` and `hmac` to the page. The secret stays here.
  3. Call identify on the page. Pass the same id and the signature your backend produced.

    Zev.identify('user_12345', 'the_hmac_your_backend_computed', {
    email: 'jane@example.com',
    firstName: 'Jane',
    lastName: 'Doe',
    });

The third argument is optional. Use it to attach known details to the contact.

FieldTypeDescription
emailstringThe contact’s email address.
firstNamestringThe contact’s first name.
lastNamestringThe contact’s last name.
metadataobjectAny extra key and value pairs to store.

Once a visitor is identified, their past conversations follow them. A customer who chatted on their laptop and later opens the widget on their phone, signed in as the same user, sees the same history. That is the main reason to identify signed-in customers rather than leaving them anonymous.

A common and important question: can someone see another person’s past conversations just by typing their email into the chat? No. Here is how the widget protects conversation history.

Each visitor’s browser holds an opaque visitor token in a first-party cookie. That token, and nothing else, is what grants access to a conversation. There is no way to look up conversations by email address. A visitor only ever holds a token for conversations their own browser started.

Details a visitor types into the chat, whether through the pre-chat form or a Zev.set call, are treated as unverified. They are stored for display only. They never link the visitor to a contact and never unlock anyone’s history. So a person on another device who types someone else’s email sees only their own conversations, which for a new browser is none.

Cross-device history requires a verified identity

Section titled “Cross-device history requires a verified identity”

The only thing that links conversations across devices is a verified Zev.identify call. The server recomputes the signature from your identify secret and compares it in constant time. A match is required before any contact link is made, and in production an identify call with a missing or wrong signature is rejected outright. Because the secret lives only on your backend, no one can forge a signature for another person’s id, so no one can reach another person’s history.

  • Embed-domain allowlist. Every request is checked against your brand’s allowlisted domains, so a leaked embed key used from another site returns nothing.
  • Workspace and brand scoping. A visitor token is only honored for the workspace and brand it belongs to.
  • No enumeration. Unknown tokens and unknown conversation ids return an empty result rather than an error, so they cannot be probed to discover what exists.

The one thing to keep in mind is shared devices. Because anonymous history rides on the browser cookie, a second person using the same browser profile can see the previous person’s anonymous conversations, the same as with any cookie-based chat. This is not an email or identity weakness. It is the nature of an anonymous session on a shared browser. Verifying a customer with Zev.identify does not change it either way.

Use caseMethod
A signed-in customer you can verify server-sideZev.identify
Casual context, such as the current page or planZev.set
An anonymous visitor filling in the chat formNeither, the widget collects it