Connect Agent conversations to the people in your product, and keep their history when they sign in.
Every browser that opens the Agent gets an anonymous identity. When you tell HeroUI who the person is, their earlier anonymous conversations move with them, so history survives login.
Identity is always minted on your server. The browser never sees your API key, and HeroUI never stores the raw id you send — it is hashed into a pseudonymous key before it reaches the runtime or monitoring.
The SDK creates a per-agent id, stores it in localStorage, and passes it to your getAuthToken callback. Minting a token with that id is all an unauthenticated visitor needs:
await createAuthToken({
apiKey: process.env.HEROUI_AGENT_API_KEY!,
identity: {id: anonymousId, type: "anonymous"},
agentId,
});Read your own session inside the token route and mint an identified token instead. Pass the browser's anonymousId alongside it so conversations started before login are merged into the identified person:
import {createAuthToken} from "@heroui/agent/server";
import {getSession} from "@/lib/session";
export async function POST(request: Request) {
const {anonymousId, agentId} = await request.json();
if (agentId !== process.env.HEROUI_AGENT_ID) {
return Response.json({error: "Invalid agent"}, {status: 400});
}
const session = await getSession();
return Response.json(
await createAuthToken({
anonymousId,
apiKey: process.env.HEROUI_AGENT_API_KEY!,
identity: session
? {id: session.user.id, type: "user"}
: {id: anonymousId, type: "anonymous"},
profile: session ? {email: session.user.email, name: session.user.name} : undefined,
agentId,
}),
{headers: {"Cache-Control": "no-store"}},
);
}Use the same stable id you use elsewhere in your systems, such as a database id. Avoid values that change between sessions, and never send a password, token, or other secret.
profile accepts name, email, and avatarUrl. These are optional and only used to make the person recognizable in Agent monitoring. Send whatever you have on each call; omitted fields keep their previous value.
The SDK caches a credential for the length of its lifetime, so a person who signs in mid-session keeps the anonymous token until it expires. Call refreshAuth() after login to mint a new one immediately:
"use client";
import {useAgent} from "@heroui/agent";
export function LoginButton() {
const agent = useAgent();
const handleLogin = async () => {
await signIn();
agent.refreshAuth();
};
return <button onClick={handleLogin}>Log in</button>;
}Call shutdown() when a person signs out. It clears the active credential, the local conversation state, and the browser's anonymous id, so the next person on a shared device starts as a new anonymous visitor:
const agent = useAgent();
const handleLogout = async () => {
await signOut();
agent.shutdown();
};Call shutdown() on logout even when you do not expect devices to be shared. Without it, the next
visitor inherits the previous person's conversations.
When you identify a browser that already chatted anonymously:
externalId and profileMerging only ever moves an anonymous identity into an identified one. Identified users are never merged into each other, and an identity is never split back apart, so send a stable id from the start.
Placeholder values are rejected with a 400 because they would collapse unrelated visitors into one person. This includes empty strings and, case-insensitively, anonymous, guest, id, distinct_id, distinctid, email, not_authenticated, undefined, null, none, nan, true, false, 0, and [object Object].