> For the complete documentation index, see [llms.txt](https://docs.klink.cloud/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.klink.cloud/getting-started/setup-channels/live-chat/identity-verification-secure-user-login.md).

# Identity Verification (Secure User Login)

Identity verification proves that the `authUser` passed to the K-Link chat widget really is the person your website says it is. Without it, anyone who opens the widget could claim any email address and read that user's conversation history.

It works the same way as Intercom's identity verification: your **server** computes an HMAC-SHA256 hash of the user's identity using a secret only you and K-Link know, and passes it to the widget as `userHash`. The chat backend recomputes the hash and only honors the identity when it matches. The secret never appears in the browser, so visitors cannot forge a hash for someone else's identity.

### Choosing the identity value

The identity is whatever stable, unique value identifies the user in your system — a customer id, email, phone number, or any other unique id. Pass it in the `identity` field. If you don't set `identity`, the widget uses `email` as the identity, so existing email-based integrations keep working unchanged.

**The one rule that matters:** the `userHash` must sign the exact value that serves as the identity — i.e. `identity` if you send it, otherwise `email`. That same value is what the backend uses to recognize a returning user, so pick one identity strategy per channel and stay with it. `email`/`phone` you also pass are stored as profile attributes for display; only the identity value is signed and deduplicated on.

### 1. Generate the channel's identity secret

Use the K-Link public API with your existing API token (`kpt_…`):

```bash
curl -X POST "https://apigw.klinkcx.com/api/v1/webchat/channels/<channelId>/identity-verification/secret" \
  -H "Authorization: Bearer kpt_xxxxxxxx"
# => { "requestId": "…", "secret": "9f2c4a…64 hex chars…" }
```

`<channelId>` is your klink.cloud webchat channel id — the same value passed to the widget as `scId`. The API only accepts channels that belong to your tenant.

The plaintext secret is returned only by this call. Store it in your server's secret manager — never in frontend code, HTML, or public repos. Calling the endpoint again rotates the secret (the old one stops working immediately).

### 2. Compute `userHash` on your server

Hash the **exact identity string** you pass to the widget — the value of `identity` (or `email` if you don't send `identity`). The comparison is byte-for-byte, so watch out for casing/whitespace differences. In the examples below `identity` is the customer id you pass as `authUser.identity`.

Node.js:

```js
const crypto = require("crypto");

const userHash = crypto
  .createHmac("sha256", process.env.KLINK_IDENTITY_SECRET)
  .update(identity) // e.g. user.id — or user.email if that's your identity
  .digest("hex");
```

PHP:

```php
$userHash = hash_hmac('sha256', $identity, getenv('KLINK_IDENTITY_SECRET'));
```

Python:

```python
import hashlib, hmac, os

user_hash = hmac.new(
    os.environ["KLINK_IDENTITY_SECRET"].encode(),
    identity.encode(),
    hashlib.sha256,
).hexdigest()
```

### 3. Pass it to the widget

Render the hash into the page for the logged-in user. Identify by any stable unique value via `identity`:

```html
<script>
  klinkChatSDK.run({
    serverUrl: "https://platform.klinkcx.com",
    scId: "<your-channel-id>",
    authUser: {
      name: "Jane Doe",
      identity: "cust_9931", // your stable user id; the value the hash signs
      email: "jane@example.com", // optional profile attribute
      phone: "+95912345678", // optional profile attribute
      userHash: "<hmac-of-identity-computed-on-your-server>",
    },
  });
</script>
```

If you identify users by email, omit `identity` and sign the email instead — the existing behavior:

```html
<script>
  klinkChatSDK.run({
    serverUrl: "https://platform.klinkcx.com",
    scId: "<your-channel-id>",
    authUser: {
      name: "Jane Doe",
      email: "jane@example.com", // used as the identity; sign this value
      userHash: "<hmac-of-email-computed-on-your-server>",
    },
  });
</script>
```

Anonymous visitors (no login) simply omit `authUser` — nothing changes for them.

### 4. Enable enforcement

While enforcement is off, identities without a `userHash` are still accepted (legacy behavior), but a **provided** hash is always validated — deploy step 3, watch the browser console for `[KLinkChat]` warnings, and once clean, turn enforcement on:

```bash
curl -X PUT "https://apigw.klinkcx.com/api/v1/webchat/channels/<channelId>/identity-verification" \
  -H "Authorization: Bearer kpt_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "enforce": true }'
```

From then on, any `authUser` without a valid `userHash` is rejected: the visitor is treated as anonymous, the server logs the rejection, and the widget prints the reason to the browser console. Check status at any time:

```bash
curl "https://apigw.klinkcx.com/api/v1/webchat/channels/<channelId>/identity-verification" \
  -H "Authorization: Bearer kpt_xxxxxxxx"
# => { "requestId": "…", "secretConfigured": true, "enforceIdentityVerification": true }
```

### Behavior summary

| Channel state               | `authUser` without hash | `authUser` with valid hash      | `authUser` with invalid hash    |
| --------------------------- | ----------------------- | ------------------------------- | ------------------------------- |
| No secret configured        | trusted (legacy)        | trusted (legacy, cannot verify) | trusted (legacy, cannot verify) |
| Secret set, enforcement off | trusted (legacy)        | verified ✓                      | rejected → anonymous            |
| Secret set, enforcement on  | rejected → anonymous    | verified ✓                      | rejected → anonymous            |

Here "hash" means a `userHash` that correctly signs the identity value. A hash that signs a different value (e.g. the email when `identity` is set) is treated as invalid.

Additional guarantees, regardless of enforcement:

* A user record that was ever verified can only be matched or updated by another verified claim — an unverified claim can never take over a verified user's conversation history or overwrite their profile.
* Message sender identity is stamped server-side from the registered session; a client cannot send messages as someone else.
* Switching a channel's identity strategy (e.g. from email to a customer id) keys returning users differently, so the same person may start a fresh record. Pick one identity value per channel and keep it stable.

### Notes

* Rotating a secret invalidates hashes computed with the old one. Deploy the new secret to your server at the same time; sessions registered in between fall back to anonymous rather than breaking the widget.
* The API endpoints use the same `kpt_` API token you already use for the K-Link public API, and only accept webchat channels that belong to your account.
