Developer embed JS API
The chat widget exposes a window.ChatWidgetV3 API for host pages to drive it imperatively, listen to widget events, and seed per-page context. Useful when:
- You want to open the chat from your own button (instead of the floating bubble).
- You want to track widget usage in your analytics.
- You want to feed the bot context the user is already looking at.
In this guide:
- Imperative commands
- Event listeners
- Pre-mount queueing
- Initial messages and page context
- Backward compatibility
- Examples
The API at a glance
window.ChatWidgetV3.open();
window.ChatWidgetV3.close();
window.ChatWidgetV3.resetChat();
window.ChatWidgetV3.sendMessage('hello');
const off = window.ChatWidgetV3.onMessage(msg => console.log(msg));
window.ChatWidgetV3.onOpen(() => trackEvent('chat_opened'));
window.ChatWidgetV3.onClose(() => {});
window.ChatWidgetV3.onHumanHandoff(() => {});
window.ChatWidgetV3.onSessionStart(({ identity }) => {});
window.ChatWidgetV3.onError(err => Sentry.captureException(err));
off(); // unsubscribe one listener
Imperative commands
| Method | Description |
|---|---|
open() | Open the chat panel. |
close() | Close the chat panel. |
resetChat() | Discard the current session and start a fresh one. The next user message will start a new conversation, and onSessionStart will fire. |
sendMessage(text) | Send text as if the user typed it. Useful for quick-action buttons that pre-populate questions. |
All commands are no-ops before the widget mounts; calls are queued and replayed on mount.
Event listeners
Each event registrar returns an unsubscribe function — call it to stop receiving the event.
| Event | Payload | Fires when |
|---|---|---|
onMessage(cb) | { role, content, timestamp, ... } | Every new user or assistant message. |
onOpen(cb) | void | The widget transitions from closed to open. |
onClose(cb) | void | The widget transitions from open to closed. |
onHumanHandoff(cb) | { conversationId, reason } | A live agent takes over the conversation. |
onSessionStart(cb) | { sessionId, identity } | A new session starts. identity reflects JWT verification status. |
onError(cb) | Error | A widget-level error occurs. |
Pre-mount queueing
You can register listeners synchronously in the same <script> block where you load the widget — even before the widget has finished mounting. The bridge buffers them and replays on mount.
<script src="https://cdn.hilalsoftware.tools/chat_v3.js" data-org-id="..." data-bot-id="..."></script>
<script>
window.ChatWidgetV3.onMessage(msg => track('chat_msg', msg));
window.ChatWidgetV3.onOpen(() => track('chat_open'));
</script>The queue has a defensive cap to prevent runaway memory use from broken integrations — register listeners deliberately, not in loops.
Initial messages and page context
Seed the chat with information about the page the user is on. The bot uses this as context when replying.
<script
src="https://cdn.hilalsoftware.tools/chat_v3.js"
data-org-id="..."
data-bot-id="..."
data-initial-page-context="Visitor is on the pricing page; viewing the Enterprise plan"
data-initial-floating-messages='["What is your cheapest plan?", "Do you offer refunds?"]'
></script>Or via init():
window.HilalChat.init({
organizationId: '...',
botId: '...',
initialMessages: {
pageContext: 'Visitor is on the pricing page; viewing the Enterprise plan',
floating: ['What is your cheapest plan?', 'Do you offer refunds?']
}
});pageContextis a string injected as background context. Not visible to the user.floatingis an array of suggestion buttons rendered as clickable bubbles next to the chat icon.
Backward compatibility
The four legacy methods still work:
| Legacy method | Behavior |
|---|---|
mount() | Mount the widget (called automatically; rarely needed). |
unmount() | Tear down the widget. |
isReady() | Boolean — has the widget mounted? |
destroy() | Remove the widget from the DOM. Preserves event listeners so SPA route changes that re-mount keep your callbacks. |
The original 4-method bootstrap surface is unchanged and runs alongside the new API.
Examples
Track every chat open in your own analytics
window.ChatWidgetV3.onOpen(() => {
// segment / amplitude / mixpanel / your own
analytics.track('Chat Opened');
});Open the chat from your own button
<button onclick="window.ChatWidgetV3.open()">Need help?</button>Pre-fill a question when the user clicks a CTA
<button onclick="
window.ChatWidgetV3.open();
window.ChatWidgetV3.sendMessage('How does refund work?');
">Refund question?</button>Notify your CRM when an agent takes over
window.ChatWidgetV3.onHumanHandoff(({ conversationId }) => {
fetch('/internal/notify-crm', {
method: 'POST',
body: JSON.stringify({ conversationId })
});
});React to verified identity (JWT)
window.ChatWidgetV3.onSessionStart(({ identity }) => {
if (identity?.identitySource === 'verified') {
// Hide the user-info form on your page since we already know who this is
document.getElementById('email-form').remove();
}
});Limitations
- Single instance per page. Mounting two widgets on the same page is not supported.
- No
off(event, cb)/removeAllListeners(). Use the unsubscribe function returned by each registrar —AbortSignal-style. - Shopify extension parity. The Shopify storefront entry point is on a different bootstrap flow; this API ships there in a follow-up.
Troubleshooting
window.ChatWidgetV3 is undefined. The widget script hasn’t loaded yet. Move your code below the<script src=".../chat_v3.js">tag, or wrap it in a deferred handler.- Listeners fire twice. You re-registered without calling the previous unsubscribe — verify you’re storing and calling the returned
offfunction. sendMessagedoesn’t appear in the UI. The widget might not be open. Callopen()first; the queued message renders on mount.