Skip to main content

Why identify

When you identify users and companies, the AI can:
  • Greet users by name and reference their plan, role, or any custom attribute
  • Access company-specific context (integrations, events, plan details)
  • Apply the right escalation rules based on user/company segments
  • Show real names in your inbox instead of “Anonymous”
  • Link conversations and tickets to the right contact record
Identifying users is the second-most-important thing you can do after installing the widget.

identify()

Call identify() to associate the current session with a user:
ha.identify("user_123", {
  name: "Jane Doe",
  email: "[email protected]",
  role: "admin",
  plan: "pro",
  department: "Engineering",
});
userId
string
required
Your system’s unique identifier for this user.
traits
UserTraits
Key-value attributes. See User Traits for recommended fields.
identity
{ userToken?: string }
Optional. Pass a fresh signed identity proof (typically a JWT) at the same time as identify, e.g. when your app refreshes the user’s JWT. The new token is forwarded on every subsequent SDK request, so user-scoped endpoints can re-verify ownership without you reinitializing the widget. See Identity Verification.
Behavior:
  • Traits are merged with any previously set traits (new values overwrite old)
  • The user record is created or updated in the database
  • All subsequent chat messages include these traits
  • If identity is provided, the new userToken replaces the one from init() for all subsequent requests

identifyCompany()

Call identifyCompany() to associate the user with a company:
ha.identifyCompany("company_456", {
  name: "Acme Corp",
  plan: "enterprise",
  industry: "SaaS",
  employee_count: 150,
  space_id: "space_789",
});
companyId
string
required
Your system’s unique identifier for this company.
traits
CompanyTraits
Key-value attributes. See Company Traits.
Behavior:
  • The company record is created or updated
  • The current user is linked to this company
  • Company traits are included in the AI prompt alongside user traits
  • Traits are merged with existing data (new values overwrite)

When to call

Call identify() as soon as you know the user’s identity — typically right after login:
const user = await fetchCurrentUser();

ha.identify(user.id, {
  name: user.name,
  email: user.email,
  role: user.role,
});

if (user.company) {
  ha.identifyCompany(user.company.id, {
    name: user.company.name,
    plan: user.company.plan,
  });
}
You can call identify() and identifyCompany() multiple times. Traits merge additively.

Authenticated apps with delayed first login

If signup and first session are separate (email verification, magic links, invite flows, OAuth onboarding later), calling only identify() from the browser is not enough. Halo creates a contact after Stripe sync, SDK/server identify, or similar. Rows in your product database do not appear in Contacts or automations until you sync them.
EventYour app databaseHalo (widget identify only)
User completes signupUser + team createdNo contact yet
User verifies email and opens the dashboardUser activeWidget runs identify(), contact appears
That gap is expected, not a Halo bug. Fix it by identifying from your signup server handler in addition to the widget on login. Recommendation: In the same code path that persists the new user and team (where you already fire analytics webhooks), call: Use your publishable widget key (Authorization: Bearer ab_live_...). Pass signed_up_at as ISO 8601 from your product’s user created_at. Do not send created_at in traits; Halo sets that when the row is inserted.
// After user + team are saved in your signup API (fire-and-forget)
async function syncNewAccountToHalo(user: { id: string; name: string; email: string; role?: string; createdAt: Date }, team?: { id: string; name: string; createdAt: Date }) {
  const apiKey = process.env.HALOAGENTS_API_KEY;
  if (!apiKey) return;

  const headers = {
    "Content-Type": "application/json",
    Authorization: `Bearer ${apiKey}`,
  };

  try {
    await fetch("https://api.haloagents.ai/api/sdk/users/identify", {
      method: "POST",
      headers,
      body: JSON.stringify({
        user_id: String(user.id),
        traits: {
          name: user.name,
          email: user.email,
          role: user.role ?? "owner",
          signed_up_at: user.createdAt.toISOString(),
        },
      }),
    });

    if (team) {
      await fetch("https://api.haloagents.ai/api/sdk/companies/identify", {
        method: "POST",
        headers,
        body: JSON.stringify({
          company_id: String(team.id),
          user_id: String(user.id),
          traits: {
            name: team.name,
            signed_up_at: team.createdAt.toISOString(),
          },
        }),
      });
    }
  } catch (err) {
    console.error("Halo identify at signup failed", err);
  }
}
Keep widget identify() on every authenticated page load so traits stay fresh. Run a one-time backfill for accounts created during any gap before this shipped. If automations filter on Role or Signed Up, see Automation Troubleshooting.

