For developers

Signed visitor identity

Your server signs a token, and the chat knows which signed-in visitor is writing.

On this page

If visitors sign in on your site, your server can tell the chat who is writing. Then:

  • operators see the account and the fields you send;
  • the visitor finds their conversation on any device and on any of the project's domains.

Without a signature, the browser keeps a conversation per site. On a new device or mirror, the visitor starts afresh.

How it works

  1. Your server signs a token for the signed-in user with the project's identity secret.
  2. The page passes it to the chat: data-identity on the tag, or identify(token).
  3. The widget sends it to POST https://api.tolkly.io/v1/projects/<project>/identify. The server checks it and says which conversation is this visitor's: the one in this browser or, if this browser has none, their latest open one.

The widget checks nothing itself. If the server refuses a token, no part of it is trusted: the chat goes on as an anonymous one.

The token

An HS256 JWT. The server always reads the algorithm as HS256, whatever the header says. A header naming any other algorithm, none included, is refused.

ClaimRequiredWhat it is
subyesYour user id, a string (a number is taken as one), up to 200 characters. A longer one refuses the token.
namenoThe name operators see, up to 120 characters.
emailnoDropped if it doesn't look like an email.
phonenoDropped if it doesn't look like a phone number.
fieldsnoAn object of the fields the project declared (below).
expnoExpiry, in seconds since the epoch. Without it, a token works as long as the secret does.

exp is checked with 60 seconds of slack, so a slightly slow clock doesn't refuse a fresh token. A token longer than 8192 characters is refused unread.

The payload is signed, not encrypted: the visitor can read it in the page. Send only what the visitor may see.

Fields

Fields are the project's own: an account number, a KYC status, a VIP level, a link to the user in your admin. The project declares each field with a key and a type. Anything else in fields is dropped, and so is a value of the wrong type. Owners and admins of the company declare them.

TypeTakes
texta string up to 500 characters (a number is taken as text)
numbera number, or a string that is one
dateYYYY-MM-DD or a full ISO 8601 time
listan array of strings or numbers, up to 100 characters each; the first 20 are kept. A single value counts as one item. If any item is empty, too long or of another type, the whole list is dropped.
linkan http: or https: URL, up to 2000 characters
booleantrue or false (also "true", "false", 1, 0)

Fields are declared in the project's «Install» section, under «Advanced settings», in the block «Visitor fields»: a title operators see, the key your server writes, the type, and whether operators see the value. A key stays once made: your server already sends it, so a new key is a new field. Turn «Show to operators» off for a value kept for rules only, such as a risk score.

The secret

Owners and admins of the company find it in the project's «Install» section: «Advanced settings» › block «Visitor identity» › «Show the secret».

Keep it on your site's server, in an environment variable such as TOLKLY_IDENTITY_SECRET. Never put it in browser code or a page: whoever has it can sign in as any of your customers.

Sign on your server

Node.js:

JavaScript
// npm install jsonwebtoken
import jwt from 'jsonwebtoken'

const token = jwt.sign(
  {
    sub: String(user.id),
    name: user.name,
    email: user.email,
  },
  process.env.TOLKLY_IDENTITY_SECRET,
  { algorithm: 'HS256', expiresIn: '12h' },
)

PHP:

PHP
// composer require firebase/php-jwt
use Firebase\JWT\JWT;

$token = JWT::encode([
  'sub' => (string) $user->id,
  'name' => $user->name,
  'email' => $user->email,
  'exp' => time() + 12 * 3600,
], getenv('TOLKLY_IDENTITY_SECRET'), 'HS256');

Python:

Python
# pip install pyjwt
import os, time, jwt

token = jwt.encode({
    "sub": str(user.id),
    "name": user.name,
    "email": user.email,
    "exp": int(time.time()) + 12 * 3600,
}, os.environ["TOLKLY_IDENTITY_SECRET"], algorithm="HS256")

Add fields next to the other claims, for example fields: { vip: user.vip }.

Pass the token to the chat

If the page is rendered for a signed-in user, put the token on the tag:

HTML
<tolkly-chat endpoint="https://api.tolkly.io" project="your-project"
  data-identity="<token>"></tolkly-chat>

