Chatonio

This is taking longer than usual.

Chatonio

Web chat widget: signing visitors in with their account

October 26, 2026 4 viewsChannels & Integrations

If visitors sign in on your website, the chat can sign them in too. Their chat history then follows their account: they see the same conversations on their phone, their laptop and any other browser where your site signs them in, and the chats they had in that browser before signing in move into the account. Your team sees which of your users is writing.

This is single sign-on for the web chat. It uses the same secret and the same account rules as your help center’s single sign-on, so a user who signs in to both is one contact for your team. It is available on paid plans.

When to use it

  • Your customers have accounts on your site (a shop, an app, a client area) and come back from different browsers or devices.
  • Your team needs to know for sure who is writing, not just a name or an email someone typed in.

Nothing that comes from the visitor’s browser alone can sign anyone in — not an email typed into the pre-chat form, not values from chatonio('set'), not a name or email passed with chatonio('send'). Only a pass signed by your server with your secret can.

How it works

  1. Your server signs a short-lived pass for the signed-in user.
  2. Your page gives the chat a function that fetches that pass.
  3. The chat sends the pass to Chatonio, which checks it and signs the chat in. Each pass works once.

Step 1: get the secret

Open your web chat channel’s page (Projects → your project → Channels, then your Web Chat channel). In the Web Chat Widget card, open Sign visitors in with their account and click Generate secret. Copy the secret straight away — it is shown once — and keep it on your server, never in your pages.

The Sign visitors in with their account section of the Web Chat Widget card after Generate secret: the hidden secret with Rotate secret, the one-time panel with Copy, and the code sample

Important: this is the same secret your help center’s single sign-on uses, and there is one per project. If you already use single sign-on for your help center, the secret is already there (the card shows it as set): sign chat passes with it too. Rotate secret replaces it for both at once, so until your server signs with the new secret, chat sign-in and help-center single sign-on both stop working.

Step 2: sign a pass on your server

The pass is a JSON Web Token (JWT) signed with HS256 and your secret. It carries:

  • sub — your user’s ID, as a string. Required: this is what Chatonio recognises the user by.
  • email — the user’s email. Required.
  • name — the name your team sees.
  • iat and exp — when the pass was issued and when it expires, in seconds. Required. exp can be at most 5 minutes after iat.
  • jti — a value that is unique for each pass. Without it, two passes for the same user issued in the same second are identical, and the second is refused as already used.
  • "email_verified": true — only if you have confirmed that the user owns the address. See Which account a visitor is signed in to below.

Serve the pass from an address on your site that answers only for a signed-in user, takes the user from your own session (never from the request), and returns the pass as plain text that is not cached. In Node.js with jsonwebtoken:

const jwt = require('jsonwebtoken');
const crypto = require('crypto');

app.get('/your-server/chat-pass', requireSignedIn, (req, res) => {
  const pass = jwt.sign(
    { sub: String(req.user.id), email: req.user.email, name: req.user.name, jti: crypto.randomUUID() },
    process.env.CHATONIO_SSO_SECRET,
    { algorithm: 'HS256', expiresIn: '5m' },
  );
  res.set('Cache-Control', 'no-store').type('text/plain').send(pass);
});

In Python with PyJWT:

import time, uuid, jwt

def chat_pass(user):
    now = int(time.time())
    return jwt.encode(
        {'sub': str(user.id), 'email': user.email, 'name': user.name,
         'iat': now, 'exp': now + 300, 'jti': uuid.uuid4().hex},
        CHATONIO_SSO_SECRET, algorithm='HS256')

Step 3: sign the chat in on your pages

For signed-in users on your site, add this next to the widget’s embed code:

<script>
window.chatonio = window.chatonio || function () { (window.chatonio.q = window.chatonio.q || []).push(arguments); };

chatonio('login', function () {
  return fetch('/your-server/chat-pass').then(function (r) { return r.text(); });
});
</script>

The first line lets you call chatonio(...) before the widget has loaded: the call waits and runs once the widget is ready. When the page loads, the chat also waits a few seconds for the sign-in, so it opens already signed in.

Give login a function: the chat calls it when it needs a pass — once per page load, then about every 30 minutes while the page stays open — so a long visit stays signed in. The function can return the pass or a promise of it. A server-rendered page can instead put a fresh pass straight into the page with chatonio('login', pass), but that sign-in cannot be renewed and ends after an hour at most.

Signing out

When the user signs out of your site, call:

chatonio('logout');

It ends the sign-in, and the chat in that browser starts over as a new anonymous visitor, so the next person at that computer does not carry on from the previous one.

What visitors get

  • One history on every device. Wherever your site signs them in, the chat shows their conversations from this chat.
  • Earlier chats come along. At sign-in, the chats a visitor had in that browser on this chat move into their account. Only the conversations move: details they gave about themselves before signing in (contact fields, such as those in the pre-chat form) stay with the anonymous visitor.
  • Only this chat’s conversations. The account’s help-center tickets and email threads never appear in the chat.
  • Safe on shared computers. The chat stays signed in only while your page keeps signing it in. A page load without a pass starts an anonymous chat, so the next person on a shared computer never sees the previous person’s chats. Without a fresh pass a sign-in ends after an hour at most.

What your team sees

In the conversation’s info panel, a signed-in visitor’s contact shows an Account block with the account’s email and User ID — the sub from your pass. Help-center clients show the same block, and a user who signs in to both your help center and the chat is one contact, with their tickets and chats together.

Which account a visitor is signed in to

The rules are the same as for your help center’s single sign-on:

  • A visitor is matched by the user ID in the pass (sub). If their email has changed on your side, the account’s email is updated, unless another account already has the new address.
  • If no account has that user ID but one has the same email, the pass is refused — unless that account has no user ID yet (a client your team invited, or one created before you used single sign-on) and the pass carries "email_verified": true. Then the account is linked to the user.
  • Otherwise a new help-center account is created for the user, unless your help center is set to Invite only. Then only users who already have an account can be signed in.

More on these rules: Helpdesk login options.

If the chat does not sign in

A refused pass leaves the chat anonymous, and the browser console shows a line starting with [Chatonio] chat sign-in refused, followed by the reason:

  • feature_not_available — your plan does not include chat sign-in; it is available on paid plans.
  • sign_in_not_configured — no secret has been generated yet.
  • invalid_signature — the pass was not signed with the current secret, for example after Rotate secret.
  • expired — the pass has expired. Check your server’s clock.
  • malformed — a required claim is missing, sub is not a string, the pass lives longer than 5 minutes, iat is more than 30 seconds in the future, or it was not signed with HS256.
  • replay — the pass was already used. Fetch a fresh pass every time, do not cache the response, and add a unique jti.
  • account_conflict — another account already uses that email (see above).
  • not_invited — your help center is set to Invite only and the user has no account yet.
  • account_disabled — the user’s account has been disabled.

If your function throws, or the promise it returns is rejected, the console says that the function that fetches the sign-in pass failed. Sign-in is switched off in automated browsers (Selenium, Playwright, Puppeteer and others that set navigator.webdriver), so test it in an ordinary browser.

Was this article helpful?