Skip to main content

User Identification

The Lovina Chat SDK supports both anonymous and identified users. This guide explains how each works and when to use them.

Anonymous Users (Default)​

When no user is identified, the widget creates an anonymous session:

  • A session token is auto-generated on the first message
  • The token is saved to localStorage for session persistence
  • On page reload, the conversation resumes via the stored token
  • The user appears as "Guest" in the CRM dashboard
  • No personal data is collected
// No setUser() call needed — widget works out of the box
window.lovinaChatSDK.run({
websiteToken: 'YOUR_TOKEN',
baseUrl: 'https://chat.lovina.app',
});
When to use anonymous mode
  • Public-facing websites where visitors aren't logged in
  • Landing pages, marketing sites, help centers
  • Quick support without requiring sign-in

Identified Users​

When your app knows who the user is (e.g., after login), call setUser() to link the chat session to their identity:

Web​

window.$lovina.setUser('user-123', {
name: 'Jane Doe',
email: 'jane@example.com',
avatarUrl: 'https://example.com/jane.jpg',
phoneNumber: '+628123456789',
customAttributes: {
company: 'Acme Inc',
plan: 'pro',
},
});

Android​

val widget = LovinaChatWidget(this)
widget.configure(LovinaChatConfig(
websiteToken = "YOUR_TOKEN",
baseUrl = "https://chat.lovina.app",
user = LovinaChatUser(
identifier = "user-123",
name = "Jane Doe",
email = "jane@example.com",
)
))

iOS​

let config = LovinaChatConfig(
websiteToken: "YOUR_TOKEN",
baseUrl: "https://chat.lovina.app",
user: LovinaChatUser(
identifier: "user-123",
name: "Jane Doe",
email: "jane@example.com"
)
)

React Native​

<LovinaChatWidget
config={{
websiteToken: 'YOUR_TOKEN',
baseUrl: 'https://chat.lovina.app',
user: {
identifier: 'user-123',
name: 'Jane Doe',
email: 'jane@example.com',
},
}}
/>

Flutter​

LovinaChatWidget(
config: LovinaChatConfig(
websiteToken: 'YOUR_TOKEN',
baseUrl: 'https://chat.lovina.app',
user: LovinaChatUser(
identifier: 'user-123',
name: 'Jane Doe',
email: 'jane@example.com',
),
),
)

User Data Fields​

FieldTypeRequiredDescription
identifierstringYesUnique user ID from your system
namestringNoDisplay name shown to agents
emailstringNoEmail address
avatarUrlstringNoProfile image URL
phoneNumberstringNoPhone number
companyNamestringNoCompany or organization name
customAttributesobjectNoAny extra metadata (plan, tier, etc.)

Note: companyName is stored on the contact record but isn't currently surfaced in the CRM dashboard — only name, email, avatarUrl, phoneNumber, and customAttributes reach agents today.

What Agents See​

ModeCRM Dashboard Shows
Anonymous"Guest User" — no name, email, or history across sessions
IdentifiedUser's name, email, avatar, custom attributes, full conversation history

Identity Verification​

To cryptographically verify that a session belongs to the claimed user, pass the identity config option alongside setUser().

The identity Option​

Pass a server-signed identity object alongside your other config -- on web, to run() rather than lovinaSettings, since it's a signed credential rather than a display setting:

window.lovinaChatSDK.run({
baseUrl: 'https://chat.ailovina.com',
websiteToken: 'YOUR_TOKEN',
identity: {
mode: 'user',
userId: 'user-123',
exp: 1735689600,
sigHash: 'server-computed-hmac-hash',
},
});
OptionTypeDescription
mode'user'Identity mode. Only 'user' is available today.
userIdstringThe same user ID you pass to setUser().
expnumberUnix timestamp (seconds) after which sigHash expires.
sigHashstringHMAC signature computed on your server -- never in the browser or app.

Enforcement Modes​

Every channel has an enforcement mode that decides what Lovina does with identity. The mode is set on the channel behind your website token, so it applies to every SDK that uses that token -- web, React Native, iOS, Flutter, and Android alike, not only the web widget:

