Why identity verification
Without identity verification, anyone can claim to be any user by sending an arbitraryuserId to your widget. For internal tools and demos this is fine. For production B2B SaaS, a malicious user could impersonate a colleague and read their support history.
Identity verification fixes this by requiring every chat call to include a JWT signed with your Identity Secret. The Halo backend verifies the signature; if it doesn’t match, the call is rejected with 403.
The JWT can also carry trusted claims — email, name, role, plan, company_id — that Halo will use without a database lookup. This makes the AI more responsive and lets you prove identity even when the user record hasn’t been pushed to Halo yet.
Two keys, two roles
Halo generates two keys when you set up your project:| Key | Prefix | Where it goes | Purpose |
|---|---|---|---|
| Widget key | ab_live_... | Frontend (browser) | Identify your workspace — publishable, safe to expose |
| Identity secret | ha_secret_... | Backend only | Sign JWTs for identity verification |
JWT payload
The JWT must includeuser_id and may include any additional claims:
| Claim | Required? | Notes |
|---|---|---|
user_id | Required | Must match the userId passed to Halo from the frontend |
exp | Recommended | Standard JWT expiration (Unix timestamp) |
| All other claims | Optional | Used as trusted user data by the AI |
ha_secret_...).
Server-side: generate the JWT
Sign the JWT on your backend whenever you serve a page that loads the Halo widget. Pass the resulting token to your frontend (via the page HTML, an API endpoint, or session state).getUserToken or SPA refresh), log errors in your catch blocks before returning 500. Empty catch {} blocks make production debugging much harder.
Client-side: pass the token
Pass the JWT asuserToken when initializing Halo. Always include userTraits with at least name and email so users show up correctly in your inbox:
Token refresh
JWTs expire. The SDK supports two refresh patterns:Recommended: getUserToken (SDK 0.9.0+)
Provide an async callback that fetches a fresh JWT from your backend. The SDK calls it when the static userToken from init() is missing or near expiry (5-minute skew). The identity secret stays on your server.
getUserToken or identify(..., { userToken }) succeeds. Server-side enforce mode is unchanged.
Manual: identify() rotation
You can still rotate tokens yourself on session refresh:
exp matching your app’s session length (e.g. 24 hours) and refresh both at the same time.
Verification modes (dashboard)
After generating an identity secret under Settings > Security, choose how strictly Halo validates tokens:| Mode | Behavior |
|---|---|
| Off | No verification |
| Monitor | Validate when present; log warnings but allow requests. Default when you generate a secret |
| Enforce | Reject unverified authenticated requests with 403 |
Server-to-server calls
The same token applies to direct REST API calls from your backend. For example,POST /api/sdk/chat accepts user_token:
user_id is provided without a valid user_token, the request returns 403.
Best practices
- Always pass name and email in
userTraits(during init) or viaidentify(). Without them, users show as “Anonymous” in tickets and conversations. - Set
expto match your session length — 24 hours is a common default. Shorter is more secure. - Refresh tokens with
getUserToken(preferred) oridentify(..., { userToken })on session refresh. - Wire
onIdentityExpiredso your app refreshes credentials when enforce mode rejects stale JWTs. - Include trusted claims — the more claims (email, role, plan, company_id), the more personalized the AI without extra database lookups.
- Never expose the Identity Secret client-side — keep it in environment variables, never in your HTML or frontend bundle.
Where to go next
Identify Users
The basic identification flow before adding verification.
API Reference
Server-to-server calls with JWT.
Security Settings
Generate your identity secret and set verification mode.