If the user signs in without a page reload, pass the token from a script:

JavaScript
await customElements.whenDefined('tolkly-chat')
document.querySelector('tolkly-chat').identify(token)

The element also watches data-identity: change the attribute and the chat names the visitor again.

On sign-out, call reset() and stop rendering data-identity. The chat forgets the visitor and the conversation kept in this browser, so the next person at this computer starts afresh. If the browser holds another signed visitor's conversation, the server gives it a fresh one too.

Hand it to an AI agent

Owners and admins get «Hand it to an AI agent» in the project's «Install» › «Advanced settings», block «Visitor identity». It gives the prompt below, with the project filled in, for the coding agent that edits the site (Claude Code, Cursor). With it, the agent:

  • signs on the server with the secret from TOLKLY_IDENTITY_SECRET;
  • passes the token by data-identity or identify(token);
  • calls reset() on sign-out;
  • checks the result in the inbox's visitor card.

The prompt names the environment variable, never the secret. Here it has your-project in place of the project's id. «Open in…» → «Prompt for an AI agent» copies it.

Add signed visitor identity to the Tolkly support chat on this website, so operators see which customer writes.

Project: your-project
Widget endpoint: https://api.tolkly.io
Secret: the environment variable TOLKLY_IDENTITY_SECRET on the site’s server. The owner copies it from Tolkly: «Install» › «Advanced settings» › «Visitor identity» › «Show the secret».

1. Read https://tolkly.io/docs/en/identity.md. If it does not load, this prompt is enough.
2. Find where the site’s server knows the signed-in user (the session, the auth middleware) and the layout that renders the <tolkly-chat> tag. If the site has no <tolkly-chat> tag yet, stop and tell me: the chat is installed first.
3. On the server, sign an HS256 JWT for the signed-in user with the secret from TOLKLY_IDENTITY_SECRET, using the platform’s usual JWT library. Never sign in browser code and never send the secret to the page. If the variable is not set, stop and ask me to set it; never write the secret into the repository.
4. Claims: "sub" is the user’s id as a string (required, up to 200 characters); "name" is the name operators see (up to 120); "email" and "phone" when the site has them; "exp" is the expiry in seconds since the epoch, for example 12 hours ahead. Extra data goes in "fields": {…}, only the keys the project declared (listed under «Fields your site may send» in the same block); anything else is dropped. The payload is readable in the page: send only what the visitor may see.
5. Hand the token to the chat. When the page is rendered for a signed-in visitor, put it on the tag: data-identity="<token>". After a sign-in without a page reload, call identify(token) on the element: await customElements.whenDefined('tolkly-chat'); document.querySelector('tolkly-chat').identify(token). An anonymous visitor gets no data-identity.
6. On sign-out, call document.querySelector('tolkly-chat').reset() and stop rendering data-identity, so the next person at this computer starts afresh.
7. Check it in a browser: sign in on the site, open the chat and write a message. In Tolkly’s inbox, under «All», the conversation’s visitor card «Account on the site» shows the user id, marked «Verified». The browser console has no line "tolkly: the identity token was refused (…)"; if it has one, the word in brackets says why (malformed, algorithm, signature, expired). Then sign out and open the chat again: it is anonymous, with a new conversation. Then list the files you changed.

Without a signature

identify({ id, name, email, phone, fields }) names the visitor without a token. Operators see it marked «not verified». It stays on that conversation only. It never joins a history across devices and never takes over a signed visitor's conversation.

A refused token

The chat goes on as an anonymous one and logs the reason in the browser console:

tolkly: the identity token was refused (signature)
CodeWhy
malformedNot three base64url parts of JSON, no sub or one over 200 characters, an exp that is not a number, or longer than 8192 characters.
algorithmThe header names an algorithm other than HS256.
signatureSigned with another secret: an old one, or another project's.
expiredexp has passed, beyond the allowed clock skew.

Rotate the secret

Press «Issue a new secret» in the app (owners and admins only). The old secret stops working at once. Tokens signed with it are refused with signature until your server signs with the new one. So rotate the secret together with your deploy.