ModeWhat Lovina doesWhat your visitors see
offIgnores identity.Chat works with or without identity.
monitorVerifies identity and records the result on the session, but never rejects.Every visitor's messages send successfully -- signed or unsigned, valid signature or not. The verification result is only recorded server-side, so nothing about it is visible to the visitor. Use this mode to confirm your signatures verify before enforcing them.
requiredVerifies identity and rejects a missing or invalid one with 401 when the session is created.Users with a valid signature chat normally. Anyone else can open the chat, but their first message fails to send. Existing sessions that were never verified can no longer be resumed: a visitor whose session was created before signing was verified starts a new conversation on their first visit after the switch and loses the earlier transcript, even if their new signature is valid.

Omitting identity never blocks a visitor on off or monitor -- only required turns a missing or invalid signature into a failed conversation. Treat monitor as a dress rehearsal in which every session should already be signed, not as a mode to leave sessions unsigned in.

monitor and required need a signing secret on the channel, so ask your Lovina contact to set one up and to move your channel to monitor first and then to required.

A mode or secret change can take up to 30 seconds to apply.

When Verification Happens​

Lovina verifies identity once, when the session is created -- that is, on the visitor's first message (or first attachment upload), not when the widget loads. Later messages in that session are not re-checked, and resuming the session on a later page load does not need a new signature.

This is why a short exp is safe: once the session exists, the signature expiring does not interrupt the conversation. exp only has to still be valid when the visitor sends their first message, so choose a lifetime that covers the time between page load and that message (for example, one hour), and sign a fresh identity on every page load.

Signature Rules​

RuleValue
Signed message<exp>:<userId>, exactly -- exp first.
AlgorithmHMAC-SHA256 with your channel's signing secret.
sigHash formatHex-encoded digest, 64 characters (not base64).
exp unitUnix time in seconds, not milliseconds -- Math.floor(Date.now() / 1000), not Date.now().
Latest expAt most 24 hours after the session is created.
Expiry toleranceAccepted until 60 seconds after exp has passed.
userIdNon-empty, and must not contain a colon (:).

Fail Closed When Signing Fails​

A broken identity does not stop the widget. If the object is malformed -- for example, exp arrives as a string -- the SDK logs a console warning, drops it, and boots anonymously as if nothing were wrong. On a required channel the failure only shows up when the visitor's first message is rejected.

That console warning is a developer-experience aid only. The SDK never decides access: it forwards what it accepts, and the server alone verifies the signature and grants or refuses the session. So booting anonymously on a required channel is a failure state, not a supported fallback -- treat it as an outage of your chat, not as degraded-but-working behavior.

So fetch the signed identity from your own backend before starting the widget, and keep two cases apart. A visitor who is signed in but whose signature could not be minted is the failure case: on a monitor or required channel, do not call run() at all -- show your own "chat unavailable" state instead. A visitor with no signed-in user has nothing to sign -- on an off or monitor channel, boot the widget without identity and anonymous chat works normally, but on a required channel there is no anonymous chat, so don't boot for a visitor you cannot sign for: hide the launcher, or show your own "sign in to chat" state.

async function startLovinaChat() {
const config = {
baseUrl: 'https://chat.ailovina.com',
websiteToken: 'YOUR_TOKEN',
};
// Set to true and deploy this BEFORE asking Lovina to switch the channel to `required`.
const channelRequiresIdentity = false;

const bootAnonymously = () => {
if (channelRequiresIdentity) return;
window.lovinaChatSDK.run(config);
};

let identity;
try {
const res = await fetch('/api/lovina-identity', { credentials: 'include' });
if (!res.ok) throw new Error(`identity endpoint returned ${res.status}`);
identity = await res.json();
if (identity === null) return bootAnonymously();
if (
identity.mode !== 'user' ||
typeof identity.userId !== 'string' || identity.userId === '' ||
typeof identity.sigHash !== 'string' || identity.sigHash === '' ||
!Number.isFinite(identity.exp)
) {
throw new Error('identity endpoint returned a malformed identity');
}
} catch (err) {
// Booting now would look fine until the visitor's first message fails on a `required` channel.
console.error('Lovina chat not started:', err);
return;
}

window.lovinaChatSDK.run({ ...config, identity });
}

