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

# Verify logged-in users in AI chat widget

> Sign your website users in to the Centillion widget with a JWT or user hash, so the agent links each chat to the right contact. Set up verification.

When a user signs in to your website, your server can identify them to the
Centillion widget, so you verify logged-in users in the AI chat widget instead
of trusting what they type. The widget links their conversation to the contact
with the same **External ID**, signed JWT fields update that contact, and public
attributes give the agent context for its replies.

## What it does

* Links a signed-in visitor's chat to the contact with the same External ID,
  creating the contact when none exists.
* Accepts a JWT signed with HS256, whose `exp` claim, when present, is at most
  24 hours ahead (include one, so the token stops working), or a legacy
  HMAC-SHA256 user hash.
* Updates the contact's email, name, phone, custom attributes and Stripe
  customer IDs from signed JWT fields.
* Passes public attributes, up to 1,000 characters of JSON, to the agent as
  context.
* Gives actions such as [Stripe billing](https://cntillion.com/docs/customer-data/stripe.md) the verified
  customer, so they act only on that person's records. A Twilio action scoped
  to the customer uses the verified number too; one set to any number does not.

## What you need

* Standard and up. If the workspace loses that plan access, visitors can still
  chat anonymously, but the agent stops loading their verified contact details.
  See [plans](https://cntillion.com/docs/workspaces/billing.md).
* A workspace admin to turn verification on, copy the secrets and rotate them.
* A server that already authenticates your users and can sign a token.
* The website widget installed; see the [website widget](https://cntillion.com/docs/channels/website.md).

If you are new to the product, start with [what Centillion is](https://cntillion.com/docs/introduction.md).

## Set it up

**1. Open Identity**

Open **Channels → Install → Website widget → Set up identity verification**.
You can also open **Settings → Identity**. Choose **Turn on identity
verification** the first time.

**2. Copy the secrets**

The **Identity verification** card shows two secrets, one for each method: a
**Token secret** for JWTs and a **User hash secret** for user hashes. Store
them on your server as `CENTILLION_TOKEN_SECRET` and
`CENTILLION_USER_HASH_SECRET`.

**3. Pick a signing method**

Under **Set it up**, choose **Token (JWT)** or **User hash (legacy)**. The
page shows the server code and the website code for that method.

**4. Sign and identify**

After login, sign a JWT on your server, or generate the user hash if you
chose the legacy method, then pass it to the widget with `identify` or
`centillionUserConfig`, as below.

**5. Choose what the agent may read**

Under **What the agent may read**, select the contact attributes the agent
may use in its answers.

> [!WARNING]
>
> Keep both secrets on your server. Never put them in browser JavaScript, an embed
> snippet, or a public environment variable.

> [!NOTE]
>
> Verification used to have one shared secret, often kept in
> `CENTILLION_IDENTITY_SECRET`. That secret no longer verifies either kind of
> signature. Copy the **Token secret** for JWTs or the **User hash secret** for
> user hashes from **Settings → Identity**, and update your server.

## Use it every day

### Method 1: JWT, recommended

After authenticating the user with your existing login system, sign a token on
your server. Derive the user ID from that authenticated session.

```js theme={null}
import jwt from "jsonwebtoken";

function generateWidgetToken(user) {
  return jwt.sign(
    {
      user_id: String(user.id),
      email: user.email,
      name: user.name,
      phonenumber: user.phone,
      custom_attributes: { plan: user.plan },
      stripe_accounts: [
        { label: "Default account", stripe_id: user.stripeCustomerId }
      ]
    },
    process.env.CENTILLION_TOKEN_SECRET,
    { algorithm: "HS256", expiresIn: "1h" }
  );
}
```

Only `user_id` or `sub` is required. Use a stable string identifier. Numeric
`user_id` values are also accepted and converted to strings. If both are
present, `sub` takes precedence. `phone` is an alias for `phonenumber` and takes
precedence, including when set to `null`.

Expiration is optional, but use a short-lived token and refresh it while the
website session remains valid. When supplied, `exp` must be no more than 24
hours away. Expired tokens, future `nbf` claims, wrong signatures and algorithms
other than HS256 do not authenticate the visitor. Verification allows one minute
of clock skew. JWTs are signed, not encrypted, so keep passwords and other
secrets out of them.

### Identify after login

Return the token from your authenticated backend endpoint, then pass it to the
widget. The endpoint below is part of your website and uses your website
session.

```js theme={null}
const response = await fetch("/api/widget-token");
if (!response.ok) throw new Error("Could not sign in to the widget");
const { token, displayName } = await response.json();

window.Centillion("identify", {
  token,
  name: displayName // Public context visible to the AI agent
});
```

Identification creates or updates the contact even if the widget has not been
opened. The widget verifies the signed identity again for each chat request.
`window.centillion("identify", ...)` is also supported.

### Identify before the script loads

Set the configuration before your normal embed snippet:

```html theme={null}
<script>
  window.centillionUserConfig = {
    token: "SIGNED_TOKEN_FROM_YOUR_SERVER",
    name: "Public display name"
  };
</script>
<script
  src="https://app.cntillion.com/embed.js?v=10"
  data-centillion-agent="YOUR_AGENT_ID"
  defer
></script>
```

`window.centillionSettings` remains supported. If both configurations are set,
`centillionUserConfig` takes precedence. Provide a fresh token on each page load.

If login can finish before the script loads, define this command queue first:

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

### Sign out

Call this when your website signs the user out:

```js theme={null}
window.Centillion("resetUser");
```

This clears the widget identity, public metadata and conversation. Signing in as
another user also starts a new conversation. Refreshing a token for the same
user preserves the current conversation. The existing `logout` and `shutdown`
commands remain supported.

### Contact updates and privacy

JWT fields are private contact data. They are available to configured actions,
including billing actions, and are not automatically added to the AI context. In
**Settings → Identity → What the agent may read**, you can explicitly expose
selected contact attributes. Billing identifiers remain private.

| JWT field | Contact update |
| - | - |
| `email`, `name`, `phonenumber` | Updates the corresponding contact field |
| `custom_attributes` | Updates active attributes defined in your Contacts schema; unknown or invalid values are ignored |
| `stripe_accounts` | Stores the signed account list; the first entry supplies the customer ID for this agent's connected Stripe account |

Omitted fields keep their current values. Pass `null` to clear a field. For
custom attributes, `{"plan": null}` deletes only `plan`; `custom_attributes:
null` clears defined custom attributes while preserving billing IDs.
`stripe_accounts: null` or `[]` clears the billing mapping. Email and phone
values already held by another contact are not reassigned. Attributes are
defined under [Contacts](https://cntillion.com/docs/customer-data/contacts.md), **Manage attributes**.

```js theme={null}
// Keep all existing fields, update plan, and remove billing information.
const claims = {
  user_id: "user_123",
  custom_attributes: { plan: "pro" },
  stripe_accounts: null
};
```

> [!WARNING]
>
> Attributes outside the token are visible to the AI agent. Keep them free of
> confidential information. They provide context and never prove permissions,
> select a billing customer, or update the contact.

Public attributes can be supplied directly beside `token`, or inside
`user_metadata`. Their combined JSON must fit within 1,000 characters. Oversized
or malformed metadata is ignored without discarding a valid identity.

### Method 2: user hash, legacy

For existing integrations, generate an HMAC-SHA256 of the exact user ID on your
server, keyed with the **User hash secret**. Prefer JWTs for new integrations
because they support expiration and contact updates.

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

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

Pass the ID and the 64-character hexadecimal hash to the widget:

```js theme={null}
window.Centillion("identify", {
  user_id: "user_123", // sub is also accepted
  user_hash: userHash,
  user_metadata: { name: "Ana", current_page: "dashboard" }
});
```

The same fields work in `centillionUserConfig`. User hashes verify only the ID.
They match a contact by **External ID**, creating an empty contact when none
exists. Maintain contact details through Contacts or your CRM integration.

Each secret only checks its own kind of signature. A user hash made with the
Token secret is refused, and so is a token signed with the User hash secret.

### Shopify sign-in

For the [Shopify theme embed](https://cntillion.com/docs/channels/shopify.md), open **Channels → Shopify** and
choose **Prepare customer sign-in**. Shopify signs the customer's store identity
and the embed refreshes its short-lived token automatically. This preserves
existing identity secrets. Typing an email in chat does not sign a shopper in.

### Website events

The current embed also supports [event listeners](https://cntillion.com/docs/developer-guides/event-listeners.md)
through `window.Centillion.addEventListener` or
`window.centillion.addEventListener`. These share the same listener registry. A
sign-out or visitor change clears the conversation and stops events from its
pending replies. Runtime display options do not verify a customer's identity.

### Connecting website and WhatsApp support

Use one stable customer ID as the Contact's **External ID**, and store the
customer's phone number on that same Contact. A signed website identity uses
that customer ID. When a WhatsApp conversation becomes a Helpdesk ticket, the
sender's phone number can reuse the existing Contact, preserving its External ID
and linking the customer's tickets across channels.

Phone matching uses an international number including its country code. If
multiple Contacts already have that number, correct the duplicate records before
linking the conversation. Helpdesk does not choose one or merge them.

An anonymous website session stays separate until it has a verified identity.
Sharing a Contact lets support staff see related tickets in **Customer history**
when a person takes over from the [helpdesk inbox](https://cntillion.com/docs/helpdesk.md). It does not copy
previous website transcripts into a new WhatsApp AI conversation.

### Rotating the secrets

**Rotate secrets** replaces both at once: signatures made with the old ones stop
working, and visitors chat anonymously until your server signs with the new
ones. **Turn off** deletes them.

## Limits and good to know

* JWTs must use HS256, and `exp`, when present, can be at most 24 hours away.
  Verification allows one minute of clock skew.
* Public attributes outside the token fit within 1,000 characters of JSON and
  are visible to the agent.
* Billing identifiers are never exposed to the agent.
* The old shared `CENTILLION_IDENTITY_SECRET` no longer verifies anything.
* Rotating replaces both secrets at once; until your server signs with the new
  ones, visitors chat anonymously.

## Questions

### Should I use a JWT or a user hash?

A JWT for anything new. It supports expiration and updates the contact's
details. User hashes verify only the ID and are kept for existing integrations.

### Does the agent see everything in the token?

No. JWT fields are private contact data, available to configured actions. The
agent reads only the attributes you select under **What the agent may read**,
plus public attributes passed outside the token.

### What happens when a visitor isn't signed in?

They chat anonymously. Actions that need a verified customer, such as Stripe
billing, refuse anonymous visitors.

### What happens when my site signs the user out?

Call `window.Centillion("resetUser")`. It clears the widget identity, public
metadata and conversation.

### Can a customer prove who they are by typing their email?

No. Only a signature made on your server verifies a visitor. Typing an email in
chat, on the website or in Shopify, does not sign anyone in.

### What happens if I rotate the secrets?

Both secrets change at once. Signatures made with the old ones stop working, and
visitors chat anonymously until your server signs with the new ones.

## Try it

Turn on identity verification, sign one test user on your server, and open the
widget: the conversation should appear on that user's contact.

> [card] [Start free](https://app.cntillion.com/sign-up?utm_source=docs\&utm_medium=article\&utm_campaign=customer-data-identity-verification)
>
> Start on the Free plan, or take a seven-day trial of a paid plan. Your agent is live on your site in an afternoon.

> [card] [Book a demo](https://cal.com/patrick-turricelli-smgfks/15min?utm_source=docs\&utm_medium=article\&utm_campaign=customer-data-identity-verification)
>
> Fifteen minutes with the team, on your own use case.

Or ask the assistant on this page.

## Related

> [card] [What Centillion is](https://cntillion.com/docs/introduction.md)
>
> How the agent, its knowledge and its channels fit together.

> [card] [Stripe billing](https://cntillion.com/docs/customer-data/stripe.md)
>
> Show signed-in customers their invoices and subscriptions in chat.

> [card] [Website widget](https://cntillion.com/docs/channels/website.md)
>
> Install the chat widget the identity is passed to.


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