Embed HeroUI Agent in any modern website with React, Next.js, or vanilla JavaScript.
Click Copy Prompt above and paste it into Codex, Claude Code, Cursor, or your coding assistant with this application open. It guides your assistant through skill and MCP setup, Agent configuration, the token endpoint, and the embed, then asks it to verify a message and client-tool interaction in the running app.
You choose the workspace and approve permissions during browser sign-in. Read the Building with AI overview for access requirements, or follow the framework examples below.
Complete Installation first. You should have an Agent ID and workspace API key
from the Agents dashboard. React integrations also install @heroui/agent; vanilla websites load the
browser bridge from agent.heroui.pro/loader.js.
The examples below read HEROUI_AGENT_API_KEY and HEROUI_AGENT_ID on the server. The browser
receives only the Agent ID, which identifies the Agent but does not authorize a request.
Every integration needs a server-only token exchange and one browser bridge. Choose the setup that fits your website:
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,
anonymousId,
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 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,
anonymousId,
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,
anonymousId: data.anonymousId,
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,
anonymousId,
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.
Vanilla websites load the small browser bridge from HeroUI. React is not required. The token exchange still runs on your backend or serverless platform so the workspace API key never reaches the browser.
import {createAuthToken} from "@heroui/agent/server";
const AGENT_ID = process.env.HEROUI_AGENT_ID!;
// Mount this handler at POST /api/heroui-agent/auth-token.
export async function handleAgentAuth(request: Request) {
const {anonymousId, agentId} = await request.json();
if (typeof anonymousId !== "string" || agentId !== AGENT_ID) {
return Response.json({error: "Invalid agent"}, {status: 400});
}
return Response.json(
await createAuthToken({
anonymousId,
apiKey: process.env.HEROUI_AGENT_API_KEY!,
identity: {id: anonymousId, type: "anonymous"},
agentId: AGENT_ID,
}),
{headers: {"Cache-Control": "no-store"}},
);
}<script src="https://agent.heroui.pro/loader.js" defer></script>
<script>
window.addEventListener("heroui-agent:loaded", () => {
window.agent = window.HeroUIAgent.mount({
agentId: "your_agent_id",
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();
},
});
});
</script>The returned controller exposes show, hide, toggle, newConversation, refreshAuth,
shutdown, and destroy. Vanilla client tools use raw JSON Schema for parameters; see
Client Tools.
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 hosted Agent ships in tiers so websites download only what the current experience needs:
This is automatic. React integrations do not need to lazy-load HeroUIAgent, and vanilla websites
can load the CDN bridge with defer as shown above.
Configure appearance to match the embed to your product, then identify your users so conversations follow them across sessions and devices.