Developer guide

Visitor identification API

Programmatically identify logged-in users so they never see the email prompt, and their conversations are automatically linked to a contact.

When to use this

If your website has logged-in users (SaaS apps, dashboards, member portals), you already know who they are. Use the identify command to pass their email to Sonny so they don't have to enter it again.

  • Skip the email prompt entirely for logged-in users
  • Automatically link chat conversations to their contact record
  • See the visitor's name and email in your inbox immediately
  • Continue history across browsers and devices with verified identity

Cross-device continuity

Verified visitor identity

Basic email identification is convenient, but the browser can claim any email. Verified identity adds a short-lived JWT signed by your server. Sonny can then safely use the customer’s contact as the owner of their website chat history, so the same conversations appear on another browser or device.

Off

The default. Nothing changes for existing embeds or visitors; history remains tied to the browser’s visitor ID.

Optional verification (recommended)

Valid JWTs get cross-device history. Visitors without a JWT keep the existing anonymous or email-identify experience.

Required to identify

Anonymous chat still works, but identify and custom-attribute calls require a valid signed JWT.

1. Generate a signing secret

Open Settings → Sources → Live Chat, find Secure visitor identity, choose a mode, and generate a secret. The plaintext is shown once. Save it in your backend’s secret manager as SONNY_IDENTITY_SECRET. Never put the signing secret in browser code, an embed snippet, a public environment variable, or your source repository.

2. Mint a short-lived JWT on your backend

Sign with HS256. Sonny requires user_id, email, iat, and exp. The user_id must be a stable ID from your own database, not an email address. Tokens can live for at most 24 hours; 15 minutes is a good default. Clocks may differ by up to 60 seconds.

Node.js
import { SignJWT } from 'jose';

export async function createSonnyIdentityToken(user) {
  const secret = new TextEncoder().encode(process.env.SONNY_IDENTITY_SECRET);

  return new SignJWT({
    user_id: String(user.id),
    email: user.email
  })
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('15m')
    .sign(secret);
}
Ruby
require 'jwt'

def sonny_identity_token(user)
  now = Time.now.to_i
  payload = {
    user_id: user.id.to_s,
    email: user.email,
    iat: now,
    exp: now + 15 * 60
  }

  JWT.encode(payload, ENV.fetch('SONNY_IDENTITY_SECRET'), 'HS256')
end
Python
import os
from datetime import datetime, timedelta, timezone
import jwt

def sonny_identity_token(user):
    now = datetime.now(timezone.utc)
    return jwt.encode({
        "user_id": str(user.id),
        "email": user.email,
        "iat": now,
        "exp": now + timedelta(minutes=15),
    }, os.environ["SONNY_IDENTITY_SECRET"], algorithm="HS256")
PHP
use Firebase\JWT\JWT;

function sonnyIdentityToken($user): string {
    $now = time();
    return JWT::encode([
        'user_id' => (string) $user->id,
        'email' => $user->email,
        'iat' => $now,
        'exp' => $now + (15 * 60),
    ], $_ENV['SONNY_IDENTITY_SECRET'], 'HS256');
}

3. Pass only the JWT to the widget

Return the JWT from an endpoint protected by your normal application session. Call identify before or after init. Sonny keeps the JWT in memory only: it is never saved to localStorage, cookies, or a URL.

JavaScript
// Fetch a short-lived JWT from your authenticated backend.
// Your signing secret never reaches this code.
const { userJwt } = await fetch('/api/sonny-identity').then((res) => res.json());

sonny('identify', { userJwt });
sonny('init', { siteId: 'YOUR_SITE_ID' });

// Sonny asks for a fresh token after expiry or an emergency key change.
window.addEventListener('sonny:identity-required', async () => {
  const { userJwt } = await fetch('/api/sonny-identity').then((res) => res.json());
  sonny('identify', { userJwt });
});

Refresh, logout, and rotate safely

  • Refresh: refresh the JWT before it expires—around 10 minutes for a 15-minute token—then call sonny('identify', { userJwt }). Also listen for sonny:identity-required as the fallback for expiry or a signing-key change.
  • Logout: call sonny('reset'). This drops the in-memory JWT, clears the browser’s widget session, and starts a fresh anonymous visitor.
  • Account switch: call sonny('reset') for user A before fetching user B’s JWT and calling sonny('identify', { userJwt }). Never carry one user’s token into another signed-in session.
  • Routine rotation: Sonny accepts the previous secret for 24 hours, giving you time to update every backend instance. A second routine rotation is blocked until that overlap ends.
  • Suspected secret leak: choose Replace immediately. Both old signing keys stop authenticating new requests at once; connected widgets ask the host page for a fresh JWT.
  • Turn off verification: Sonny discards the current and previous signing secrets and disconnects verified widgets. Generate a new secret before enabling verification again.

Identity conflicts are not auto-merged

