Identity Verification

Verify who is chatting by signing a JWT for your logged-in users. Agentkit verifies the token and links the conversation to a Contact.

Identity verification lets you tell Agentkit who is chatting. Your backend signs a short-lived JWT for the logged-in user, the widget hands that token to Agentkit, and Agentkit verifies it and links the conversation to a Contact.

It is enrichment and anti-spoofing, never an access gate. A missing, expired, or invalid token simply means the visitor chats anonymously — the chat is never blocked.

Note: Identity tokens carry private user data. Always sign them on your server, never in client-side JavaScript — signing in the browser would expose your secret.

How It Works

  1. A user logs into your app.
  2. Your backend mints a short-lived JWT, signed with your chatbot's identity secret.
  3. You hand the token to the widget (before it loads, or at runtime).
  4. The widget sends the token with each chat request.
  5. Agentkit verifies the signature and claims, then creates or updates a Contact keyed by the token's subject and links the conversation to it.

Enabling Verification

  1. Open your agent and go to Settings → Identity.
  2. Turn on Verify signed-in visitors (JWT).
  3. Copy the Signing secret.

The signing secret is server-side only — keep it in an environment variable and never expose it in client code. Regenerating the secret immediately invalidates every token signed with the old one.

Token Specification

Sign the token with the HS256 algorithm. Other algorithms (including alg: none and asymmetric keys) are rejected.

Claims

ClaimRequiredNotes
sub (or user_id)YesYour app's immutable user id. Use a stable internal id — not an email, username, or Agentkit id. If both sub and user_id are present they must agree.
expYesExpiration time. Use short-lived tokens (~1 hour). Tokens whose exp is more than 24 hours in the future are rejected.
audRecommendedBind the token to this agent: agentkit:chatbot:{chatbotId}. If present, it must match.
emailNoStored on the Contact.
nameNoStored on the Contact.
phonenumberNoStored on the Contact's phone field.
custom_attributesNoObject for any extra data (plan, company, Stripe ids, …).

Only email, name, phonenumber, and custom_attributes are read from the token. Any other top-level claim is ignored — put extra or integration data inside custom_attributes.

Limits

  • email, name, phonenumber: up to 1024 characters each.
  • custom_attributes: up to 8192 bytes, 50 keys, and 5 levels of nesting.

A token that violates these limits verifies as anonymous (the failure is logged) rather than blocking the chat.

Signing the Token

Any JWT library works (jsonwebtoken, PyJWT, …) — you just need an HS256 token with the claims above. This example uses jose, which also runs on edge runtimes (Vercel Edge, Cloudflare Workers, Deno):

// On YOUR backend, mint a short-lived JWT for your logged-in user.
import { SignJWT } from 'jose';

const secret = new TextEncoder().encode(
  process.env.AGENTKIT_CHATBOT_IDENTITY_SECRET,
);

const token = await new SignJWT({
  // Optional private profile claims — stored on the Contact, never shown to the
  // bot. ONLY these keys are read: email, name, phonenumber, custom_attributes.
  // Put extra/integration data (Stripe ids, plan, company, …) in custom_attributes.
  email: user.email,
  name: user.name,
  phonenumber: user.phone, // maps to the Contact's "phone"
  custom_attributes: {
    plan: user.plan,
    stripe_id: user.stripeCustomerId,
  },
})
  .setProtectedHeader({ alg: 'HS256' })
  .setSubject(user.id) // immutable id from YOUR system (not email/username)
  .setIssuedAt()
  .setExpirationTime('1h') // short-lived; this is a browser bearer token
  .setAudience('agentkit:chatbot:YOUR_CHATBOT_ID')
  .sign(secret);

The same snippet, pre-filled with your agent's audience, is shown on the Settings → Identity page.

# Python example using PyJWT
import jwt, os, time

token = jwt.encode(
    {
        "sub": user.id,
        "email": user.email,
        "name": user.name,
        "phonenumber": user.phone,
        "custom_attributes": {"plan": user.plan},
        "aud": "agentkit:chatbot:YOUR_CHATBOT_ID",
        "iat": int(time.time()),
        "exp": int(time.time()) + 3600,
    },
    os.environ["AGENTKIT_CHATBOT_IDENTITY_SECRET"],
    algorithm="HS256",
)

Passing the Token to the Widget

Set the token before the widget script loads (for example, inject it server-side into the page):

<script>
  window.agentkitUserConfig = { token: 'YOUR_SIGNED_TOKEN' };
</script>

In single-page apps, update or clear the identity at runtime — for example after a login or logout:

// After the user logs in (or the token is refreshed):
window.agentkit('identify', { token: newToken });

// After the user logs out:
window.agentkit('resetUser');

The widget attaches the token to each chat request automatically; you do not need to set any headers yourself.

Privacy

Verified profile fields (email, name, phonenumber, custom_attributes) are stored on the Contact server-side only. They are never injected into the agent's prompt or RAG context, and never returned by any public endpoint. The agent does not "see" who it is talking to.

Verification Behavior

Verification never blocks a chat. The visitor always gets to talk to the agent; the only difference is whether a Contact is linked.

SituationResult
No tokenAnonymous visitor. No Contact linked. (Not an error.)
Valid tokenVerified. Contact created/updated and linked to the conversation.
Invalid signature / malformed tokenAnonymous. Failure logged.
Expired tokenAnonymous. Failure logged.
aud doesn't match this agentAnonymous. Failure logged.
Profile claims exceed limitsAnonymous. Failure logged.

Other Access Controls Still Apply

Identity verification is independent of who can load the widget. Domain restrictions and visibility settings continue to apply separately.

Next Steps