Identity verification (JWT)

For production, enable identity verification so users can’t impersonate each other. When enabled, every chat call must include a JWT signed with your Identity Secret on your server. The JWT can also carry trusted claims (email, role, plan, company_id) that Halo will use without a database lookup.

How it works

Halo generates two separate keys when you set up your project:
KeyPrefixWhere it goesPurpose
Widget keyab_live_...Frontend (browser)Identify your workspace — publishable, safe to expose
Identity secretha_secret_...Backend (server only)Sign JWTs for identity verification

JWT payload

The JWT must include user_id and may include any additional claims:
{
  "user_id": "user_123",
  "email": "[email protected]",
  "name": "Jane Doe",
  "company_id": "company_456",
  "role": "admin",
  "plan": "pro",
  "exp": 1716242622
}
  • user_idrequired, must match the userId passed to Halo
  • exp — optional but recommended (Unix timestamp)
  • All other claims — optional, used as trusted user data

Server-side: generate the JWT

Sign a JWT with your Identity Secret using HS256:
const jwt = require("jsonwebtoken");

function generateUserToken(user, secretKey) {
  return jwt.sign(
    {
      user_id: user.id,
      email: user.email,
      name: user.name,
      role: user.role,
      company_id: user.companyId,
    },
    secretKey,
    { algorithm: "HS256", expiresIn: "24h" }
  );
}

const token = generateUserToken(currentUser, process.env.HALOAGENTS_SECRET_KEY);

Client-side: pass the token

Pass the JWT as userToken when initializing. Always include userTraits with at least name and email so users show up correctly in your inbox:
<script src="https://cdn.haloagents.ai/sdk/latest/haloagents.umd.js"></script>
<script>
  window.HaloAgents.init({
    orgId: "your-org-id",
    apiKey: "ab_live_xxxxxxxxxxxxxxxx",
    userId: "user_123",
    userToken: "YOUR_JWT_TOKEN",
    userTraits: {
      name: "Jane Doe",
      email: "[email protected]",
    },
  });
</script>
Passing userId and userToken alone is not enough to show user names in your inbox. You must also include userTraits with name and email, or call identify() separately.

Token refresh (SDK 0.9.0+)

Prefer getUserToken to fetch fresh JWTs from your backend when the init token expires:
window.HaloAgents.init({
  orgId: "your-org-id",
  apiKey: "ab_live_xxxxxxxxxxxxxxxx",
  userId: "user_123",
  userToken: initialJwt,
  getUserToken: () =>
    fetch("/api/haloagents-token")
      .then((r) => r.json())
      .then((d) => d.token),
  onIdentityExpired: ({ reason }) => {
    // Refresh your session, then identify() with a new token if needed
    console.warn("Halo identity expired:", reason);
  },
});
You can also rotate manually with identify(userId, traits, { userToken: newToken }). See Identity Verification for the full guide.

Best practices

  1. Always pass name and email in either userTraits (during init) or via identify(). Without them, users show as “Anonymous” in tickets and conversations.
  2. Set exp to match your session length (e.g. 24 hours). Shorter-lived tokens are more secure.
  3. Refresh the JWT with getUserToken or identify(..., { userToken }) when your session refreshes.
  4. Wire onIdentityExpired if you use enforce mode, so stale tokens refresh before users lose chat access.
  5. Include useful claims — the more claims (email, role, plan, company_id), the more personalized the AI without extra database lookups.
The JWT must be signed on your server using the Identity Secret. Never expose the secret in client-side code — anyone could forge tokens and impersonate users.
For a deeper dive on identity verification with full server-side examples, see Identity Verification.

Anonymous users

If you don’t call identify() or pass userTraits, the user is treated as anonymous. The AI still works but:
  • Conversations and tickets show “Anonymous” in your inbox
  • The AI doesn’t have access to user-specific traits or company data
  • Escalation filters and priority rules don’t match (they depend on user/company attributes)
You can still use setContext() to provide session-level context, but for the best experience, always identify users with at least name and email.

Where to go next

Send Context

Push structured state about the user beyond simple traits.

Identity Verification

Production setup with full server-side examples.

User Traits

Recommended fields and the full trait schema.

Automation Troubleshooting

Missing contacts before first login, 0 matched audience filters, and trait gaps.

Company Traits

Recommended fields for companies.