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.

  • 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.

app/api/clivly/widget/session/route.ts
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 widget scaffolds this exact route for you.
  • allowedOrigins are exact-match origins — the scheme and host of the pages that embed the widget. No wildcards: *.example.com is 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.

app/api/clivly/widget/session/route.ts
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_source of host_session or self_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.
  • resolveUser returning 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.

chat-widget.tsx
import { ChatWidget, createSessionFetcher } from "@clivly/chat-widget";

<ChatWidget
widgetId="support"
getSession={createSessionFetcher({
  sessionUrl: "/api/clivly/widget/session",
})}
/>;
embed.html
<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

AttributeRequiredWhat it does
data-widget-idyesThe public slug you configured in the dashboard.
data-session-urlyesYour mounted session route. Usually same-origin.
data-themenoauto (default), light, or dark.
data-socket-url-templatenoOverrides 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. style is the intended way to override palette tokens, since inline declarations beat the built-in rules without specificity games.
  • storage — supply your own getItem/setItem if you do not want localStorage.

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.

  • 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 visitorToken so a returning visitor resumes their existing thread instead of starting a new one.

Theming

theme="auto" follows the host app rather than guessing.

Override the palette with CSS custom properties on the widget root:

chat-widget.tsx
<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

SymptomCause
origin_not_allowedThe embedding page's origin is not an exact match in the widget's allowed origins. Check scheme and port too.
Session request fails with 401The session route's CLIVLY_SECRET_KEY is missing, wrong, or revoked.
Widget never appearsdata-widget-id or data-session-url is absent — both are required, and the script exits quietly without them.
Visitor shows as self-reportedresolveUser returned null or threw. That is deliberate: identity failures downgrade the visitor rather than breaking the chat.
Widget is inert with no errorThe widget is disabled, or its origin list is empty — an empty list permits nothing.