# Identity verification

> Securely tell the agent who the signed-in visitor is from the embedded widget. Sign a JWT on your server with HS256, call window.bookbag("identify", { token }), and the agent recognizes the customer, auto-syncs a contact, and personalizes replies.

If your visitors are already signed in to your site, you can tell the embedded agent who they are. To stop anyone impersonating a user, you sign the visitor's identity on your **server** and the widget forwards that signed proof with every message. Bookbag verifies it, recognizes the customer, syncs a [contact](/docs/contacts/overview), and personalizes the conversation.

> **TWO METHODS — USE JWT:** The recommended method is a signed **JWT** (below): one token carries the user id plus profile claims and an expiry. The older **user-hash** method (further down) is still supported for Chatbase-style setups, but JWT is more capable and is what new integrations should use.

> **NEVER SIGN IN THE BROWSER:** The token (or hash) must be generated on your server with your agent's verification secret. If you put the secret in client-side JavaScript, anyone can read it and impersonate any user.

## JWT method (recommended)

Your server signs a **JSON Web Token** with **HS256** using the agent's **verification secret**, and your page hands that token to the widget. The token carries the user id and any profile claims you want synced; the widget never sees the secret.

### Your verification secret

Each agent has its own verification secret. It's the same secret shown under **Deploy → Chat widget → Embed** (Identity verification), and the dashboard can fetch it for you. Store it as a server-side environment variable, and **Rotate** it from the same screen if it's ever exposed (the old secret stops working immediately).

### Token claims

Sign a payload with these claims. Either `user_id` or `sub` is required — it's the stable id Bookbag keys the contact on. Everything else is optional.

| Claim | Required | What it does |
| --- | --- | --- |
| `user_id` *or* `sub` | Yes | The user's stable id. Becomes the contact's `external_id`. |
| `email` | No | Synced to the contact and visible to the agent. |
| `name` | No | Synced to the contact and visible to the agent. |
| `phonenumber` | No | Synced to the contact. |
| `custom_attributes` | No | An object of extra fields; **merged** into the contact's [custom attributes](/docs/contacts/custom-attributes). |
| `stripe_accounts` | No | An array stored for actions (e.g. Stripe billing) — **never exposed to the model**. |
| `exp` | Recommended | Standard JWT expiry. Keep it short (≤ 24h). |

> **SENSITIVE CLAIMS STAY OUT OF THE MODEL'S CONTEXT:** Claims like `stripe_accounts` are stored against the contact for actions to use, but are **not** put into the agent's context — so the model can't read or leak them. Name/email/phone are synced and the agent can use them to personalize.

### 1. Sign the token on your server

```javascript
const jwt = require("jsonwebtoken");
const secret = process.env.BOOKBAG_SECRET; // your agent's verification secret

const token = jwt.sign(
  {
    user_id: String(currentUser.id),   // or `sub`
    email: currentUser.email,
    name: currentUser.name,
    phonenumber: currentUser.phone,
    custom_attributes: { plan: "growth", lifetime_value: 4200 },
    stripe_accounts: [currentUser.stripeCustomerId], // hidden from the model
  },
  secret,
  { algorithm: "HS256", expiresIn: "1h" } // sets `exp`
);
```

### 2. Identify the user in the browser

Render the server-signed token into the page and pass it to `identify`. Any fields you pass **outside** the token (like `name`, `age`) are also given to the chatbot as context — use them for non-sensitive personalization only.

```javascript
window.bookbag("identify", {
  token,            // the JWT from your server
  name: "Jane Doe", // outside the token → visible to the chatbot
  age: 34,          // outside the token → visible to the chatbot
});
```

> **PRE-LOAD BEFORE THE SCRIPT:** To identify the visitor before the embed even finishes loading, set the token first and the widget picks it up on boot:

```html
<script>
  window.bookbagUserConfig = { token: "THE_JWT_FROM_YOUR_SERVER" };
</script>
<script src="https://app.bookbag.ai/widget/embed" data-agent-id="123" defer></script>
```

### Log out

When the visitor signs out, clear the identity so the agent stops treating them as that user:

```javascript
window.bookbag("resetUser");
```

### Contact sync semantics

- **Upsert by id** — Bookbag finds or creates the [contact](/docs/contacts/overview) whose `external_id` equals the token's `user_id`/`sub`, and fills name/email/phone from the claims. Return visits update the same record.
- **Custom attributes merge** — `custom_attributes` are merged into the contact's [custom attributes](/docs/contacts/custom-attributes), not replaced.
- **Stripe accounts stored, not exposed** — `stripe_accounts` are saved for actions (e.g. Stripe billing actions) but kept out of the agent's context.

