> ## Documentation Index
> Fetch the complete documentation index at: https://docs.helpalive.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Verify HelpAlive users with a signed JWT user token

> Have your server sign each user id, so nobody can use the agent as someone else once Only allow verified users is on.

Verifying users means that your server signs each signed-in user's id into a user token, which proves to HelpAlive who that user is. This page shows your developer how. It is optional and recommended. It is complete on its own, so an AI coding agent can follow it without reading any other page.

Without it, HelpAlive trusts the user id your page sends. This means that someone who edits the page in their browser could send another user's id and read that user's chats. With it, your server signs each id. Once you turn on **Only allow verified users**, nobody can pretend to be someone else. Add it before you open the agent to all your users.

## User verification at a glance

* Your server signs a user token with HS256, keyed on the text of your signing secret.
* Your page passes the token to `identify()` as `userToken`, in place of `userId` and `tenantId`.
* The secret stays on your server, out of your pages, your repository and any prompt for an AI coding agent.
* Once you turn on **Only allow verified users**, HelpAlive refuses every request without a valid user token.
* Setup takes your developer about 15 minutes, and everything is also on **Settings → Setup & API Key**, step **Verify your users**.

## Prerequisites

* HelpAlive is installed and your pages call `identify()`. See [Install the script](/installation).
* A server that knows who is signed in.
* Your developer, about 15 minutes.

## The rules

1. Sign the token on your server, never in the browser.
2. Sign it with HS256, keyed on the text of your signing secret.
3. Put the same id your page passes as `userId` in `sub`, as a string.
4. Put the same id your page passes as `tenantId` in `tenant`, as a string. Leave `tenant` out if your app has no companies or workspaces.
5. Don't add `exp`, `kid` or any other claim.
6. Send the token only to the signed-in user's own page, with `Cache-Control: no-store`.
7. Pass the token to `identify()` as `userToken`, in place of `userId` and `tenantId`.
8. Keep the secret out of your pages, your repository and any prompt for an AI coding agent.
9. Keep `reset()` in your logout code, apart from `identify()`, as before.

## How to set up user verification

