Developer embed JS API

Developer embed JS API

⚡ New — Available in PR #738

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

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

EventPayloadFires when
onMessage(cb){ role, content, timestamp, ... }Every new user or assistant message.
onOpen(cb)voidThe widget transitions from closed to open.
onClose(cb)voidThe 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)ErrorA 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?']
  }
});
  • pageContext is a string injected as background context. Not visible to the user.
  • floating is an array of suggestion buttons rendered as clickable bubbles next to the chat icon.

Backward compatibility

The four legacy methods still work:

Legacy methodBehavior
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 off function.
  • sendMessage doesn’t appear in the UI. The widget might not be open. Call open() first; the queued message renders on mount.

What’s next