If a stable user ID and email point to different Sonny contacts, verification returns a conflict instead of combining customer records silently. Correct the token or merge the contacts in Sonny, then retry.

A chat linked only by an earlier unsigned email is not automatically treated as verified history. This prevents a browser-supplied email from unlocking another customer’s conversations.

Email identification is not authentication

The browser-supplied email form improves support context, but it does not authenticate the visitor. A visitor can inspect and run JavaScript on their own page, so email, name, and custom attributes never unlock another customer’s Sonny history. Verified identity requires the server-signed JWT described above. Keep authorization for actions in your own product inside your signed-in application.

Code examples

The simplest call — just pass the user's email:

JavaScript
// Identify a logged-in user
sonny('identify', {
  email: 'jane@example.com'
});

Pass the user's name too, so agents see it in the inbox:

JavaScript
// Identify with full name
sonny('identify', {
  email: 'jane@example.com',
  name: 'Jane Smith'
});

Add useful support context during identification, update it later, or watch a value that changes while the page is open:

JavaScript
// Identify with support context
sonny('identify', {
  email: 'jane@example.com',
  name: 'Jane Smith',
  attributes: {
    plan: 'starter',
    seats: 5,
    trial: true
  }
});

// Update only the values that changed
sonny('setAttributes', {
  plan: 'growth',
  seats: 8,
  trial: null // Clears this property
});

// Keep a changing value in sync (checks every 10 seconds)
sonny('watchAttributes', () => ({
  monthly_usage: window.currentUsage
}), { interval: 10000 });

Full example with the async snippet. Note that identify can be called before init — the identity is queued and sent as soon as the widget connects:

HTML
<!-- Sonny widget snippet -->
<script>
  (function(w,d,s,o,f,js,fjs){
    w['Sonny']=o;w[o]=w[o]||function(){
    (w[o].q=w[o].q||[]).push(arguments)};
    js=d.createElement(s);fjs=d.getElementsByTagName(s)[0];
    js.id=o;js.src=f;js.async=1;fjs.parentNode.insertBefore(js,fjs);
  })(window,document,'script','sonny','https://www.usesonny.com/widget.js');

  // Identify before init — identity is queued and sent on connect
  sonny('identify', {
    email: 'jane@example.com',
    name: 'Jane Smith'
  });

  sonny('init', { siteId: 'YOUR_SITE_ID' });
</script>

Call reset when the user logs out to clear their identity and start a fresh session:

JavaScript
// Reset on logout — clears identity and starts a fresh session
sonny('reset');

How it works

  1. 01

    Widget loads and connects

    The widget connects to Sonny and sends any stored identity along when it joins.

  2. 02

    Identity is sent to the server

    Sonny looks for a contact with that email in your workspace, and creates one if it doesn't exist yet.

  3. 03

    Conversations are linked

    Any unlinked conversations in the current browser's visitor session are linked to the identified contact. Identification does not merge history between browsers or devices.

  4. 04

    Email prompt is skipped

    Since the visitor is already identified, the in-widget email prompt is suppressed — no interruption for the user.

Custom attribute rules

  • Send at most 50 attributes per call.
  • A value can be a string, number, boolean, or null. Strings are limited to 1000 characters. Pass null or an empty string to clear a saved value.
  • Keys must start with a letter and contain only letters, numbers, and underscores, with a maximum of 64 characters.
  • These keys are reserved and ignored: email, name, id, phone, createdAt, updatedAt.

API reference

sonny('identify', { email, name?, attributes? })
Identifies the current visitor. Sets the email and optional name, skips the email prompt, and sends the identity to the server. Can be called before or after init.
  • emailstringVisitor's email address
  • namestring?Visitor's display name
  • attributesobject?Custom attributes to attach to the contact (see the widget setup guide for key and value rules)
sonny('identify', { userJwt, name?, attributes? })
Securely identifies the current signed-in customer from a server-generated JWT. The token stays in memory and is sent only in authenticated request or socket payloads.
  • userJwtstringA fresh HS256 JWT minted by your authenticated backend
  • namestring?Visitor's display name
  • attributesobject?Custom attributes to attach to the verified contact
sonny('setAttributes', { ... })
Updates custom attributes for the identified visitor. Only changed values are sent. If the visitor isn't identified yet, updates wait and are sent after identify runs.
  • attributesobjectKey-value pairs to set. Pass null as a value to clear an attribute.
sonny('watchAttributes', getter, { interval? })
Calls your getter function on a timer and syncs any changed attributes automatically. Useful when values like plan or usage change while the page is open.
  • getterfunctionA function that returns the current attributes object
  • intervalnumber?How often to check, in milliseconds. Default 10000, minimum 2000.
sonny('reset')
Clears the current browser's widget ID, email, name, and local conversation history, then starts a fresh visitor session. It does not delete the contact or their saved properties in Sonny. Use this on logout.

Need help?

Check out the widget setup guide or get in touch with our team.

Related docs