Identity tokens
An identity token tells KauZap which of your users is chatting. Your server signs it with a secret only you and KauZap know, so a visitor cannot pretend to be someone else.
Format
A JWT signed with HS256. The header carries kid, the id of the identity secret you signed with.
| Claim | Required | Meaning |
|---|---|---|
iss | Yes | The property's public key, pk_… |
sub | Yes | Your stable user id. The same sub always maps to the same contact. |
iat | Yes | Issued at, Unix seconds |
exp | Yes | Expiry, Unix seconds, at most 600 seconds after iat |
jti | Yes | A unique id. Each token can be used once. |
name, email, phone | No | Shown to agents and kept on the contact |
attributes | No | An object of your own fields, such as plan or account id, shown in the customer panel |
Mint one
import { createHmac, randomUUID } from 'node:crypto';
const b64url = (value) => Buffer.from(value).toString('base64url');
/**
* Mint a KauZap identity token (JWT, HS256) on your server. Never do this in the browser:
* the secret must stay on your backend.
*
* @param {object} opts
* @param {string} opts.secret Identity secret for the property (Settings, Developers).
* @param {string} opts.kid Key id of that secret, sent in the JWT header.
* @param {string} opts.propertyKey The property's public key (pk_...), sent as `iss`.
* @param {string} opts.sub Your stable user id.
* @param {string} [opts.name]
* @param {string} [opts.email]
* @param {string} [opts.phone]
* @param {Record<string, unknown>} [opts.attributes] Shown to agents in the customer panel.
* @param {number} [opts.ttlSeconds] Lifetime, at most 600 seconds. Default 300.
* @param {number} [opts.now] Current time in milliseconds. Default Date.now().
* @returns {string}
*/
export function mintIdentityToken({
secret,
kid,
propertyKey,
sub,
name,
email,
phone,
attributes,
ttlSeconds = 300,
now = Date.now(),
}) {
if (!secret || !kid || !propertyKey || !sub) {
throw new Error('secret, kid, propertyKey and sub are required');
}
if (!(ttlSeconds > 0 && ttlSeconds <= 600)) {
throw new Error('ttlSeconds must be between 1 and 600');
}
const iat = Math.floor(now / 1000);
// JSON.stringify drops the optional claims you left undefined.
const claims = {
iss: propertyKey,
sub,
name,
email,
phone,
attributes,
iat,
exp: iat + ttlSeconds,
jti: randomUUID(),
};
const header = b64url(JSON.stringify({ alg: 'HS256', typ: 'JWT', kid }));
const input = `${header}.${b64url(JSON.stringify(claims))}`;
const signature = createHmac('sha256', secret).update(input).digest('base64url');
return `${input}.${signature}`;
}
examples/node-integration/identity-token.mjsAny JWT library works too: sign with HS256, put kid in the header and keep the lifetime at 10 minutes or less.
What KauZap checks
- The signature, using the secret named by
kid, compared in constant time. issmatches the property the widget runs for.- The lifetime is at most 10 minutes, and
iatandexpare within 60 seconds of our clock. - The page's origin is in the property's allowed origins.
- The
jtihas not been used before. A replayed token is rejected.
| Error code | What happened | What to do |
|---|---|---|
invalid_identity_token | Bad signature, unknown kid, wrong iss, expired, or lifetime over 10 minutes | Check the secret, kid, property key and your server clock |
identity_token_replayed | The jti was already used | Mint a new token for every identify call |
origin_not_allowed | The page origin is not in the allowed origins | Add the origin to the property |
Rotate a secret
- Create a second identity secret for the property. It gets a new
kid. - Deploy your server with the new secret and
kid. Tokens signed with either secret are accepted meanwhile. - Revoke the old secret once no server signs with it.
Anonymous visitors
Visitors can chat before they sign in. When you later call KauZap.identify with a valid token, the anonymous conversation is merged into the identified contact.
Never mint tokens in the browser and never ship the identity secret in front-end code. Anyone holding it can sign in as any of your users.