Skip to main content

Identity verification

identify tells the inbox who a visitor is. Anyone can call it from the browser console, so by default the inbox treats those details as the visitor's own claim. When your server signs the user ID with your site's identity secret, the inbox trusts it: the visitor is marked Verified and all sessions of that user, on any browser or device, share one profile and one chat history.

Before you start​

  • The widget is installed and your users sign in to your website or app.
  • Your backend can compute an HMAC-SHA256. Every mainstream language can.
  • Your role has the chat:manage permission (dashboard) or you have an API key with the chat:write scope.

Unsigned and signed identify​

Unsigned (no hash)Signed (valid hash)
Inbox badgeUnverified: "Name, e-mail and phone were typed by the visitor."Verified: "Identity signed by the website's server."
user_idKept as a claim (claimed_user_id in the visitor's custom data), never trusted.Stored as the visitor's external_id.
Other sessions with the same user_idStay separate.Merged into one visitor and one history.
Customer profileName, email and phone fill a customer profile, but an email or phone that already belongs to a customer who signed in is not linked to this visitor.Linked to the customer by email or phone.
"We replied" emailsNot sent to an email that belongs to a signed-in customer.Sent as usual.

The last two rows stop someone from typing another customer's email to see their history or receive their replies. Agents can still merge customers by hand after checking.

1. Get the identity secret​

Each website has its own identity secret. A secret is created with the site but never shown, so generate one the first time:

  1. Open Support → Channels, pick your website and scroll to Identity secret.
  2. Click Rotate & reveal. The secret appears once, at the top of the page.
  3. Copy it into your backend's environment, for example as HATANG_IDENTITY_SECRET.

You can also rotate it through the REST API with a key that has the chat:write scope. The response holds the new secret:

curl -X POST https://api.hatang.dev/chat/sites/site_123.../identity-secret \
-H "Authorization: Bearer ht_123..."
{ "secret": "..." }
danger

The secret stays on your server. Never put it in page JavaScript, a mobile app bundle or a public plugin setting. The browser and the app only ever receive the computed hash.

2. Compute the hash on your server​

hash = lowercase hex of HMAC-SHA256(key = identity secret, message = user_id)

The result is 64 hex characters. user_id must be exactly the string you pass to identify: convert numeric IDs to strings the same way on both sides and do not add spaces.

import { createHmac } from "node:crypto";

export function hatangHash(userId) {
return createHmac("sha256", process.env.HATANG_IDENTITY_SECRET).update(String(userId)).digest("hex");
}

Return the hash to the browser together with the signed-in user, for example in your /me response or in the server-rendered page.

3. Call identify with the hash​

<script>
window.$hatang = window.$hatang || [];
$hatang.push(["identify", {
user_id: "42",
name: "Nguyen Thu Ha",
hash: "9f2c...e41a",
data: { plan: "pro" }
}]);
</script>

Call it on every page load while the user is signed in, or once after sign-in in a single-page app. Commands pushed before the widget loads are queued, so you can render this right after the snippet.

caution

The field is user_id. The website widget also accepts userId (what @hatang/chat-react and the WordPress plugin send) and turns it into user_id; calls straight to the gateway or the protocol must use user_id, or the hash is ignored and the visitor stays Unverified.

Mobile apps use the same secret and the same hash. Each SDK has its own identify call; see SDKs.

What happens when sessions merge​

When a signed identify carries a user_id that another visitor of the same site already has (from another browser, another device or your app), the gateway:

  1. Updates that existing visitor with the new email, name and phone, and merges data into its custom data (new keys win, old keys stay).
  2. Moves the current device's push registrations to that visitor, so notifications follow the person.
  3. Sends the client a new session token for that visitor. The web widget saves it and reloads itself; native SDKs reconnect with the new token.

From then on the person sees the history of their verified profile wherever they sign in. A conversation started anonymously on this device before sign-in stays with the anonymous session; it is not moved into the verified profile.

If no other visitor has that user_id yet, the current visitor simply gets it as its external_id.

Each identify from the widget, signed or not, also sends the chat.visitor.identified webhook event.

Sign-out: call reset​

When the user signs out, call reset:

$hatang.push(["reset"]);

The old session token stops working, a new anonymous visitor is created and the page reloads. Without reset, the next person on the same browser sees the previous user's conversation.

Rotating the secret​

Click Rotate & reveal again (or call the API) when you suspect a leak, then update your backend. From that moment, hashes made with the old secret are invalid: those identify calls are treated as unsigned. Visitors already merged keep their profile and history.

Custom data​

data in identify, and the setData command, carry key-value pairs that agents see on the visitor's profile: plan, order count, customer number. Each call can carry up to 8 KB of JSON; new keys overwrite old ones and missing keys are kept. Use pushEvent for things that happen over time (viewed the pricing page, added to cart). See JavaScript API.

Profile links take agents from a conversation to the same person in your own tools: a URL template such as https://example.com/admin/users/{{user_id}}, filled for each conversation. Set them up in the dashboard under Customer Support → Profile links (help article) or with PUT /chat/profile-links.

{{user_id}} is why signing matters here. With a valid hash, it is the verified external_id. Without one, it falls back to the user_id the visitor claimed, and agents see the link marked unverified: anyone can type someone else's id into an unsigned identify, so check before acting on what that page shows. Links built from {{data.<key>}} are always marked unverified for the same reason. A link whose variables have no value in a conversation is not shown.

Privacy​

  • Send only what agents need. Never send passwords, card numbers or session tokens in data.
  • A hash does not reveal the secret, but it is a stable proof for one user_id. Do not log it or put it in URLs. To invalidate every hash you have issued, rotate the secret.
  • The email from identify is used for "we replied" emails: when an agent answers a visitor who has left and has no push device, the visitor gets the replies by email with a link back to the chat. See Push notifications.

Troubleshooting​

SymptomCauseFix
The visitor stays UnverifiedA direct protocol or gateway call sends userId instead of user_id.Rename the key (the widget accepts both).
The visitor stays UnverifiedThe hash was computed over a different string (a number vs. a string, extra spaces) or with an old secret.Hash exactly the user_id you send, with the current secret.
Nothing changes after identifyemail is not a valid address, or a value is over its limit, so the whole call is rejected.Validate before sending: user_id up to 200 characters, email up to 320, name up to 200, phone up to 40, data up to 8 KB.
A user sees someone else's chatThe previous user signed out without reset.Call reset on sign-out.

Next steps​

  • JavaScript API: all commands and events.
  • SDKs: identify users in iOS, Android, Flutter and React Native apps.
  • Mobile apps: app keys and how they belong to a website.

View this page as Markdown