Endpoint
identify() in the SDK, but you can also call it directly from your backend.
Backfilling or importing existing users? Every
identify call bumps the user’s last_seen to now, so looping it over your user base marks everyone “active today” and corrupts activity data. Use Update / Backfill Users instead, which never touches activity timestamps. See Importing & Backfilling Data.Authentication
Requires your publishable widget key in theAuthorization header:
org_id is derived from the widget key. You do not need to pass it in the request body.
Request Body
Your system’s unique identifier for this user.
User attributes. Recognized fields (
name, email, signed_up_at, renewal_date, renewal_status, contract_term, payment_terms, on_contract, mrr, arr) are stored in dedicated columns. All other fields are stored in custom_fields. See User Traits for recommended fields.Do not send id, external_id, org_id, company_id, created_at, updated_at, first_seen, last_seen, or last_contacted_at. These are managed by Halo and the endpoint returns HTTP 400 if any of them appear in the body. Use signed_up_at to set the date the user signed up in your product. See System-managed fields.Structured context entries for AI consumption. Stored in the
context jsonb column. Merged with existing context (new keys overwrite). See Context Entries.If provided, any orphaned transcripts or tickets from this session (created before the user was identified) are retroactively linked to the user record.
JWT signed with your Identity Secret (HS256). Required when identity verification is enabled for your workspace. The token’s
user_id claim must match the user_id field. See Identity Verification.Recognized Traits
These trait keys are stored in dedicated database columns for filtering and display:| Key | Type | Description |
|---|---|---|
name | string | User’s display name |
email | string | User’s email address |
signed_up_at | string | ISO 8601 date the user signed up in your product. Populates the dedicated end_users.signed_up_at column. |
mrr | number | Monthly recurring revenue in cents (only when MRR mode is “manual” or Stripe is not connected) |
arr | number | Annual recurring revenue in cents (same conditions as MRR) |
renewal_date | string | ISO 8601 date for contract renewal |
contract_term | string | One of: monthly, quarterly, annual, bi_annual |
payment_terms | string | One of: monthly, quarterly, annual, bi_annual |
on_contract | boolean | Whether the user is on a contract |
renewal_status | string | One of: up_for_renewal, in_progress, likely_to_renew, expansion_opportunity, set_to_cancel, at_risk, renewed, lost |
Rejected Traits
These keys trigger HTTP 400 if present in thetraits body. They are managed by Halo and customer-set values would silently shadow the real columns. See System-managed fields for context.
| Key | Why |
|---|---|
id | Halo’s internal primary key. Pass your identifier as user_id instead. |
external_id | Halo’s mirror of the user_id you sent. Use user_id at the top level. |
org_id | Set by Halo from the API key. |
company_id | Set by Halo when you call /api/sdk/companies/identify. Don’t override here. |
created_at | Halo sets this when the row is inserted. Pass signed_up_at if you want to record customer signup. |
updated_at | Maintained automatically by a database trigger. |
first_seen | Maintained by Halo (first time we observed any activity from the user). |
last_seen | Bumped by every identify, event, and message. |
last_contacted_at | Set when your team sends a broadcast, series, reply, or chat. |
Example
timestamptz values serialized as ISO 8601 with microsecond precision and a +00:00 offset.
created_at,updated_at,first_seen,last_seen, andlast_contacted_atare read-only Halo-managed timestamps. Do not echo them back into thetraitsbody on subsequent calls (the endpoint returns HTTP 400 if you do).signed_up_atis customer-writable viatraits.signed_up_aton a follow-up identify call. On update the column usesCOALESCEsemantics, so passingnull(or omitting the key) preserves the existing value. Pass a new ISO 8601 string to overwrite.
Behavior
- If a user with the same
user_idexists within your organization, it is updated - If it doesn’t exist, it is created
- Traits are merged: existing values are preserved, matching keys are overwritten
- Context is merged: existing keys are preserved, new keys are added, matching keys are overwritten
- The combined size of
traitsandcontextmust not exceed 20,000 bytes - When
session_idis provided, orphaned transcripts and tickets from that session are retroactively linked to the user