Install the SDK and embed HeroUI Agent with Next.js, Vite, TanStack Start, or React Router.
Complete Installation first. You should have @heroui/agent and its styles
in your application, plus an Agent ID and project API key from the Pro dashboard.
The examples below read HEROUI_AGENT_API_KEY and HEROUI_AGENT_ID on the server. The browser
receives only NEXT_PUBLIC_HEROUI_AGENT_ID in Next.js or VITE_HEROUI_AGENT_ID in the other
frameworks.
Every integration needs a server-only token exchange and one HeroUIAgent near the root of your
application. Choose your framework for the files and entry point that fit your project:
Each example treats visitors as anonymous. See Identifying users to connect conversations to the people signed in to your product.
Use the @heroui/agent/next entry point with an App Router Route Handler.
Next.js 16 + Sentry
On Next.js 16 with Sentry (especially tunnelRoute), you may see MaxListenersExceededWarning on
ServerResponse. Next attaches 6+ close listeners per response; the Agent only adds more
requests (token exchange, CSS, widget). Raise ServerResponse max listeners on the Node process,
or wait for Next.js to fix it — the Agent does not add those listeners.
import {createAuthToken} from "@heroui/agent/server";
export async function POST(request: Request) {
const {anonymousId, agentId} = await request.json();
const apiKey = process.env.HEROUI_AGENT_API_KEY;
if (!apiKey || agentId !== process.env.HEROUI_AGENT_ID) {
return Response.json({error: "Invalid agent"}, {status: 400});
}
return Response.json(
await createAuthToken({
apiKey,
identity: {id: anonymousId, type: "anonymous"},
agentId,
}),
{headers: {"Cache-Control": "no-store"}},
);
}"use client";
import type {GetAuthToken} from "@heroui/agent";
import {HeroUIAgent} from "@heroui/agent/next";
const getAuthToken: GetAuthToken = async (context) => {
const response = await fetch("/api/heroui-agent/auth-token", {
body: JSON.stringify(context),
headers: {"Content-Type": "application/json"},
method: "POST",
});
if (!response.ok) throw new Error("Agent authentication failed");
return response.json();
};
export function AppAgent() {
return (
<HeroUIAgent getAuthToken={getAuthToken} agentId={process.env.NEXT_PUBLIC_HEROUI_AGENT_ID!} />
);
}Add <AppAgent /> near the end of your root layout.
Vite runs your React client, but the API key still belongs in a backend or serverless function.
Mount this Fetch-compatible handler at POST /api/heroui-agent/auth-token in your existing backend:
import type {GetAuthTokenContext} from "@heroui/agent";
import {createAuthToken} from "@heroui/agent/server";
export async function handleAgentAuth(request: Request) {
const {anonymousId, agentId} = (await request.json()) as GetAuthTokenContext;
const apiKey = process.env.HEROUI_AGENT_API_KEY;
if (!apiKey || agentId !== process.env.HEROUI_AGENT_ID) {
return Response.json({error: "Invalid agent"}, {status: 400});
}
return Response.json(
await createAuthToken({
apiKey,
identity: {id: anonymousId, type: "anonymous"},
agentId,
}),
{headers: {"Cache-Control": "no-store"}},
);
}import type {GetAuthToken} from "@heroui/agent";
import {HeroUIAgent} from "@heroui/agent";
const getAuthToken: GetAuthToken = async (context) => {
const response = await fetch("/api/heroui-agent/auth-token", {
body: JSON.stringify(context),
headers: {"Content-Type": "application/json"},
method: "POST",
});
if (!response.ok) throw new Error("Agent authentication failed");
return response.json();
};
export function AppAgent() {
return <HeroUIAgent getAuthToken={getAuthToken} agentId={import.meta.env.VITE_HEROUI_AGENT_ID} />;
}Render <AppAgent /> once from your root App component.
Use a TanStack Start server function for the token exchange. The function stays server-side while its typed caller can be used directly by the embed.
import type {GetAuthTokenContext} from "@heroui/agent";
import {createAuthToken} from "@heroui/agent/server";
import {createServerFn} from "@tanstack/react-start";
export const getAgentAuthToken = createServerFn({method: "POST"})
.validator((context: GetAuthTokenContext) => context)
.handler(async ({data}) => {
const apiKey = process.env.HEROUI_AGENT_API_KEY;
const agentId = process.env.HEROUI_AGENT_ID;
if (!apiKey || !agentId || data.agentId !== agentId) {
throw new Error("Invalid agent");
}
return createAuthToken({
apiKey,
identity: {id: data.anonymousId, type: "anonymous"},
agentId,
});
});import type {GetAuthToken} from "@heroui/agent";
import {HeroUIAgent} from "@heroui/agent";
import {getAgentAuthToken} from "./lib/agent-auth.functions";
const getAuthToken: GetAuthToken = (context) => getAgentAuthToken({data: context});
export function AppAgent() {
return <HeroUIAgent getAuthToken={getAuthToken} agentId={import.meta.env.VITE_HEROUI_AGENT_ID} />;
}Render <AppAgent /> once from your root route.
In React Router Framework Mode, expose the token exchange as a resource route with an action.
import {type RouteConfig, route} from "@react-router/dev/routes";
export default [
route("api/heroui-agent/auth-token", "./routes/api.heroui-agent.auth-token.ts"),
// Keep your existing application routes here.
] satisfies RouteConfig;import type {Route} from "./+types/api.heroui-agent.auth-token";
import {createAuthToken} from "@heroui/agent/server";
export async function action({request}: Route.ActionArgs) {
const {anonymousId, agentId} = await request.json();
const apiKey = process.env.HEROUI_AGENT_API_KEY;
if (!apiKey || agentId !== process.env.HEROUI_AGENT_ID) {
return Response.json({error: "Invalid agent"}, {status: 400});
}
return Response.json(
await createAuthToken({
apiKey,
identity: {id: anonymousId, type: "anonymous"},
agentId,
}),
{headers: {"Cache-Control": "no-store"}},
);
}import type {GetAuthToken} from "@heroui/agent";
import {HeroUIAgent} from "@heroui/agent";
const getAuthToken: GetAuthToken = async (context) => {
const response = await fetch("/api/heroui-agent/auth-token", {
body: JSON.stringify(context),
headers: {"Content-Type": "application/json"},
method: "POST",
});
if (!response.ok) throw new Error("Agent authentication failed");
return response.json();
};
export function AppAgent() {
return <HeroUIAgent getAuthToken={getAuthToken} agentId={import.meta.env.VITE_HEROUI_AGENT_ID} />;
}Render <AppAgent /> once from your root route component.
Never expose your API key to the browser. Keep it in a server-only environment variable and mint short-lived tokens through your own endpoint or server function.
Start your application in a supported browser, open the Agent launcher, and send a message. If your server endpoint can mint a token for the Agent, the conversation connects to the hosted runtime.
That is the whole integration. The embed applies the appearance saved for your project, so it picks up your launcher icon, colors, typography, and greeting from the dashboard without any style props. Pass props for anything you would rather pin in code; they take precedence.
The Agent ships in tiers so the panel never makes people wait on a download:
HeroUIAgent and paints on the same frame the panel opens, in both floating and sidebar mode.This is automatic. You do not need to lazy-load HeroUIAgent yourself, and wrapping it in next/dynamic only delays the shell.
Configure appearance to match the embed to your product, then identify your users so conversations follow them across sessions and devices.