Chatbot widget embedding
Embed the Nerva chatbot widget on your website with one script tag — no JavaScript framework required.
The Nerva chatbot widget is a single JavaScript bundle that mounts a chatbot launcher on your site. It runs entirely inside a Shadow DOM, so its styles never collide with yours.
Quick install
Paste this into your site's <head> (or anywhere in the body — anywhere
works):
Replace agent_xxxxxxxxx with your chat agent's ID, which you can copy from
Dashboard → Agents → your chat agent → Embed tab.
That's it. A floating chat launcher appears in the bottom-right of every page. Visitors click to open and start chatting.
Configuration via data- attributes
The auto-init script picks up these attributes:
| Attribute | Default | Description |
|---|---|---|
data-agent-id | — (required) | The chat agent ID. |
data-auto-open | false | Open the widget immediately on page load. |
data-position | bottom-right | bottom-right, bottom-left, top-right, or top-left. |
data-idle-timeout-ms | (agent default) | Override the agent's idle timeout for this embed. |
data-idle-timeout-message | (agent default) | Custom message shown when the session times out. |
Example with options:
Programmatic control
If you need to control the widget from your own JavaScript (e.g., open it
when a button is clicked, change the agent dynamically), use the global
namespace window.ctplai.cbp:
Skip the auto-init by omitting data-agent-id from the script tag, then
call init() yourself when ready:
Theming and appearance
The widget's colors, branding, and copy are configured server-side on the agent itself — not in the embed snippet. Change them from Dashboard → Agents → your chat agent → Appearance:
- Brand color — primary accent.
- Logo / avatar — shown in the header.
- Launcher icon — the floating button.
- Welcome message — first thing visitors see when opening.
Updates take effect immediately on the next session (or call
updateConfig() to refresh in-place).
Visitor identity
The widget identifies returning visitors using:
- Fingerprint — via FingerprintJS, generates a stable browser identifier without cookies.
- Session ID — stored in
localStoragekeyed per agent ID.
Both are sent to the chat WebSocket as query parameters so the agent can recognize returning visitors. No personal data is collected by the fingerprint — it's a hash of stable browser characteristics.
Page context
Each session sends the host page's context to the agent on connect:
pageUrl— the URL the widget was opened on.pageReferrer— the previous URL.userLocale— the visitor's browser locale.
Use these in your system prompt to make the agent context-aware (e.g., "if the visitor is on the pricing page, focus on plan comparison").
Style isolation
The widget renders inside a Shadow DOM. Your site's CSS will not affect the widget, and the widget's CSS will not affect your site. You don't need to worry about specificity wars or CSS leaks.
SSR frameworks (Next.js, Remix, etc.)
The auto-init code waits for the browser's load event before mounting,
which avoids the React hydration mismatch warnings you'd otherwise get on
SSR-hydrating frameworks. No special setup needed for Next.js, Remix, or
similar.
If you use a Content Security Policy, add the CDN domain:
Testing locally
The widget connects to wss://app.ctpl.ai/ws/chat/{agentId} by default.
For local development against a self-hosted nerva-voice instance, override
the WebSocket URL on the agent's Embed → Advanced tab.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Widget doesn't appear | Script blocked by CSP, or data-agent-id missing. Check the browser console. |
| Widget appears but won't open | agent_xxx is wrong or the agent is inactive. Verify in the dashboard. |
| Widget overlaps a page element | Use data-position to relocate, or hide it on specific pages with window.ctplai.cbp.destroy(). |
| Conversation never connects | WebSocket connection failed. Check Network tab for the wss:// request. CSP connect-src may be blocking. |
| Hydration warnings on Next.js | Make sure the script is rendered inside <Script src="..." strategy="afterInteractive" /> or in a plain <script> tag with defer. Don't render inside a client component's render body. |
See also
- Chat agents — configure the agent that powers the widget
- Webhooks — get events when conversations happen
- Lead capture — capture leads via in-widget forms