Auth
Identity tokens.
A short-lived HS256 JWT your server signs with the Embed Secret. It tells Onpilot which agent to load and who the user is. The browser never has the secret.
How it works
- Your server calls
onpilot.signIdentityToken({ copilotId, user, expiresIn }) - The SDK signs the JWT locally — no network call
- You pass the JWT to the widget via
identityToken(oridentityTokenProviderfor long-lived sessions) - The widget exchanges the JWT for a chat session
JWT
Claim shape
{
"iss": "<tenantId>",
"sub": "<userId>",
"copilot_id": "<copilotId>",
"role": "admin",
"name": "Optional Name",
"email": "optional@example.com",
"iat": 1730131200,
"exp": 1730134800
}| Claim | Required | Notes |
|---|---|---|
iss | yes | Tenant ID. SDK sets from your client. |
sub | yes | Your user's stable identifier. |
copilot_id | yes | The agent the user should load. |
role | no | "admin" or "user". Defaults to "user". |
name | no | Display name. Surfaces in greetings. |
email | no | Useful for tools that need it. |
iat / exp | yes | Standard JWT timestamps. |
Signed HS256 with your Embed Secret (per-tenant). Default expiry is 24h server-side; we recommend 1h for customer-facing flows.
Pattern
Static vs provider
Two ways to pass the token to the widget. Static is simpler; provider keeps long-lived sessions alive.
Static
<CopilotBubble identityToken={token} />expiresIn: "1h" and the user keeps the page open for 90 minutes, the chat silently breaks at minute 60. Either bump the expiry or use the provider pattern below.Provider — recommended for SPAs
<CopilotBubble
identityTokenProvider={async () => {
const r = await fetch("/api/onpilot/token", {
credentials: "include",
});
const { identity_token } = await r.json();
return identity_token;
}}
/>The SDK calls the provider on mount and again whenever the current token approaches expiry — refresh window is 80% of remaining TTL with a 30-second floor and 60-second ceiling.
Token endpoint (Next.js Route Handler)
// app/api/onpilot/token/route.ts
import { NextResponse } from "next/server";
import { signOnPilotIdentity } from "@/lib/onpilot";
import { getCurrentUser } from "@/lib/session";
export async function GET() {
const user = await getCurrentUser();
if (!user) return new Response("unauthorized", { status: 401 });
const identity_token = signOnPilotIdentity(user);
return NextResponse.json({ identity_token });
}Operations
Rotating the embed secret
Settings → Profile → Embed Secret → Rotate. Rotation is immediate. Every in-flight token signed with the old secret stops verifying as soon as you confirm.
If you use identityTokenProvider, rotation recovers automatically — the next token your endpoint mints uses the new secret. If you use static identityToken, in-flight users will see the next message fail; a page refresh server-renders a fresh token.
Errors
What can go wrong
The SDK fires onError with one of these codes when token resolution fails. Iframe runtime errors don't surface here — they're handled inside the chat.
| Code | Meaning |
|---|---|
RESOLVE_ERROR | JWT rejected by the resolve endpoint — likely expired, signed with the wrong secret, or iss doesn't match a known tenant. |
TOKEN_FETCH_ERROR | Your identityTokenProvider threw or returned non-2xx. Check the endpoint, auth, and CORS. |
Security
Allowed domains
Restrict where the widget can mount. Set Publish → Allowed Domains on each agent. The resolve endpoint enforces the allow-list — if the embedding origin isn't on it, the chat refuses to load.