### What the server rejects

Verification fails — and, when **Require verified identity** is on, the chat is refused with a **401** — for any of:

- **Wrong algorithm** — the token must be signed with **HS256**; other algorithms are rejected.
- **Bad signature** — signed with the wrong secret or tampered with.
- **Expired** — `exp` is in the past.
- **Not yet valid** — `nbf` is in the future.

> **TURN ON "REQUIRE VERIFIED IDENTITY" ONLY AFTER WIRING IDENTIFY():** With it on, unverified, forged, or expired tokens get a 401 and the chat is blocked. Ship `identify()` first, confirm it works, then require it.

## User-hash method (legacy)

> **PREFER JWT FOR NEW WORK:** The user-hash method below predates JWT and exists for Chatbase-style setups. It still works, but JWT carries more (profile claims, expiry, Stripe accounts) in a single signed token — use it for new integrations.

Instead of a token, you sign just the user id with an HMAC and send the id, hash, and metadata separately. Bookbag verifies the hash and trusts the identity.

### How it works

- On your server, compute `user_hash = HMAC-SHA256(user_id)` using your agent's **signing secret**.
- On the page, call `window.bookbag('identify', { user_id, user_hash, user_metadata })`.
- The widget attaches `user_id`, `user_hash`, and `user_metadata` to every chat request; Bookbag verifies the hash and trusts the identity.

### Your signing secret

Each agent has its own signing secret. Open your agent → **Chat Interface → Embed → Identity verification**, click **Reveal**, and copy it. You can **Rotate** it at any time (the old secret stops working immediately). Store it as a server-side environment variable.

### 1. Sign the user id on your server

```javascript
const crypto = require("crypto");
const secret = process.env.BOOKBAG_SECRET; // your agent's signing secret
const userId = String(currentUser.id);
const userHash = crypto.createHmac("sha256", secret).update(userId).digest("hex");
```

### 2. Identify the user in the browser

Render the server-computed values into the page, then call `identify`:

```javascript
window.bookbag("identify", {
  user_id: "user-123",
  user_hash: "the-hash-from-your-server",
  user_metadata: {
    name: "Jane Doe",
    email: "jane@example.com",
    plan: "growth"   // any extra keys become contact custom attributes
  }
});
```

> **RE-IDENTIFY ON SIGN IN/OUT:** Call `identify` again after the user signs in or out. The widget always uses the most recent identity, and clears it if you call `identify(null)`.

## What identity unlocks

However you identify a visitor — JWT or user-hash — a verified identity gives you:

- **Contact auto-sync** — on a verified chat, Bookbag upserts the [contact](/docs/contacts/overview) whose `external_id` equals the user id, filling name/email/phone and merging any extra attributes into [custom attributes](/docs/contacts/custom-attributes). Return visits update the same record.
- **Personalized answers** — the agent receives the contact's known details and can greet the customer by name and tailor replies without re-asking.
- **Action context** — actions can reference the verified customer with `{{contact.email}}`, `{{contact.name}}`, `{{contact.external_id}}`, and `{{contact.attr.<key>}}`.

## Require verified identity (optional)

By default the agent accepts both anonymous and verified visitors. Turn on **Require verified identity** (Chat Interface → Embed → Identity verification) to **reject any chat** that isn't signed. For JWT, Bookbag verifies the token (HS256, signature, `exp`/`nbf`); for the user-hash method it recomputes `HMAC-SHA256(user_id)` with your secret and compares it to the `user_hash` the widget sent. A missing, forged, or expired credential is refused with a `401`.

> **TURN IT ON ONLY AFTER WIRING IDENTIFY():** If you require verified identity before your site calls `identify()` with a valid token, all chats are rejected. Roll it out: ship `identify()` first, confirm it works, then require it.

## Shopify

On Shopify the storefront integration identifies the logged-in customer for you from a theme-injected `window.__bbCustomer`. See the [Shopify integration](/docs/integrations/shopify).

- [Contacts overview](/docs/contacts/overview) — The records identity syncs into, and how the agent uses them.
- [JavaScript embed & SDK](/docs/developers/javascript-embed) — Install the widget and the window.bookbag global.
- [Custom attributes](/docs/contacts/custom-attributes) — Typed fields your user_metadata maps onto.
