Guides
Chat widget
Configure the visitor widget, expose a session bootstrap endpoint, embed it through React or a script tag, and theme it to match your app.
Create the widget in your dashboard
Create and configure the widget from your Clivly dashboard — no raw SQL. The dashboard applies slug, origin, and uniqueness validation the old manual insert skipped.
Browser
your visitor
Your route
/api/clivly/session
Session issued
api.clivly.com
The secret key never leaves your server. The browser only ever holds a token scoped to one conversation.
- Launch "Add chat widget" from the dashboard, the ⌘K command palette, or the Settings → Chat widget page — all open the same dialog.
- Set the name, widget ID (a public slug used in the embed), allowed origins, greeting, and brand colour, with a live preview.
- After you create it, the install step shows the embed snippet and the widget's active / blocked status.
The widget ID is a public slug, not a credential — support, sales, whatever you name it. It grants nothing on its own: your allowed origins decide which pages may open a session, and the secret key on your session route is what authorises one.
Add the session route
Your app keeps the secret key server-side and hands the browser a session bootstrap.
import { createChatSessionHandler } from "clivly/sdk";
export const POST = createChatSessionHandler({
apiKey: process.env.CLIVLY_SECRET_KEY ?? process.env.CLIVLY_API_KEY ?? "",
allowedOrigins: [
"https://app.example.com",
],
});clivly add widgetscaffolds this exact route for you.allowedOriginsare exact-match origins — the scheme and host of the pages that embed the widget. No wildcards:*.example.comis easy to write and easy to get wrong, and a copied snippet running on someone else's subdomain is what the allowlist exists to stop.- An empty origin list permits nothing. A widget stays inert until you configure one.
Vouch for a logged-in visitor
By default a visitor is self-reported: whatever they typed in the pre-chat form. If they are already logged into your app, your server can vouch for who they are.
Self-reported
Anyone can type anything. Fine for a marketing page — not something to act on.
Vouched by your server
Your existing auth already proved this. Clivly records it as verified.
What makes the difference
Your session route resolves the logged-in user before minting the session — the same trust your app already grants them.
Why it matters in the inbox
Operators see which identities were verified, so they know what they can safely act on.
import { createChatSessionHandler } from "clivly/sdk";
import { auth } from "@/auth";
export const POST = createChatSessionHandler({
apiKey: process.env.CLIVLY_SECRET_KEY ?? "",
allowedOrigins: ["https://app.example.com"],
// Optional. Runs on the browser request with session cookies intact.
// Returning a user makes your app vouch for that identity.
resolveUser: async (request) => {
const session = await auth.api.getSession({ headers: request.headers });
return session?.user
? {
id: session.user.id,
email: session.user.email,
name: session.user.name ?? undefined,
}
: null;
},
});- Conversations record an
identity_sourceofhost_sessionorself_reported, so operators can see which claims are trustworthy. - A verified visitor with no matching CRM contact is recorded as verified with no contact linked — Clivly never auto-creates a contact from this path.
resolveUserreturning null, or throwing, leaves the visitor self-reported. It never fails the session.
This is the reason the session route exists at all. Only a server holding your secret key can assert who someone is; a browser cannot be trusted to assert it about itself.
Embed the widget
The current package supports both React embedding and script-tag delivery.
import { ChatWidget, createSessionFetcher } from "@clivly/chat-widget";
<ChatWidget
widgetId="support"
getSession={createSessionFetcher({
sessionUrl: "/api/clivly/widget/session",
})}
/>;
<script
src="https://api.clivly.com/widget.js"
data-widget-id="support"
data-session-url="/api/clivly/widget/session"
data-theme="auto"
defer
></script>Script tag attributes
| Attribute | Required | What it does |
|---|---|---|
data-widget-id | yes | The public slug you configured in the dashboard. |
data-session-url | yes | Your mounted session route. Usually same-origin. |
data-theme | no | auto (default), light, or dark. |
data-socket-url-template | no | Overrides the realtime endpoint. Only needed for self-hosted Clivly. |
React props
ChatWidget takes widgetId and getSession, plus:
theme—"auto","light"or"dark", as above.initiallyOpen— start with the panel open instead of the launcher.launcherLabel— the accessible label on the launcher button.className/style— style the root.styleis the intended way to override palette tokens, since inline declarations beat the built-in rules without specificity games.storage— supply your owngetItem/setItemif you do not wantlocalStorage.
Messages are realtime
Once a session exists the browser holds a WebSocket straight to Clivly. Your session route runs once, at the start, and never carries messages.
Visitor's browser
holds the session token
Conversation room
one per conversation
Operator inbox
one per workspace
Your session route runs once, at the start. Messages never pass through your server afterwards.
- Each conversation gets its own room, which orders messages and fans them out to both sides.
- A workspace-wide inbox room pushes updates to any dashboard your team has open, which is why replies appear without a refresh.
- The visitor's browser keeps a
visitorTokenso a returning visitor resumes their existing thread instead of starting a new one.
Theming
theme="auto" follows the host app rather than guessing.
data-theme on <html>
the host's explicit choice wins
a dark or light class
what Tailwind and most design systems toggle
the operating system
prefers-color-scheme, when the host says nothing
Set theme="light" or theme="dark" to pin the widget regardless of what the host page is doing.
Override the palette with CSS custom properties on the widget root:
<ChatWidget
widgetId="support"
getSession={getSession}
style={{
"--clivly-chat-accent": "#185adb",
"--clivly-chat-panel": "#0b0f19",
"--clivly-chat-text": "#e6edf7",
}}
/>;Brand colour and greeting are also set per widget in the dashboard, so non-developers can adjust them without a deploy.
Troubleshooting
| Symptom | Cause |
|---|---|
origin_not_allowed | The embedding page's origin is not an exact match in the widget's allowed origins. Check scheme and port too. |
| Session request fails with 401 | The session route's CLIVLY_SECRET_KEY is missing, wrong, or revoked. |
| Widget never appears | data-widget-id or data-session-url is absent — both are required, and the script exits quietly without them. |
| Visitor shows as self-reported | resolveUser returned null or threw. That is deliberate: identity failures downgrade the visitor rather than breaking the chat. |
| Widget is inert with no error | The widget is disabled, or its origin list is empty — an empty list permits nothing. |