# Identity verification

> Recognize visitors who are signed in to your website, so their chats follow them across devices and your actions can act for them.

When a customer is signed in to your website, your agent can know who they are. Your server signs a short token for the signed-in user, and your page hands it to the chat. Hourzero checks the token and links the visitor to one contact, whatever browser or device they use.

**Where to find it:** open your agent and go to **Playground**. Under **Preview channel**, choose **Chat Bubble**, **Center Stage**, or **Chat Iframe**, select **Publish**, and look for **Identity verification** in the dialog.

## What you get

- **One contact per customer.** Every device where they sign in links to the same contact, with their name, email, and phone in **Contacts**, **Conversations**, and the **Helpdesk**.
- **History that follows them.** A customer who signs in on their phone sees the conversations they had on their laptop.
- **An agent that knows who it's talking to.** It doesn't ask for an email it already has, and forms fill in their details.
- **Actions that act for them.** Your actions can look up *this* customer's orders or balance without asking for an account number. See [Use the signed-in user in actions](#use-the-signed-in-user-in-actions).

Identity verification is included on every plan. It works in Chat Bubble, Center Stage, and [Chat Iframe](https://hourzero.dev/docs/integrations/chat-iframe) installed with **Script**. Chat Iframe installed as **Iframe only**, share links, and messaging apps stay as they are.

## How it works

1. **Generate**: Create the agent's identity secret in the publish dialog.
2. **Sign**: Your server signs a token for the signed-in user with the secret.
3. **Identify**: Your page passes the token to the chat.
4. **Verify**: Hourzero checks the token and links the visitor to their contact.

Your page can't simply tell the chat an email address: anyone could type someone else's email into their browser and read that person's conversations. The signature proves the details came from your server.

## Step 1: Generate the secret

1. In the Playground, open a website channel and select **Publish**, then **Website widget** or **Chat Iframe**.
2. Under **Identity verification**, select **Generate secret**.
3. Copy the secret and store it on your server, for example as `HOURZERO_IDENTITY_SECRET`. Select **I saved it**.

One secret covers all of the agent's website channels. Only workspace owners and admins can generate, reveal, roll, or turn off the secret.

> [!WARNING] Keep the secret on your server
> Anyone with the secret can sign in to your agent's chat as any of your users. Never put it in browser code, a mobile app, or a public repository.

## Step 2: Sign a token on your server

When a signed-in user loads a page with the chat, your server creates a JSON Web Token (JWT) for them, signed with the secret using **HS256**.

| Claim | Required | What it's for |
| --- | --- | --- |
| `user_id` | Yes | Your own ID for this user. `sub` works too. Up to 256 characters. |
| `email` | No | Shown on the contact, and used for forms and tickets. |
| `name` | No | Shown on the contact and to your team. |
| `phonenumber` | No | Shown on the contact. `phone` works too. |
| `custom_attributes` | No | Your own details, such as `{ "plan": "pro", "account_id": "acc_42" }`. Up to 50 keys and 4 KB. |
| `exp` | No | When the token expires. We recommend an hour; keep it under 24 hours. |

Each time a token arrives, the contact is updated: a detail in the token is saved, a detail you leave out stays as it was, and `null` clears it. The same goes for each key in `custom_attributes`.

In Node.js, with the `jose` package:

```js
import { SignJWT } from "jose";

const secret = new TextEncoder().encode(process.env.HOURZERO_IDENTITY_SECRET);

export function hourzeroToken(user) {
  return new SignJWT({
    user_id: user.id,
    email: user.email,
    name: user.name,
    custom_attributes: { plan: user.plan },
  })
    .setProtectedHeader({ alg: "HS256" })
    .setIssuedAt()
    .setExpirationTime("1h")
    .sign(secret);
}
```

In Python, with `PyJWT`:

```python
import os
import time

import jwt

def hourzero_token(user):
    return jwt.encode(
        {
            "user_id": str(user.id),
            "email": user.email,
            "name": user.name,
            "exp": int(time.time()) + 3600,
        },
        os.environ["HOURZERO_IDENTITY_SECRET"],
        algorithm="HS256",
    )
```

Tokens are signed, not encrypted: the visitor can read what's in them. Only include details they may see.

## Step 3: Identify the visitor on your page

The page needs the Hourzero script: the [website widget](https://hourzero.dev/docs/integrations/website-widget) code, or Chat Iframe installed with **Script**. After the user signs in, pass the token from your server:

```js
window.hourzero("identify", { token: "TOKEN_FROM_YOUR_SERVER" });
```

If your page renders the token before the script loads, set it first instead:

```html
<script>
  window.hourzeroUserConfig = { token: "TOKEN_FROM_YOUR_SERVER" };
</script>
```

When the user signs out, reset the chat:

```js
window.hourzero("resetUser");
```

Some things to know:

- One call identifies every Hourzero chat on the page. If the page shows chats from more than one agent, see [Pages with more than one agent](#pages-with-more-than-one-agent).
- Only the token is read. Anything else you pass next to it is ignored, and the browser's developer console shows a warning.
- When a token expires, the chat shows "Your sign-in to this website ended. Refresh the page to keep chatting." Your page can sign the user in again without a reload by listening for the `hourzero:identity-error` event:

```js
window.addEventListener("hourzero:identity-error", async () => {
  const token = await fetch("/api/hourzero-token").then((r) => r.text());
  window.hourzero("identify", { token });
});
```

### Pages with more than one agent

Each agent has its own secret, so a token signed for one agent is refused by another. When a page shows chats from two agents, for example Acme Support's chat bubble and Acme Sales' Chat Iframe, sign a token for each and pass the agent's public ID, the `data-agent-id` from its embed code:

```js
window.hourzero("identify", { token: supportToken, agentId: "acme-support" });
window.hourzero("identify", { token: salesToken, agentId: "acme-sales" });
```

- A token with an `agentId` reaches only that agent's chats, and wins over a token set without one.
- `window.hourzero("resetUser", { agentId: "acme-sales" })` signs out only that agent's chats. `window.hourzero("resetUser")` with no `agentId` signs out every chat on the page.
- `window.hourzeroUserConfig` takes an `agentId` too.

## When visitors sign in and out

- **Signing in mid-chat:** the conversation open in that browser joins their account. Earlier anonymous chats in that browser stay with the browser.
- **Signing out:** after `resetUser`, the chat starts again as a new anonymous visitor, so the next person on a shared computer sees nothing.
- **Switching users:** works like signing out and in.
- **Each channel keeps its own history.** A customer's Chat Bubble conversations don't appear in Chat Iframe, though both belong to the same contact.
- **Each agent keeps its own contacts and secret.** Two agents on the same website don't share contacts.

## Use the signed-in user in actions

### Fill inputs from the signed-in user

In a [custom action](https://hourzero.dev/docs/ai-agent-management/actions/custom-action) that makes a server API request, open a request input and set **Value comes from** to **Signed-in user's ID**, **email**, **name**, **phone**, or **custom attribute**. Hourzero fills in the value itself; the model never sees or changes it, so it can't look up another customer by mistake.

For example, Acme's **Credit balance** action calls `GET https://api.acme.com/users/{user_id}/credits` with its API key in a header, and fills `user_id` from **Signed-in user's ID**. A signed-in customer asks "How many credits do I have?" and gets their own balance.

When a required value isn't known, for example because the visitor isn't signed in, the agent isn't offered the action at all.

### Send the token to your API

To let your API check the user itself, turn on **Send the signed-in user's token** in the action's **Headers**. Requests then carry the token in a `Hourzero-User-Token` header, which your API verifies with the same secret:

```js
import { jwtVerify } from "jose";

const secret = new TextEncoder().encode(process.env.HOURZERO_IDENTITY_SECRET);

export async function signedInUser(request) {
  const token = request.headers.get("hourzero-user-token");
  const { payload } = await jwtVerify(token, secret, { algorithms: ["HS256"] });
  return payload.user_id;
}
```

The header isn't sent for visitors who aren't signed in, and it's dropped if the request redirects to another website.

### Website handlers

Your [website JavaScript handlers](https://hourzero.dev/docs/ai-agent-management/actions/custom-action#website-javascript-handler) receive the signed-in user as their second argument, as in Chatbase: `user_id`, `user_hash` (an HMAC-SHA256 of `user_id` with your secret, in hex), and `user_metadata` with `email`, `name`, `phonenumber`, and `custom_attributes`, next to `anon_user_id`.

## Test it in the Playground

1. In the Playground, open a website channel and select the visitor button next to **Preview**. It says **Anonymous**.
2. Turn on **Signed in** and fill in a **User ID**, and any email, name, phone, or custom attributes.
3. Chat in the preview. The agent treats you as that user, and actions that use the signed-in user run for them.

If the agent has a secret, Hourzero signs a 10-minute token for the test user, so actions that send the token work too. The test user is saved in your browser only.

## Roll or turn off the secret

- **Roll secret** creates a new secret. Choose how long the current one keeps working: immediately, 1 hour, 12 hours, or 24 hours. Deploy the new secret to your server before the old one expires, and signed-in visitors won't notice.
- **Turn off** deletes the secret. Chats that still send a token are refused until you remove the identify call or generate a new secret. Contacts and their conversations stay.

## Moving from Chatbase

The integration is the same. Rename `window.chatbase` to `window.hourzero` and `window.chatbaseUserConfig` to `window.hourzeroUserConfig`, and sign tokens with your Hourzero secret. The `user_id`, `sub`, `email`, `name`, `phonenumber`, and `custom_attributes` claims work unchanged, and `identify` and `resetUser` behave the same way.

Not supported: the older `user_hash` sign-in method, and details passed next to the token instead of inside it. The `stripe_accounts` claim is ignored.

## Troubleshooting

### Visitors are still anonymous

- **Is the script on the page?** Chat Iframe installed as **Iframe only** can't receive a token. Use **Script**.
- **Does the page call identify?** The browser's developer console shows "Hourzero: identify needs { token }" when the call has no valid token.
- **Is identity verification on?** Without a secret, chats that send a token are refused.

### The chat says the sign-in ended

The token expired or wasn't accepted. The console shows "Hourzero: the identity token expired" or "Hourzero: the identity token was rejected". Check that your server signs with this agent's current secret using HS256, then sign a new token and call identify again.

### An action doesn't show up for signed-in visitors

An input that comes from the signed-in user is required and its value is missing. Check that the token includes the email, name, phone, or custom attribute the action needs, and that the attribute key is spelled the same.

## Next steps

- [Custom action](https://hourzero.dev/docs/ai-agent-management/actions/custom-action): Look up the signed-in customer's data in your system.
- [Contacts](https://hourzero.dev/docs/ai-agent-management/contacts): See every customer and their conversations.
- [Website widget](https://hourzero.dev/docs/integrations/website-widget): Add the chat bubble or chat bar to your website.
- [Chat Iframe](https://hourzero.dev/docs/integrations/chat-iframe): Embed a full-size chat inside a page.
