Skip to main content

Why identity verification

Without identity verification, anyone can claim to be any user by sending an arbitrary userId 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: Copy your widget key from Setup > Install. Generate your identity secret under Settings > Security. Never confuse them — exposing the identity secret in client code defeats the entire system.

JWT payload

The JWT must include user_id and may include any additional claims:
Algorithm: HS256. The signing key is your Identity Secret (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).
If you expose JWT minting through an HTTP route (for 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 as userToken when initializing Halo. Always include userTraits with at least name and email so users show up correctly in your inbox:
For server-rendered pages, you can inject the token directly:

Token refresh

JWTs expire. The SDK supports two refresh patterns: 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.
When identity fails (expired, missing, or rejected with 403), the SDK stops sending known-expired JWTs and pauses user-scoped polling until getUserToken or identify(..., { userToken }) succeeds. Server-side enforce mode is unchanged.

Manual: identify() rotation

You can still rotate tokens yourself on session refresh:
A common pattern: generate the JWT with 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: Stay in monitor while integrating. The Security page tracks production health and unlocks enforce only after verified requests succeed in the last 24 hours with no failures.

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:
If identity verification is enabled and a non-anonymous user_id is provided without a valid user_token, the request returns 403.

Best practices

  1. Always pass name and email in userTraits (during init) or via identify(). Without them, users show as “Anonymous” in tickets and conversations.
  2. Set exp to match your session length — 24 hours is a common default. Shorter is more secure.
  3. Refresh tokens with getUserToken (preferred) or identify(..., { userToken }) on session refresh.
  4. Wire onIdentityExpired so your app refreshes credentials when enforce mode rejects stale JWTs.
  5. Include trusted claims — the more claims (email, role, plan, company_id), the more personalized the AI without extra database lookups.
  6. 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.