<Steps>
  <Step title="Create your signing secret">
    On **Settings → Setup & API Key**, step **Verify your users**, click **Create your signing secret**. Keep it on your server, like a password, wherever you keep other secrets. The code below reads it from `HELPALIVE_SIGNING_SECRET`; the name is up to you.
  </Step>

  <Step title="Make a user token on your server">
    Where your server knows the signed-in user, add these lines. They make the user token, which refers to a short signed text that says who the user is. `user` stands for your app's signed-in user.

    <Tabs>
      <Tab title="Node.js">
        ```javascript theme={null}
        // npm install jsonwebtoken
        import jwt from "jsonwebtoken"

        const userToken = jwt.sign(
          {
            sub: String(user.id), // this person's id in your app
            // Optional: delete the next line if your app has no companies or workspaces.
            tenant: String(user.companyId), // their company or workspace id
          },
          process.env.HELPALIVE_SIGNING_SECRET,
          { algorithm: "HS256" },
        )
        ```

        On a CommonJS server, write `const jwt = require("jsonwebtoken")` in place of the `import` line.
      </Tab>

      <Tab title="Python">
        ```python theme={null}
        # pip install PyJWT
        import os
        import jwt

        user_token = jwt.encode(
            {
                "sub": str(user.id),  # this person's id in your app
                # Optional: delete the next line if your app has no companies or workspaces.
                "tenant": str(user.company_id),  # their company or workspace id
            },
            os.environ["HELPALIVE_SIGNING_SECRET"],
            algorithm="HS256",
        )
        ```
      </Tab>

      <Tab title="Ruby">
        ```ruby theme={null}
        # gem install jwt  (or add gem "jwt" to your Gemfile)
        require "jwt"

        user_token = JWT.encode(
          {
            sub: user.id.to_s, # this person's id in your app
            # Optional: delete the next line if your app has no companies or workspaces.
            tenant: user.company_id.to_s, # their company or workspace id
          },
          ENV.fetch("HELPALIVE_SIGNING_SECRET"),
          "HS256"
        )
        ```
      </Tab>

      <Tab title="PHP">
        ```php theme={null}
        // composer require firebase/php-jwt
        use Firebase\JWT\JWT;

        $userToken = JWT::encode(
            [
                'sub' => (string) $user->id, // this person's id in your app
                // Optional: delete the next line if your app has no companies or workspaces.
                'tenant' => (string) $user->company_id, // their company or workspace id
            ],
            getenv('HELPALIVE_SIGNING_SECRET'),
            'HS256'
        );
        ```
      </Tab>

      <Tab title="Java">
        ```java theme={null}
        // Maven: com.auth0:java-jwt
        import com.auth0.jwt.JWT;
        import com.auth0.jwt.algorithms.Algorithm;

        String userToken = JWT.create()
            .withSubject(String.valueOf(user.getId())) // this person's id in your app
            // Optional: delete the next line if your app has no companies or workspaces.
            .withClaim("tenant", String.valueOf(user.getCompanyId())) // their company or workspace id
            .sign(Algorithm.HMAC256(System.getenv("HELPALIVE_SIGNING_SECRET")));
        ```
      </Tab>

      <Tab title="Go">
        ```go theme={null}
        // go get github.com/golang-jwt/jwt/v5
        import (
        	"os"

        	"github.com/golang-jwt/jwt/v5"
        )

        userToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        	"sub": user.ID, // this person's id in your app, as a string
        	// Optional: delete the next line if your app has no companies or workspaces.
        	"tenant": user.CompanyID, // their company or workspace id, as a string
        }).SignedString([]byte(os.Getenv("HELPALIVE_SIGNING_SECRET")))
        ```
      </Tab>

      <Tab title="C#">
        ```csharp theme={null}
        // dotnet add package Microsoft.IdentityModel.JsonWebTokens
        using System;
        using System.Collections.Generic;
        using System.Text;
        using Microsoft.IdentityModel.JsonWebTokens;
        using Microsoft.IdentityModel.Tokens;

        var secret = Environment.GetEnvironmentVariable("HELPALIVE_SIGNING_SECRET")!;
        var handler = new JsonWebTokenHandler { SetDefaultTimesOnTokenCreation = false };
        var userToken = handler.CreateToken(new SecurityTokenDescriptor
        {
            Claims = new Dictionary<string, object>
            {
                ["sub"] = user.Id.ToString(), // this person's id in your app
                // Optional: delete the next line if your app has no companies or workspaces.
                ["tenant"] = user.CompanyId.ToString(), // their company or workspace id
            },
            SigningCredentials = new SigningCredentials(
                new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)), SecurityAlgorithms.HmacSha256),
        });
        ```
      </Tab>

      <Tab title="Edge / Deno / Bun">
        ```javascript theme={null}
        // npm install jose  (Vercel Edge, Cloudflare Workers, Deno, Bun)
        import { SignJWT } from "jose"

        const userToken = await new SignJWT({
          sub: String(user.id), // this person's id in your app
          // Optional: delete the next line if your app has no companies or workspaces.
          tenant: String(user.companyId), // their company or workspace id
        })
          .setProtectedHeader({ alg: "HS256" })
          // On Cloudflare Workers, read the secret from env instead of process.env
          .sign(new TextEncoder().encode(process.env.HELPALIVE_SIGNING_SECRET))
        ```
      </Tab>
    </Tabs>

    The token is a JWT signed with HS256, keyed on the text of your secret:

    | Claim | What goes in it |
    | - | - |
    | `sub` | The same id your page passes to `identify()` as `userId`, as a string. |
    | `tenant` | Optional: the same id your page passes as `tenantId`, as a string. Leave it out if your app has no companies or workspaces. |

    Leave out an expiry (`exp`) and a key id (`kid`). HelpAlive honors an `exp` if you add one, but a token that expires stops working in a tab left open. Rotating the secret is how you cut old tokens off.
  </Step>

  <Step title="Get the token to the page">
    Send the token only to the page of the user it names. Print it into the page from your server template, or add it to an endpoint your app already has for the current user. Send that response with `Cache-Control: no-store`.
  </Step>

  <Step title="Pass the token to identify()">
    Replace your `identify()` call with this one. The token says who the user is and which company they belong to, so you no longer pass `userId` or `tenantId`.

    ```javascript theme={null}
    // Lets the calls below run before our script has finished loading. Keep it first.
    window.HelpAlive = window.HelpAlive || { q: [] };
    ["identify", "reset", "setConsent", "track"].forEach((m) => {
      if (!HelpAlive[m]) HelpAlive[m] = (...args) => HelpAlive.q.push([m, args]);
    });

    // In place of your identify() call from step 1.
    // The token already says who the person is and which company they belong to.
    HelpAlive.identify({
      userToken,                    // the token your server made for this person
      tenantName: company.name,     // optional: the company's name, shown in your dashboard
      displayName: user.name,       // optional
      email: user.email,            // optional
      role: user.role,              // recommended, helps the agent: e.g. "admin", "editor", "viewer"
      plan: company.plan,           // recommended, helps the agent: e.g. "free", "pro", "enterprise"
      createdAt: user.createdAt,    // optional: signup date, Unix time in seconds
    })
    ```

    If you still pass a `userId` or `tenantId` that differs from the token, the token wins and the browser console shows a warning. A value that isn't a JWT is ignored, with an error in the console. That catches the secret pasted in by mistake.

    Your logout code doesn't change. Keep `reset()` there, apart from `identify()`, never straight after it:

    ```javascript theme={null}
    // In your logout code, and wherever your app ends a session. Until this runs,
    // the browser keeps the person, and the next visitor's events count as theirs.
    HelpAlive.reset()
    ```

    `reset()` also drops the token, so the next user on the same browser never inherits it.
  </Step>

  <Step title="Add the secret to your environment">
    Add `HELPALIVE_SIGNING_SECRET` to your server's environment, with the secret from step 1 as its value. If your project keeps a list of variables, such as `.env.example`, add the name there with no value.
  </Step>
