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
- A user logs into your app.
- Your backend mints a short-lived JWT, signed with your chatbot's identity secret.
- You hand the token to the widget (before it loads, or at runtime).
- The widget sends the token with each chat request.
- 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
- Open your agent and go to Settings → Identity.
- Turn on Verify signed-in visitors (JWT).
- 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
| Claim | Required | Notes |
|---|---|---|
sub (or user_id) | Yes | Your 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. |
exp | Yes | Expiration time. Use short-lived tokens (~1 hour). Tokens whose exp is more than 24 hours in the future are rejected. |
aud | Recommended | Bind the token to this agent: agentkit:chatbot:{chatbotId}. If present, it must match. |
email | No | Stored on the Contact. |
name | No | Stored on the Contact. |
phonenumber | No | Stored on the Contact's phone field. |
custom_attributes | No | Object 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.
| Situation | Result |
|---|---|
| No token | Anonymous visitor. No Contact linked. (Not an error.) |
| Valid token | Verified. Contact created/updated and linked to the conversation. |
| Invalid signature / malformed token | Anonymous. Failure logged. |
| Expired token | Anonymous. Failure logged. |
aud doesn't match this agent | Anonymous. Failure logged. |
| Profile claims exceed limits | Anonymous. 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.