# Verify signed-in users

> Keep signed-in customers' chats on every device, and tell the AI agent who they are.

When someone is signed in to your site, the widget can know who they are. Your server signs a short-lived token for them, and the widget sends it with every chat request. Then:

- Their conversations follow them to every device and browser where they're signed in.
- Nobody else can open their conversations.
- The AI agent gets their name and email as facts you vouch for, not as text anyone could type.
- In **Inbox**, their conversations show their name with a **Verified** tag.

Visitors who aren't signed in keep chatting anonymously, as before.

## What you need

- The deployment's **Secret key**, under **Parameters** on its **Install** tab. Select the eye icon to reveal it. Keep it on your server: anyone who has it can sign in as any of your users.
- An endpoint on your server that only a signed-in user can reach, such as `GET /api/help-ai-token`, which returns their token as plain text.
- Widget 1.10.0 or later. The **Install** tab's snippets load a version that has it.

The **Install** tab's **Verify signed-in users** card has these steps with the same snippets, ready to copy. It isn't shown when **Embed in an iframe** is on.

## 1. Sign a token on your server

The token is a JWT signed with HS256 and the deployment's secret key. No other algorithm is accepted.

| Claim | What it is |
| --- | --- |
| `sub` | Required. Your own id for the user, as text, up to 255 characters |
| `exp` | Required. When it expires, in seconds since 1970 (not milliseconds). There's no upper limit, but keep it to an hour: the widget fetches a new one when it runs out |
| `name` | Optional. Their name, up to 200 characters, for your team and the AI agent |
| `email` | Optional. A valid email address |

Leave out a claim you don't have. A `null` or empty `name` or `email` makes the whole token invalid. Tokens longer than 4,096 characters are refused.

### Node

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

// Serve it from an endpoint only your signed-in
// user reaches, e.g. GET /api/help-ai-token
export function signHelpAiToken(user) {
  const secret = new TextEncoder().encode(
    process.env.HELPAI_SECRET_KEY,
  );
  // Leave out what you don't have: an empty
  // name or email makes the token invalid.
  const claims = {};
  if (user.name) claims.name = user.name;
  if (user.email) claims.email = user.email;
  return new SignJWT(claims)
    .setProtectedHeader({ alg: 'HS256' })
    .setSubject(String(user.id))
    .setExpirationTime('1h')
    .sign(secret);
}
```

### Python

```python
import os, time
import jwt  # PyJWT

# Serve it from an endpoint only your signed-in
# user reaches, e.g. GET /api/help-ai-token
def sign_help_ai_token(user):
    claims = {
        "sub": str(user.id),
        "exp": int(time.time()) + 3600,  # an hour
    }
    # Leave out what you don't have: an empty
    # name or email makes the token invalid.
    if user.name:
        claims["name"] = user.name
    if user.email:
        claims["email"] = user.email
    secret = os.environ["HELPAI_SECRET_KEY"]
    return jwt.encode(claims, secret, algorithm="HS256")
```

### PHP

```php
<?php
use Firebase\JWT\JWT; // firebase/php-jwt

// Serve it from an endpoint only your signed-in
// user reaches, e.g. GET /api/help-ai-token
function signHelpAiToken($user): string
{
    $claims = [
        'sub' => (string) $user->id,
        'exp' => time() + 3600, // an hour
    ];
    // Leave out what you don't have: an empty
    // name or email makes the token invalid.
    if (!empty($user->name)) {
        $claims['name'] = $user->name;
    }
    if (!empty($user->email)) {
        $claims['email'] = $user->email;
    }
    $secret = getenv('HELPAI_SECRET_KEY');
    return JWT::encode($claims, $secret, 'HS256');
}
```

## 2. Give the widget the token

Give the widget a function that fetches the token, not the token itself. The widget calls it when it first needs a token, and again when help.ai turns a token down, for example once it has expired. If your function fails, the widget carries on as an anonymous visitor.

With the **Quick install** script tag, call `login` on each page while your user is signed in. The first line lets the call run before the widget's script, for example inline in your page; it then waits for the widget:

```js
window.HelpAI = window.HelpAI || function () { (HelpAI.q = HelpAI.q || []).push(arguments); };

HelpAI('login', () =>
  fetch('/api/help-ai-token').then((res) => res.text()),
);
```

With another install, where `getToken` is that same function:

- **`<web-ai-chat>` element:** `document.querySelector('web-ai-chat').login(getToken)`
- **NPM package:** pass `userToken: getToken` to `init()`, or call `chat.login(getToken)` after sign-in.
- **React:** `<HelpAI publicKey="…" aiAgentDeploymentId="…" userToken={getToken} />`

Pass the same function each time for the same user. Calling `login` again with a different function, for example a new arrow function on every route change of a single-page app, counts as a different user and starts the widget over. In React, define `getToken` outside your component, or with `useCallback` keyed on your user's id.

You can also pass the token itself, as text. The widget can't refresh it then: once it expires, chat requests fail until you call `login` again.

## 3. Sign them out

When your user signs out, start the widget over so the next person on that computer sees nothing of theirs:

```js
HelpAI('reset');
```

Call it while the widget is on the page, for example in your sign-out button's click handler. On `<web-ai-chat>` or an NPM install it's `chat.reset()`; in React, drop the `userToken` prop. Their conversations stay with their account for the next time they sign in.

## What your team and the agent see

- In **Inbox**, the conversation shows their name, or else their email, or else your `sub` id, with a **Verified** tag. Hover over the tag to see the id they signed in with.
- The AI agent gets their id, name and email as verified facts, never as instructions.

## Good to know

- Conversations a visitor started on this browser before signing in move to their account when they sign in.
- Signing in as a different user starts fresh, the same as signing out first.
- What the AI agent remembers from their earlier conversations follows them too, on any device. The next person on a shared browser starts without it.
- To give the agent more about them, such as their plan, see [Tell the AI agent about your visitor and page](https://docs.help.ai/customize-and-extend-the-widget/pass-visitor-and-page-details). Those details aren't verified.

## Troubleshooting

- **Requests fail with 401 `USER_TOKEN_INVALID`:** the token is malformed, signed with another key or algorithm, expired, or has a `name` or `email` that's empty, `null` or not valid. Each deployment has its own secret key. The widget asks your function for a fresh token and tries once more. A token that's turned down doesn't fall back to an anonymous chat: requests fail until your function returns a valid one.
- **New tokens fail straight away:** check your server's clock. There's no leeway, so if you add `iat` or `nbf`, a clock that runs ahead makes the token look like it comes from the future.
- **Chats don't follow the user to another device:** `sub` must be the same id for the same user everywhere, such as your database id.
- **The token works in a test but not on your site:** check that your endpoint returns only the token, as plain text, to the signed-in user, not a login page, JSON or the token in quotes.
- **`HelpAI is not defined`:** your `login` call runs before the widget's script. Add the first line from step 2.
