Embedding the Widget
How to add the Chat9 chat widget to your site, with optional userHints for personalization.
Basic embed
Paste this snippet before the closing </body> tag:
data-bot-id is your bot's public ID — safe to include in page HTML.
Loading widget.js registers window.Chat9Widget but does not mount any UI. You explicitly start the widget with Chat9Widget.start(config?) — call it with no arguments for an anonymous widget with default styling, or pass an options object to customize.
Do not add
asyncordeferto thewidget.jstag — the loader readsdocument.currentScriptsynchronously and will not register without it.
Configuration
All options are arguments to Chat9Widget.start():
| Field | Default | Notes |
|---|---|---|
mode | "bubble" | "bubble" or "inline" |
target | — | DOM element id for inline mode |
theme | "light" | "light" or "dark"; the whole chat window follows it |
color | "#7C3AED" | Color of the bubble, the send button and your visitor's messages (#RGB, #RRGGBB) |
position | "right" | "right" or "left" |
topClearance | 0 | px reserved at the viewport top (e.g. height of a fixed navbar) |
userHints | — | See userHints |
Modes:
- Bubble — floating button, bottom corner of the page. The window has a close button in its header, and Escape closes it; the bubble hides while the window is open. On phones it opens full screen.
- Inline — renders inside a container you provide. Add
<div id="chat9-widget"></div>where you want the widget. A target with a fixedheight(or one stretched by flex or grid) is filled; otherwise the widget is 600px tall. The target must be displayed (notdisplay: none) when the widget starts.
Routing from Russia
If your site primarily serves visitors in Russia, load the widget from the Russian edge proxy. Replace widget.getchat9.live with widget-ru.getchat9.live in the script src — both the widget bundle and the API origin route through it automatically:
This bypasses traffic throttling that some Russian ISPs apply to Vercel/Railway. Visitors outside Russia work the same way — they just hit the RU edge with a small extra hop. If you serve mixed audiences, detect the visitor's country server-side and emit a different src per request, or A/B test.
CSP
If your site uses a Content Security Policy, default origins:
If you load the widget from the Russian edge proxy (widget-ru.getchat9.live), allow these origins instead:
Where to get the embed code
- Log in to your Dashboard at https://getchat9.live.
- Copy the embed block from the Install widget page.
Lifecycle API
window.Chat9Widget exposes an explicit lifecycle. The script downloads once; you control when the chat appears, when it goes away, and what identity it carries.
| Method | Behavior |
|---|---|
Chat9Widget.start(config?) | Mount the FAB and chat iframe. No-op (with a console warning) if already started — call stop() first to reconfigure. |
Chat9Widget.stop() | Unmount the DOM and listeners. window.Chat9Widget stays registered; the script does not re-download. Call start() again to bring it back. |
Chat9Widget.setHints(hints | null) | Push a new identity into a running widget. If the widget is stopped, the hints are remembered for the next start(). Pass null to clear (anonymous). |
Chat9Widget.isStarted() | true while the widget is mounted. |
Chat9Widget.destroy() | Terminal teardown. Removes the DOM and deletes window.Chat9Widget. To use the widget again on this page you must reload widget.js. Most consumers want stop() instead. |
Session continuity
Conversation history is stored on the server. The widget keeps a session_id in the visitor's browser (localStorage) for up to 24 hours — on return visits it fetches the previous conversation from the server using that ID, so history persists across page reloads.
When userHints includes user_id or email, the storage key is scoped per visitor — different users on the same device get separate sessions.
A user_id personalizes the conversation (greeting, language, escalation details), keeps it in the same browser, restores it on another device or after localStorage is cleared, and groups the visitor's sessions in the Inbox. Without a key the ID is not verified: anyone who knows or guesses a visitor's user_id can open that visitor's conversation in the widget. Use unguessable IDs, or turn on identity verification if conversations may contain anything private. An email-only hint or an anonymous visitor (no userHints) always starts a fresh session on a new browser.
Escalating to a human agent does not end the conversation. If the visitor's email is already known, the bot goes on answering their questions normally while the ticket is open. If it is not — the default for anonymous visitors — the bot asks for an email address and keeps asking until it gets one, because support replies by email. After the handoff the bot asks whether it can help with anything else; if the visitor declines, the bot says goodbye and the chat stays open — the next message is answered as usual. There is no closed state and no "start new chat" control: a conversation only rolls over into a fresh one after a week without messages, and the previous one stays visible above a "New conversation" separator.
Greeting is shown once per browser, ever — per bot, and per identified user when you pass user_id or email. A returning visitor is not greeted again, even when a new conversation starts after a week without messages. The greeting is shown in userHints.locale if you set one, otherwise in the browser language, otherwise in English. It is not saved into the conversation, so it does not appear in the Inbox or after a page reload. Clearing browser storage or using a private window counts as a new browser, so the visitor is greeted again.
Opening the chat bubble, or loading a page with an inline widget, does not start a conversation. A conversation starts with the visitor's first message, so visitors who open the widget and never write do not create empty conversations.
Clarification: the bot may ask one follow-up question when a request is ambiguous. It appears as part of the normal text reply.
Login / logout in single-page apps
Because start, stop, and setHints are explicit, the widget can survive a full SPA login/logout cycle without reloading widget.js.
Substitute your own brand color in color (any #RGB/#RRGGBB) — or omit it for the default violet. All other options listed in Configuration are supported here too.
setHints() updates a running widget in-place — you do not need to stop and start to change identity.
For server-rendered or multi-page apps, the simplest pattern is to render the embed snippet on every page and pass the right userHints per request. On logout the page navigates and the widget is recreated naturally.
userHints
By default the widget is anonymous. Pass userHints to personalize the session — either at start() time, or later via setHints():
Hints are used for greeting personalization, language selection, and escalation email metadata.
Note: Hints come from the browser and can be modified by anyone. Do not use them for access control, paid-tier gating, or audit logs. Treat them as personalization suggestions only.
Fields
| Field | Notes |
|---|---|
user_id | Stable ID from your system. Personalizes the conversation, restores it across devices and groups the visitor's sessions in the Inbox. If you have an identity key, these last two need a valid user_hash. |
user_hash | Optional server-computed proof that user_id belongs to this visitor. Needed for cross-device restore and Inbox grouping only if you have an identity key. See Identity verification. |
email | Used in escalation notifications. Malformed values are dropped. |
name | Display name. |
plan_tier | "free", "starter", "growth", "pro", "enterprise". Affects escalation priority. |
audience_tag | Free-form segment label. |
locale | BCP-47 (e.g. "en-US", "ru-RU"). Overrides browser locale for language selection. |
Escalation priority
| Trigger | Other tiers / None | "pro" or "enterprise" |
|---|---|---|
| User requests a human | High | Critical |
| No relevant answer found | Medium | High |
| Answer rejected by quality check | Medium | Medium |
Identity verification
Without a key, user_id is trusted as-is: anyone who knows or guesses a visitor's user_id can open that visitor's conversation. Identity verification is a stronger, optional level: Chat9 confirms that a user_id really belongs to the visitor sending it. You choose whether to use it, and it needs a server that signs.
Set up
-
In the Dashboard, go to Settings → Widget → Widget identity verification and generate a key. You can reveal/copy it later, and rotate it if it leaks — rotating invalidates the old key immediately, so any requests still signed with it stop verifying.
-
On your server (never in the browser), compute
user_hashfor the visitor:user_hashis a lowercase hex HMAC-SHA256 of the exactuser_idstring, using the key as the HMAC key. Compute it wherever you already render the page for a logged-in visitor, and pass it down to the client alongsideuser_id.The server trims
user_idand keeps at most 200 characters before verifying — sign exactly the trimmed id you send, since a longer id won't verify. -
Pass both in
userHints, atstart()or viasetHints():
How it works with and without a key
- Without a key:
user_idworks fully. It personalizes the conversation, keeps it in the same browser, restores it across devices and afterlocalStorageis cleared, and groups the visitor's sessions in the Inbox. The ID is not verified. - With a key: only visitors with a valid
user_hashrestore conversations across devices and are grouped in the Inbox. An unsigneduser_idonly personalizes the conversation and keeps it in the same browser. - A
user_idthat starts withhint:is always rejected and treated as absent — that prefix is reserved for the server's own synthesized IDs. emailis never used to restore a conversation, verified or not — it's too easy to guess.- In the dashboard Inbox, a visitor's sessions are grouped into one row by
user_id; with an identity key, only for visitors with a validuser_hash.