# Identity verification: tell the agent who your signed-in visitors are

> Sign your users' ids on your server with an HMAC secret so intoCHAT knows who is chatting: verified conversations, {{user.*}} placeholders in API actions, and an option to let only signed-in visitors chat.

Identity verification lets your website tell the agent who a signed-in visitor is, in a way nobody can fake. Your server signs the user's id with a secret only you and intoCHAT know, and your page passes the id and the signature to the chat widget.

## What it does

For a verified visitor:

- The conversation shows **Verified:** and the user id in **Activity**, with the name and email your page passed. The CSV export has a **Verified user ID** column, and [webhooks](/en/docs/webhooks) for leads, forms and bookings include an `identity` object.
- [Server-side API actions](/en/docs/api-actions) can use the visitor's id, email, name and metadata as [placeholders](#placeholders-in-server-side-actions), filled in by intoCHAT, never by the AI.
- [Client-side action](/en/docs/client-actions) handlers on your page receive the visitor as `context.user`.
- The AI learns that the visitor is verified and their name, so it can greet them. Nothing else; see [What the AI sees](#what-the-ai-sees).

Optionally, you can let [only verified visitors chat](#only-verified-visitors-can-chat).

Identity verification needs the chat bubble from the [script tag](/en/docs/script-tag), because the identity comes from your page through the [JavaScript SDK](/en/docs/javascript-sdk).

## Set it up

### 1. Generate a secret

1. Open your agent and go to the **Share** tab.
2. On the **Identity verification** card, click **Generate secret**.
3. Copy the secret and store it on your server, for example in an environment variable named `INTOCHAT_IDENTITY_SECRET`.

Only the account owner and admins can generate, reveal or rotate the secret. Editors and viewers see whether one is set, and its last four characters.

> The secret belongs on your server only. Never put it in your page's HTML or JavaScript: anyone who has it can sign any user id and chat as that user.

### 2. Sign the user id on your server

For the signed-in user, compute `userHash`: the HMAC-SHA256 of the user id, keyed with the secret, written as lowercase hexadecimal. Sign exactly the text you will pass as `userId`.

Node.js:

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

const userHash = createHmac("sha256", process.env.INTOCHAT_IDENTITY_SECRET)
  .update(String(user.id))
  .digest("hex");
```

PHP:

```php
<?php
$userHash = hash_hmac('sha256', (string) $user->id, getenv('INTOCHAT_IDENTITY_SECRET'));
```

Python:

```python
import hashlib, hmac, os

user_hash = hmac.new(
    os.environ["INTOCHAT_IDENTITY_SECRET"].encode(),
    str(user.id).encode(),
    hashlib.sha256,
).hexdigest()
```

Render the user id and the hash into the page you send to that user.

### 3. Identify the visitor on the page

After the snippet, call `IntoChat.identify` on every page load while the visitor is signed in:

```html
<script src="https://www.intochat.ai/api/embed.js" data-chatbot-id="YOUR_AGENT_ID"></script>
<script>
  window.IntoChat.identify({
    userId: "4711",
    userHash: "THE_HASH_FROM_YOUR_SERVER",
    name: "Ada Lovelace",         // optional
    email: "ada@example.com",     // optional
    metadata: { plan: "pro" }     // optional
  });
</script>
```

The limits for each field are listed under [identify](/en/docs/javascript-sdk#identify). The widget sends the identity with every message, and intoCHAT's server checks the signature each time.

### 4. Reset on sign-out

When the visitor signs out, call:

```js
IntoChat.reset();
```

This forgets the identity and starts a new chat, so the next person on that browser doesn't see the previous conversation. Identifying a different user starts a new chat by itself as well. A visitor who chatted before signing in keeps that conversation, which then becomes verified.

## Only verified visitors can chat

Switch on **Only verified visitors can chat** on the same card to keep the chat for signed-in users:

- Until your page identifies the visitor with a valid signature, the widget shows a short notice asking them to sign in, in the widget's language, instead of the message field.
- The chat API refuses messages without a valid identity, with the reason `identity_required`, so the rule also holds outside the widget.
- An [inline iframe, a share link](/en/docs/iframe-and-link) and the [Help Page](/en/docs/help-page) can't identify anyone, so they only show the notice. [Preview links](/en/docs/preview-links) for prospects keep working: they have their own chat.
- Your own [Playground](/en/docs/instructions#test-in-the-playground) keeps working, so you can still test the agent.

The switch needs a secret, so generate one first. Editors can turn it on or off.

## Placeholders in server-side actions

A [server-side API action](/en/docs/api-actions) can use these placeholders anywhere in its URL, parameters, headers and body:

| Placeholder | Filled with |
| --- | --- |
| `{{user.id}}` | The verified user id |
| `{{user.email}}` | The email your page passed |
| `{{user.name}}` | The name your page passed |
| `{{user.metadata.<key>}}` | One metadata value, for example `{{user.metadata.plan}}` |

```text
GET https://api.example.com/customers/{{user.id}}/orders
```

intoCHAT fills them in on its servers, from the verified identity only:

- For a visitor who isn't verified, they are empty. An empty parameter or body field is left out of the request.
- The AI never sees them and can't choose or change them. Even if the AI passes a value that looks like `{{user.id}}`, it is sent as plain text.
- **Send test request** in the editor has no visitor, so they are empty there too.
- A placeholder that is only `{{user.metadata.seats}}` in the body keeps the value's type, so a number stays a number.

Any other `{{user.…}}` placeholder, such as `{{user.phone}}`, can't be saved. In a [client-side action](/en/docs/client-actions), whose request is built in the visitor's browser, these placeholders stay empty; your handler gets the visitor as `context.user` instead.

> Only the user id is signed. The name, email and metadata are what your page passed alongside a valid signature, so a signed-in visitor could change their own copy in the browser. Look up anything that matters, such as orders or permissions, by `{{user.id}}` on your side.

## Verified visitors become contacts

Each verified visitor is kept as a [contact](/en/docs/contacts) of the agent, with their user id as **External ID**, the name and email your page passed, and metadata keys that are valid attribute names (lower-case letters, digits and underscores, starting with a letter) as attributes. When your page passes new details, the contact follows. The contact is found by the user id only: a verified visitor is never merged into another contact because the emails match, and gets the email only if no other contact has it. Server-side actions can then read the contact, including attributes you imported or added through the API, as `{{contact.*}}` placeholders. Visitors from the Playground's **Test as a signed-in visitor** don't become contacts.

## What the AI sees

The AI is told that the visitor is signed in and verified, and the name your page passed, which it may use to greet them. It is never given the email address, the user id or the metadata, so it can't repeat them or be talked into revealing them. If you want the agent to know more, write it into your [instructions](/en/docs/instructions) yourself.

## Test in the Playground

To try the placeholders or the agent's greeting without signing in on your site, open the **Playground** and expand **Test as a signed-in visitor** above the chat. Enter a **User ID** and, if you like, a **Name**, an **Email** and **Metadata** as a JSON object, for example `{"plan": "pro", "seats": 5}`. The same limits apply as on your site; see [Limits](#limits). Until something is fixed, a red message says what, and the Playground chats as an anonymous visitor.

With a user ID filled in, your Playground messages count as verified:

- [Server-side actions](#placeholders-in-server-side-actions) get these values as `{{user.*}}` placeholders.
- The AI is told the visitor is verified and their name, as described in [What the AI sees](#what-the-ai-sees).
- The Playground conversation records the user, so it shows **Verified** on the Conversations tab.

No signature is needed, because you are signed in to intoCHAT. The test identity is accepted only from the Playground in your dashboard, from the owner, an admin or an editor; viewers can't use it, and the chat widget, links and the REST API ignore it. Visitors on your site always need a valid signature. The Playground is never refused by [Only verified visitors can chat](#only-verified-visitors-can-chat), with or without a test identity.

A conversation keeps the first verified user it sees, so press **Reset** before testing as a different user.

## The secret

- **Reveal** shows the current secret again, to the owner and admins.
- **Rotate** creates a new secret and confirms first. The old one stops working at once: until your server signs with the new secret, every visitor counts as unverified, and with **Only verified visitors can chat** on, they can't chat.
- The secret is stored encrypted. Duplicating an agent doesn't copy it or the switch.

## Limits

- One secret per agent. With several agents on one page, each verifies only signatures made with its own secret.
- A wrong or missing signature is never shown to the visitor as an error: they simply chat unverified, unless only verified visitors may chat.
- A conversation belongs to the first user verified in it. If a different verified user writes on the same session, without the new chat that `identify` normally starts, those messages are treated as unverified.
- A user id has up to 200 characters, a name up to 100, an email up to 254, and metadata up to 20 keys and 2 KB.
- The identity isn't checked when a visitor reopens an earlier chat from their browser's list; [Reset on sign-out](#4-reset-on-sign-out) clears that list.
