Chatbot widget embedding
Embed the Nerva chatbot widget on your website with a small script bundle and public widget preinit API.
The Nerva chatbot widget is a lightweight JavaScript runtime that mounts a floating chat launcher on your site. It runs inside a Shadow DOM so your page styles stay isolated from the widget UI.
Quick install
Paste this snippet into your page:
Replace agent_xxxxxxxxx with your chat agent ID from
Dashboard → Agents → your chat agent → Embed.
This loads the widget runtime, then initializes the chat agent once the script is ready.
How it works
- The script bundle is loaded from the CDN.
- Once loaded,
window.ctplai.cbp.init()fetches widget data from/api/widget/[agentId]/preinit. - The widget opens a launcher and connects to the chat backend over WebSocket.
The default runtime URL is controlled by NEXT_PUBLIC_WIDGET_SCRIPT_URL and
falls back to https://cdn.ctplai.org/crm.cbp.min.js.
Script-tag install (legacy)
The runtime also supports a self-initializing form. If the page contains a
script tag carrying data-agent-id, the widget boots itself after the
browser's load event without any init() call:
| Attribute | Default | Description |
|---|---|---|
data-agent-id | (required) | The chat agent ID. Its presence is what triggers auto-init. |
data-auto-open | false | Open the panel immediately once the widget mounts. |
data-position | bottom-right | bottom-right, bottom-left, top-right, or top-left. |
data-idle-timeout-ms | (agent setting) | Override the idle timeout for this embed. |
data-idle-timeout-message | (agent default) | Message shown when the session times out. |
Prefer the init() snippet above for new installs: it is what the dashboard
generates, and it gives you the full JavaScript API. The data- form is kept
for existing embeds and is not scheduled for removal.
Deferring the widget
init() runs as soon as you call it, so to delay the widget you delay the
call. Initializing on a user action is the usual approach:
Two related options are often confused with this:
autoOpen(defaultfalse) controls whether the chat panel opens by itself once the widget has mounted. It does not affect wheninit()runs.openDelay(default0, in milliseconds) delays only that automatic open. It has no effect unlessautoOpenistrue.
Widget API
After the script loads, the widget is available as window.ctplai.cbp.
Common calls include:
init() is a no-op if the widget is already initialized or currently
initializing, so it is safe to call more than once.
Dashboard-driven appearance
Most widget settings are configured in the dashboard, not in the embed code. Adjust these from Dashboard → Agents → your chat agent → Appearance:
- Brand color and button style
- Bot avatar and launcher icon
- Welcome message and placeholder text
- Position, auto-open, and idle timeout
- Lead capture form and file upload settings
The embed snippet only loads the runtime and initializes the agent.
Public widget preinit endpoint
The widget uses the public endpoint:
GET /api/widget/[agentId]/preinit
That endpoint returns the runtime configuration, the chat WebSocket URL, and
agent-specific settings such as theme, position, idle timeout, and lead form
behavior. It returns 404 when the agent does not exist or is not active, so
an inactive agent simply never mounts.
Page context and visitor identity
When the widget opens its WebSocket it sends these query parameters:
| Parameter | Description |
|---|---|
session | Session ID for this conversation thread. |
visitor | Visitor ID, stable across sessions in the same browser. |
fingerprint | FingerprintJS hash. Best-effort, may be empty. |
locale | Browser locale, from navigator.language. Defaults to en. |
pageUrl | URL of the page the widget was opened on. |
The chat backend also accepts pageReferrer and userId on this connection
for callers that supply them. The bundled widget does not send either one, so
pageReferrer arrives empty for standard embeds.
Downstream these arrive under their metadata names, so the value sent as
locale appears as userLocale in webhook payloads. See
Webhook events for the payload shapes.
Session state is stored in localStorage, keyed per agent, which lets
returning visitors resume without your site managing user accounts.
Next.js / SSR usage
Use a plain script tag or Next.js Script component. The widget initializes
in the browser after load, so it does not require server-side rendering of
widget markup.
Because onLoad is a function prop, the component holding the Script has to
be a client component:
Render <ChatWidget /> near the end of your root layout's <body>. Keeping
it in its own component avoids marking the whole layout "use client".
TypeScript does not know about window.ctplai, so declare it once:
Content Security Policy
If you use a Content Security Policy, allow the CDN that serves the bundle, the app origin the widget calls for preinit, and the voice host it opens the WebSocket against:
The Quick install snippet is an inline script, which script-src blocks
unless you allow it. Either give that tag a nonce:
or avoid inline entirely by using the script-tag install, which needs no inline code.
Substitute your own hosts if you run Nerva on a different domain. The WebSocket host is whatever your deployment advertises in the preinit response, not a value you set in the embed.
Testing locally
The widget does not hardcode a chat host. It connects to the chatWsUrl
returned by the preinit endpoint, which the app builds from its server-side
NERVA_VOICE_WS_URL environment variable. To point a local widget at a local
voice service, set NERVA_VOICE_WS_URL on the app (for example
ws://localhost:8001) rather than changing the embed snippet.
To load a locally built bundle instead of the CDN one, override
NEXT_PUBLIC_WIDGET_SCRIPT_URL on the app, or point m.src at your local
file directly.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Widget never appears | Script failed to load, network/CSP blocked, or the agentId is invalid. An inline snippet under a strict script-src is the most common cause. |
| Widget loads but won't connect | Preinit or WebSocket request blocked. Check connect-src covers both the app origin and the wss:// host, then check the browser Network tab. |
| Preinit returns 404 | The agent ID is wrong, or the agent is not active. Verify in the dashboard. |
| Widget position is wrong | Use the agent's appearance settings or call window.ctplai.cbp.destroy() on specific pages. |
| Hydration warnings in Next.js | Use Script with afterInteractive or a plain script tag outside client-rendered JSX. |