Call startLovinaChat() where the installation snippet calls run() (inside g.onload for the script tag). The null body is just this example's logged-out signal -- use whatever your own identity endpoint returns for a visitor with no signed-in user.

Each platform's LovinaChatConfig accepts the same fields in its native form -- see the Web, React Native, iOS, Flutter, and Android configuration references.

Generate the Hash (Server-Side)​

Compute sigHash on your server, HMAC-SHA256 over exp:userId using your channel's signing secret. To verify your implementation against a known answer, see Test Vector.

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.HexFormat;

Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec("YOUR_SIGNING_SECRET".getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] hash = mac.doFinal((exp + ":" + userId).getBytes(StandardCharsets.UTF_8));
String sigHash = HexFormat.of().formatHex(hash);
warning

Never compute sigHash in client-side code -- your signing secret must stay on your server.

Test Vector​

Production exp is usually derived from the current time, so live hashes are hard to compare. Use these fixed inputs as an answer key: if your server code produces the expected sigHash, your canonical string, HMAC, and hex encoding are correct.

InputValue
secretlovina-dev-secret
userIdu_42
exp1735689600
OutputValue
sigHashf79ad2982298238635062d48b08aacb49b580ceac7f3b074cadb4538a52c3ab1

The signed message is exactly 1735689600:u_42 — order is exp:userId, not reversed. Reproduce independently with:

printf '%s' '1735689600:u_42' | openssl dgst -sha256 -hmac 'lovina-dev-secret'
note

lovina-dev-secret is a public fixture for documentation and local verification — not a real channel signing secret. Use your channel's secret in production; never ship the fixture as a live credential.

Rotating the Signing Secret​

Rotating creates a new signing secret. The previous secret keeps verifying for 72 hours, so during that window signatures made with either secret are accepted. Deploy the new secret to every backend that signs identity before the window ends; Lovina tells you the exact UTC time the old secret stops being accepted. After that deadline, signatures made with the old secret fail: new sessions are rejected on a required channel and recorded as failed on a monitor channel.

Sessions created before the deadline are unaffected, because verification happens only once. Only one previous secret is kept -- rotating again during the window ends the older secret's overlap immediately.

Debugging a Rejected Session​

The 401 never says why. A missing identity, the wrong secret, an expired or too-distant exp, and a malformed userId all return the same 401 with the same generic "identity verification failed" message. No further detail is sent, by design, so don't look for it -- check your inputs against Signature Rules and the Test Vector instead.

Check the console first. Alongside the malformed-identity warning above, the SDK logs a warning when userId contains a colon or sigHash is not 64 hex characters (for example, base64). It still forwards those values, and the server rejects them. These warnings are diagnostics for you, not validation -- a clean console proves nothing about access, because only the server decides whether a session is verified.

baseUrl is not the API host. baseUrl (for example https://chat.ailovina.com) is where the widget is served from. The API host the widget calls is built into the widget at build time and is a different host, so a curl against baseUrl tests the wrong server. Read the real host from your browser's DevTools Network tab -- the widget's requests to paths starting with /sdk/chat/.

Updating User Data​

You can call setUser() multiple times — subsequent calls update the user's profile:

// Initial identification
window.$lovina.setUser('user-123', { name: 'Jane' });

// Later: update with more data
window.$lovina.setUser('user-123', {
name: 'Jane Doe',
email: 'jane@example.com',
customAttributes: { plan: 'enterprise' },
});

Resetting Identity​

To log out the user and start a fresh anonymous session:

window.$lovina.reset();

On native SDKs:

// Android
widget.clearSession()
// iOS
widget.clearSession()

This clears the stored session token and conversation history. The next interaction starts as a new anonymous user.