Identity verification (JWT)
Securely identify logged-in users with short-lived, server-signed JWTs. Includes backend recipes.
Authenticated users must be verified with a short‑lived JWT minted by your
backend — never put secrets in the browser. The SDK calls your
tokenProvider on boot, before expiry, and after a 401.
1. Get your widget's signing secret
In Settings → Integrations → Web Widget, copy the JWT secret. Store it
as a backend environment variable (e.g. SUPPORT_CHAT_WIDGET_SECRET). Treat it
like a password; rotate it from the same screen if leaked.
2. Expose a token endpoint on your backend
The endpoint returns a signed JWT (HS256) for the currently logged‑in user.
Required claims: sub (your stable user id), widget_id, workspace_id,
and exp. Recommended: email, name, company_id, jti.
{
"iss": "your_backend",
"aud": "clad_support_chat",
"sub": "user_123",
"email": "rachel@example.com",
"name": "Rachel",
"company_id": "org_456",
"workspace_id": "{YOUR_WORKSPACE_ID}",
"widget_id": "{YOUR_WIDGET_ID}",
"iat": 1760000000,
"exp": 1760000900,
"jti": "a-unique-token-id"
}Node.js (Express)
import jwt from "jsonwebtoken";
app.get("/api/support-chat-token", requireLogin, (req, res) => {
const token = jwt.sign(
{
sub: req.user.id,
email: req.user.email,
name: req.user.name,
company_id: req.user.orgId,
workspace_id: "{YOUR_WORKSPACE_ID}",
widget_id: "{YOUR_WIDGET_ID}",
},
process.env.SUPPORT_CHAT_WIDGET_SECRET,
{ expiresIn: "15m", audience: "clad_support_chat" }
);
res.type("text/plain").send(token);
});Python (Flask)
import jwt, time, os
from flask import Response
@app.get("/api/support-chat-token")
@login_required
def support_chat_token():
now = int(time.time())
token = jwt.encode({
"sub": current_user.id,
"email": current_user.email,
"name": current_user.name,
"company_id": current_user.org_id,
"workspace_id": "{YOUR_WORKSPACE_ID}",
"widget_id": "{YOUR_WIDGET_ID}",
"iat": now, "exp": now + 900,
"aud": "clad_support_chat",
}, os.environ["SUPPORT_CHAT_WIDGET_SECRET"], algorithm="HS256")
return Response(token, mimetype="text/plain")Ruby (Rails)
def support_chat_token
payload = {
sub: current_user.id, email: current_user.email, name: current_user.name,
company_id: current_user.org_id, workspace_id: "{YOUR_WORKSPACE_ID}", widget_id: "{YOUR_WIDGET_ID}",
iat: Time.now.to_i, exp: 15.minutes.from_now.to_i, aud: "clad_support_chat"
}
render plain: JWT.encode(payload, ENV["SUPPORT_CHAT_WIDGET_SECRET"], "HS256")
endGo
claims := jwt.MapClaims{
"sub": user.ID, "email": user.Email, "name": user.Name,
"company_id": user.OrgID, "workspace_id": "{YOUR_WORKSPACE_ID}", "widget_id": "{YOUR_WIDGET_ID}",
"iat": time.Now().Unix(), "exp": time.Now().Add(15 * time.Minute).Unix(),
"aud": "clad_support_chat",
}
tok, _ := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).
SignedString([]byte(os.Getenv("SUPPORT_CHAT_WIDGET_SECRET")))
w.Write([]byte(tok))3. Wire it into the SDK
await chat.boot({
user: { id: user.id, email: user.email, name: user.name },
tokenProvider: () => fetch("/api/support-chat-token").then((r) => r.text()),
});Token lifecycle
- The SDK requests a token on boot, refreshes it before expiry, and again
after any
401. - Keep tokens short‑lived (≤ 15 minutes). The widget handles refresh
transparently; expiry surfaces as an
auth:expiredevent. - The platform verifies signature, expiration, audience, and that
workspace_id/widget_idmatch the widget — and that the request origin is allow‑listed.