# 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](https://hatang.dev/docs/guides/support/web-widget/install.md) 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 badge | **Unverified**: "Name, e-mail and phone were typed by the visitor." | **Verified**: "Identity signed by the website's server." |
| `user_id` | Kept 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_id` | Stay separate. | Merged into one visitor and one history. |
| Customer profile | Name, 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" emails | Not 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](https://hatang.dev/docs/reference/api.md) with a key that has the `chat:write` scope. The response holds the new secret:

**cURL**

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

**Node.js**

```js
const res = await fetch("https://api.hatang.dev/chat/sites/site_123.../identity-secret", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.HATANG_API_KEY}` },
});
const { secret } = await res.json();
```

**Python**

```python
import os, requests

res = requests.post(
    "https://api.hatang.dev/chat/sites/site_123.../identity-secret",
    headers={"Authorization": f"Bearer {os.environ['HATANG_API_KEY']}"},
)
secret = res.json()["secret"]
```

**PHP**

```php
$client = new GuzzleHttp\Client();
$res = $client->post('https://api.hatang.dev/chat/sites/site_123.../identity-secret', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('HATANG_API_KEY')],
]);
$secret = json_decode((string) $res->getBody(), true)['secret'];
```

```json
{ "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

```text
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.

**Node.js**

```js
import { createHmac } from "node:crypto";

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

**Python**

```python
import os
from hatang import identity_hash  # pip install hatang

def hatang_hash(user_id) -> str:
    return identity_hash(str(user_id), os.environ["HATANG_IDENTITY_SECRET"])
```

**PHP**

```php
$hash = hash_hmac('sha256', (string) $userId, getenv('HATANG_IDENTITY_SECRET'));
```

**Go**

```go
import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"os"
)

func hatangHash(userID string) string {
	m := hmac.New(sha256.New, []byte(os.Getenv("HATANG_IDENTITY_SECRET")))
	m.Write([]byte(userID))
	return hex.EncodeToString(m.Sum(nil))
}
```

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

```html
<script>
  window.$hatang = window.$hatang || [];
  $hatang.push(["identify", {
    user_id: "42",
    email: "ha@example.com",
    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](https://hatang.dev/docs/guides/sdks/overview.md).

## 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](https://hatang.dev/docs/guides/getting-started/webhooks.md) event.

## Sign-out: call reset

When the user signs out, call `reset`:

```js
$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](https://hatang.dev/docs/guides/support/web-widget/javascript-api.md).

## Profile links

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](https://hatang.dev/help/inbox/profile-links)) or with [`PUT /chat/profile-links`](https://hatang.dev/docs/guides/support/rest-api.md#6-link-agents-to-your-admin).

`{{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](https://hatang.dev/docs/guides/sdks/push-notifications.md).

## Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| The visitor stays **Unverified** | A direct protocol or gateway call sends `userId` instead of `user_id`. | Rename the key (the widget accepts both). |
| The visitor stays **Unverified** | The 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 `identify` | `email` 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 chat | The previous user signed out without `reset`. | Call `reset` on sign-out. |

## Next steps

- [JavaScript API](https://hatang.dev/docs/guides/support/web-widget/javascript-api.md): all commands and events.
- [SDKs](https://hatang.dev/docs/guides/sdks/overview.md): identify users in iOS, Android, Flutter and React Native apps.
- [Mobile apps](https://hatang.dev/docs/guides/support/mobile-apps.md): app keys and how they belong to a website.

Source: https://hatang.dev/docs/guides/support/web-widget/identity-verification
