# Identity Verification

Identity verification adds cryptographically signed user identity to your chatbot conversations. When enabled, your backend signs a JWT containing the visitor's identity claims (user ID, email, name, plan, role, etc.) and passes it to the widget. Wonderchat verifies the signature on every message, attaches the verified identity to the chat log, and can optionally feed the claims to the LLM for personalization.

Use identity verification when you need to:

* Tie chat conversations to known users in your system
* Personalize responses using verified user attributes (plan, role, tier)
* Gate a private chatbot without relying on session cookies
* Maintain an auditable record of who sent each message

{% hint style="info" %}
Identity verification uses **JWTs signed with HS256 (HMAC-SHA256)**. You hold the signing secret on your backend — Wonderchat holds the same secret to verify signatures. Tokens are never stored.
{% endhint %}

## How it works

1. A user signs in on your site.
2. Your backend signs a short-lived JWT using your Wonderchat signing secret.
3. The JWT is passed to the Wonderchat widget, either via the `data-identity-token` script attribute or via the `chatbotIdentify()` JavaScript SDK.
4. On every message, Wonderchat verifies the JWT signature, extracts the claims, and attaches them to the chat log.
5. If `Inject identity claims into context` is enabled, verified claims are made available to the LLM for personalized responses.

## Prerequisites

* Your chatbot must be set to **Private** before identity verification can be configured.
* Only **workspace admins** can generate or rotate the signing secret.

## Step 1 — Generate your signing secret

1. Open your chatbot in the Wonderchat dashboard.
2. Go to **Edit Chatbot → Security**.
3. Make sure the chatbot is set to **Private**.
4. Scroll to the **Identity Verification** section.
5. Click **Generate Secret**.

Wonderchat generates a 64-character hexadecimal string (256 bits of random entropy). This is your signing secret.

{% hint style="warning" %}
Copy the secret and store it securely on your backend (environment variable, secret manager, etc.). **Never commit it to source control and never expose it to the browser.**
{% endhint %}

You can reveal, copy, regenerate, or remove the secret at any time from the same panel. Regenerating the secret immediately invalidates every token signed with the previous secret — there is no grace period.

## Step 2 — Sign a JWT on your backend