</Steps>

### With Google Tag Manager

If you installed HelpAlive with [Google Tag Manager](/sdk/google-tag-manager), pass the token through the data layer. Create a Data Layer Variable named `helpalive_user_token`, then add these lines:

```javascript theme={null}
// In your dataLayer.push, next to helpalive_user_id:
helpalive_user_token: userToken,   // the token your server made for this person

// In the identify tag, as its first line:
userToken: {{helpalive_user_token}},
```

### With an AI coding agent

The **Verify your users** step has a prompt for Claude Code, Codex, Cursor or GitHub Copilot under **With an AI coding agent**. It makes the token on your server, passes it to `identify()`, and lists `HELPALIVE_SIGNING_SECRET` with no value. You set the value yourself, so the secret never goes into a prompt. The prompt is also on [Install with an AI coding agent](/sdk/ai-coding-agents).

## Check it works

1. Deploy your change, sign in to your app and open the agent.
2. On **Settings → Setup & API Key**, step **Verify your users**, press refresh on **Check your setup**. It looks for a verified sign-in in the last 30 minutes. **Your signing works** means that it found one.
3. The **Verified sign-ins, last 7 days** card shows the share of sign-ins that arrived verified.

If tokens arrive but fail, the card says how many and what to fix:

| The card says | What to do |
| - | - |
| They don't check out | Use the current signing secret, and make the token's `sub` the same id your page passes as `userId`. |
| They had expired | Leave the expiry out of the token. |
| No verified sign-in yet | Your pages don't send a token yet. Check step 4. |

## Only allow verified users

Until you turn this on, everyone can see the agent. Turn it on once every page that calls `identify()` sends a token.

<Steps>
  <Step title="Wait for verified sign-ins">
    The switch unlocks once verified sign-ins arrive. It turns on only after a verified sign-in in the last 30 minutes, so sign in to your app and open the agent first.
  </Step>

  <Step title="Turn on Only allow verified users">
    It sits under the **Verified sign-ins, last 7 days** card. **Recommended** on the switch means that nearly all sign-ins are verified. The switch also says how many sign-ins it would have refused in the last 7 days.
  </Step>

  <Step title="Confirm">
    It asks first: "Hide the agent from anyone we can't verify?" The dialog says how many people in the last 7 days had no token, or a token that had expired or didn't check out. Press **Turn it on anyway**, or **Show me the fix** to open the setup instructions instead.
  </Step>
</Steps>

With it on, HelpAlive refuses every request without a valid user token:

* A page without a valid token gets no chat button, so the user doesn't see the agent.
* If a token expires while the chat is open, HelpAlive calls your `refreshUserToken` function, if you pass one to `identify()`, and sends the request again. If that fails, the user sees "Your session needs a refresh. Reload the page to keep chatting." with a **Reload page** button.
* If an open chat is refused for any other reason, the question box gives way to "Chat isn't available right now. Please try again later." A running assist stops quietly.
* Activity from pages without a token is refused too. Those users stop showing in **Reports**, **Audience** and the setup steps.

While the switch hides the agent from people, step 4 on **Settings → Setup & API Key** says how many couldn't see it in the last 7 days, the main reason, and the fix. We also email your team when it starts. See [Setup & API Key](/dashboard/setup#only-allow-verified-users).

You can turn it off at any time, and requests without a token are served again.

## How to rotate the secret

Click **Rotate** next to your secret and put the new one in `HELPALIVE_SIGNING_SECRET` on your server. Nothing in your code changes.

The previous secret keeps working until you rotate again, so nobody is refused while you roll it out. If a secret leaked, rotate once more after your server uses the new one, and the leaked one stops working. Then put that newest secret on your server too, or the next rotation will cut your server off.

## Why does the card say no verified sign-in when the server signs tokens?

The check moves only when the agent is used. Sign in, open the agent, and press refresh. Then check that the page passes `userToken` to `identify()`, and that the response carrying it isn't cached.

## Why don't some users see the chat button?

Some users don't see the chat button because **Only allow verified users** is on, and their page sent no valid token. Step 4 on **Settings → Setup & API Key** names the main reason and the fix. Check that every page that calls `identify()` sends a token, or turn the switch off while you fix it.

## Why do users see Your session needs a refresh?

Users see "Your session needs a refresh" because their token expired and could not be renewed. Pass `refreshUserToken` to `identify()` so HelpAlive can get a fresh one, or leave the expiry out of the token.

## Why does the console say userToken is not a JWT?

The console says this when the page passed something other than a signed token, often the secret itself. Pass the token your server made, and rotate the secret if it reached a page.

## Next

<CardGroup cols={3}>
  <Card title="Identify users" icon="user-check" href="/sdk/identify">
    Every identify() field.
  </Card>

  <Card title="Security overview" icon="lock" href="/trust/security">
    Public keys, secret keys and sessions.
  </Card>

  <Card title="Troubleshooting" icon="wrench" href="/sdk/troubleshooting">
    The failures that show no error.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.