Skip to main content
@custral/widget is the Custral chat widget as a React component, plus the headless engine underneath it. It uses a publishable key (pk_…), which is safe to ship to the browser.
No bundler, or not a React app? Load the standalone widget.js script instead. It is this same package, built as a self-contained bundle with React inlined.
This package was private (@repo/widget) until the browser packages were reorganised, and Settings once generated an install snippet naming it. That name was never on npm. Use @custral/widget.

Install

React and React DOM are peer dependencies (18 or 19), so the widget renders on your copy of React rather than bundling a second one.

The widget

One component, mounted once, anywhere in your tree. It renders its own bubble and panel and positions itself.
string
required
Your publishable key (pk_…), from Settings → Applications.
string
Target a specific widget channel. Defaults to the workspace’s first.
string
Defaults to https://api.custral.com.
boolean
Force dark or light. Omitted, the widget reads the host page.
{name?, email?, phone?}
Pre-fill who the visitor is. Skips the intake form.
string
A token minted by your server that vouches for who this visitor is. See below.

Identifying a visitor

visitor is convenient and self-reported: it fills the form in, and the person on the other end typed it. That is fine for lead capture and not fine for anything that has to be right. identityToken is the version the product will trust. Your server mints it with its secret key, so the claim comes from you rather than from the browser:
Never mint an identity token in the browser. It authenticates with your secret key, and anything that can call it can vouch for any of your users.
Without a token, a visitor’s identity is only ever what they typed into a chat box on a public page. With one, the conversation attaches to the right person in your CRM. See widget identity.

Building your own UI

The component is assembled from parts the package also exports, so you can replace the shell and keep the machinery.
useCustralChat owns the conversation: init, socket, messages, typing, intake, conversation list, booking. ChatBubble, ChatPanel, ChatHeader, ChatMessageList, ChatMessage, ChatInput, IntakeForm, ConversationList and SchedulePicker are the pieces it drives. For no React at all, CustralChat is the engine on its own:
Construct a headless client.
Loads widget config, restores any prior visitor from localStorage, and returns the resolved config (or null on failure).
Registers a callback for agent messages pushed in real time.
Sends a visitor message, creating a conversation on the first send.
Attaches self-reported contact info for lead capture.
Tears down the socket and in-memory session. Does not clear localStorage.

Troubleshooting

The widget needs a publishable key (pk_…), not a secret key. Create one in Settings → Applications. If init() returns null, the /widget/init call failed: check the browser console and confirm the key and channelId.
Delivery uses a WebSocket to /widget. If your environment blocks WebSockets, history still loads on demand, but pushed agent replies will not arrive. Look for Socket connected in the console.
Two copies of React in one page. React and React DOM are peers here for that reason; check your bundler is deduping them.
CustralChat and the components are all in @custral/widget now. Change the import and nothing else.