Your backend must sign a JWT with the HS256 algorithm using your signing secret. The JWT must include a `sub` claim (the user's unique identifier). All other claims are optional.

### Supported claims

| Claim         | Type   | Required    | Description                                                                                      |
| ------------- | ------ | ----------- | ------------------------------------------------------------------------------------------------ |
| `sub`         | string | **Yes**     | Unique user identifier (e.g. internal user ID, email)                                            |
| `email`       | string | No          | User's email address                                                                             |
| `name`        | string | No          | Display name — the bot can address the user by name                                              |
| `phoneNumber` | string | No          | User's phone number                                                                              |
| `custom`      | object | No          | Arbitrary string key-value pairs. Each value max 500 characters. Used for plan, role, tier, etc. |
| `exp`         | number | Recommended | Expiration time (Unix timestamp). We recommend **1 hour or less**.                               |
| `iat`         | number | No          | Issued-at time                                                                                   |
| `nbf`         | number | No          | Not-before time                                                                                  |
| `aud`         | string | No          | Audience                                                                                         |

### Example payload

```json
{
  "sub": "user-12345",
  "email": "jane@example.com",
  "name": "Jane Doe",
  "phoneNumber": "+1-555-0123",
  "custom": {
    "plan": "premium",
    "role": "admin",
    "tier": "enterprise"
  },
  "exp": 1682000000,
  "iat": 1681996400
}
```

### Signing examples

{% tabs %}
{% tab title="Node.js" %}

```javascript
const jwt = require("jsonwebtoken");

const token = jwt.sign(
  {
    sub: user.id,
    email: user.email,
    name: user.name,
    phoneNumber: user.phone,
    custom: {
      plan: user.plan,
      role: user.role,
    },
  },
  process.env.WONDERCHAT_SIGNING_SECRET,
  {
    algorithm: "HS256",
    expiresIn: "1h",
  },
);
```

{% endtab %}

{% tab title="Python" %}

```python
import jwt
import os
from datetime import datetime, timedelta, timezone

token = jwt.encode(
    {
        "sub": user.id,
        "email": user.email,
        "name": user.name,
        "phoneNumber": user.phone,
        "custom": {
            "plan": user.plan,
            "role": user.role,
        },
        "exp": datetime.now(timezone.utc) + timedelta(hours=1),
    },
    os.environ["WONDERCHAT_SIGNING_SECRET"],
    algorithm="HS256",
)
```

{% endtab %}

{% tab title="Ruby" %}

```ruby
require "jwt"

payload = {
  sub: user.id,
  email: user.email,
  name: user.name,
  phoneNumber: user.phone,
  custom: {
    plan: user.plan,
    role: user.role,
  },
  exp: Time.now.to_i + 3600,
}

token = JWT.encode(payload, ENV["WONDERCHAT_SIGNING_SECRET"], "HS256")
```

{% endtab %}

{% tab title="PHP" %}

```php
<?php
use Firebase\JWT\JWT;

$payload = [
    "sub"         => $user->id,
    "email"       => $user->email,
    "name"        => $user->name,
    "phoneNumber" => $user->phone,
    "custom"      => [
        "plan" => $user->plan,
        "role" => $user->role,
    ],
    "exp"         => time() + 3600,
];

$token = JWT::encode(
    $payload,
    getenv("WONDERCHAT_SIGNING_SECRET"),
    "HS256",
);
```

{% endtab %}
{% endtabs %}

## Step 3 — Pass the token to the widget

You have two options for passing the signed token to the Wonderchat widget.

### Option A — Script attribute (server-rendered pages)

Inject the token into the script tag when your server renders the page. Best for server-rendered sites where the user's identity is known at page load.

```html
<script
  src="https://app.wonderchat.io/scripts/wonderchat.js"
  data-id="YOUR_CHATBOT_ID"
  data-identity-token="SERVER_GENERATED_JWT"
  defer
></script>
```

### Option B — JavaScript SDK (SPAs and async flows)

Call `chatbotIdentify` after your user signs in. Best for single-page apps, post-login flows, or token refresh cycles.

```javascript
window.wonderchat.chatbotIdentify({
  token: "SERVER_GENERATED_JWT",
  email: "jane@example.com", // optional
  name: "Jane Doe",          // optional
});
```

You can call `chatbotIdentify` multiple times — for example, after a token refresh. The widget re-authenticates immediately without reloading.

{% hint style="info" %}
Both the full widget (`wonderchat.js`) and the lightweight SEO widget (`wonderchat-seo.js`) support identity verification. The SEO widget queues identity calls made before the widget finishes loading, so you can call `chatbotIdentify` at any time.
{% endhint %}

## Step 4 — (Optional) Inject claims into the LLM context

By default, verified claims are recorded on the chat log but are **not** sent to the LLM. To make claims available to the LLM for personalization, enable **Inject custom claims into chatbot context** in the Identity Verification panel.

When enabled, the LLM receives context along these lines:

```
The following additional verified attributes are known about the user:
plan=premium, role=admin, tier=enterprise
```

The user's `name` and `email` are always included when present. Only `custom` claims are gated by this setting.

## Step 5 — (Optional) Test tokens with the Token Debugger

The Identity Verification panel includes a **Token Debugger** that lets you paste a JWT and verify it against your current secret before deploying. The debugger returns:

* The decoded header and claims on success
* A failure reason (invalid signature, expired, malformed, etc.) on failure

Use this to sanity-check your backend signing code without having to test against the live widget.

## Error behavior

If a message arrives with an invalid, expired, or malformed token, Wonderchat rejects the token and falls back to your chatbot's other private-access rules:

1. The user is the chatbot owner
2. The user is a team member on your workspace
3. The user is a member of an allowed Google Workspace group

If none of the fallback checks pass, the chat returns a `NO_PERMISSION` error and the user sees a permission-denied message in the widget.

A token is considered valid only if **all** of the following hold:

* Signature verifies against the current secret
* Token is not expired (with a 60-second clock skew tolerance)
* Token contains a non-empty string `sub` claim

## Rotating the secret

Rotate your secret immediately if you suspect it has leaked:

1. Go to **Edit Chatbot → Security → Identity Verification**.
2. Click **Regenerate Secret**.
3. Copy the new secret to your backend.
4. Deploy.

All tokens signed with the old secret are invalidated the moment you regenerate. Plan a deploy window or expect brief verification failures until your backend picks up the new secret.

## Best practices

* **Keep the secret server-side.** Never ship it to the browser or commit it to version control.
* **Keep tokens short-lived.** One hour or less is recommended. Short expirations limit the blast radius of a leaked token.
* **Issue one token per session.** Sign a fresh token when the user logs in rather than sharing a long-lived token across devices.
* **Rotate on leak.** Regenerating is cheap and invalidates every old token instantly.
* **Always use HTTPS.** HS256 is symmetric — protect the token in transit.

## Where verified identity shows up

When a token is verified successfully, Wonderchat attaches the following fields to the chat log:

* `userId` — from the `sub` claim
* `userEmail` — from the `email` claim
* `userName` — from the `name` claim
* `userPhoneNumber` — from the `phoneNumber` claim
* `customIdentifiers` — from the `custom` object
* `identityVerified` — set to `true`

You can use these fields when exporting conversations or reviewing chat logs in the dashboard to tie each conversation to a verified user in your system.

## FAQ

**Can I use identity verification on a public chatbot?** No — the chatbot must be set to Private. Identity verification is designed to authenticate users accessing a gated chatbot.

**What happens if a user's token expires mid-conversation?** The next message will fail verification. Issue a new token on your backend and call `chatbotIdentify` again with the new token. The widget re-authenticates without reloading.

**Can I verify tokens using a public key (RS256)?** Not currently. Wonderchat uses HS256 (symmetric HMAC-SHA256). If you need asymmetric signing, contact support.

**Do I need to re-verify on every message?** Wonderchat handles this for you — the token is verified on every message server-side. You only need to make sure the token passed to the widget is still valid.

**How do I rotate without downtime?** Wonderchat currently supports one active secret per chatbot. To rotate without disruption, deploy your backend to read the new secret before regenerating in the dashboard, then regenerate and cut over immediately.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.wonderchat.io/setup-guides/adding-your-chatbot-to-your-website/identity-verification.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
