#Embedding the Chat Widget
Status: Available (current parameters and blocks 1 to 5). Planned: widget v2 parameters, block 6 "Inline block on a page", and block 7 "Your own 'Ask a question' button" in their target form. The status is marked on each parameter and block.
Purpose: add the AI seller chat window to your site with a single <script> line or through the module loader, and configure its look and behavior.
#Contents
- When to use it
- Access and requirements
- Ways to connect
- Parameters: current (Available)
- v2 parameters (Planned)
- Channel settings in the dashboard
- Site domain and frame headers
- What the widget looks like
- Ready-made blocks
- Errors and diagnostics
- Known limitations
- Notes
- Open questions
#When to use it
- You need a chat on your site without building an interface.
- You want to change the color, greeting, and position from the dashboard without touching page code.
- For your own chat design, use the client Chat API; for a chat in your own messenger, use the server API.
#Access and requirements
| Authentication | Not needed. channel_id is a public channel identifier, not a secret |
| Paid | AI replies are charged to the tenant's wallet (Overview) |
| Personal data | The page address, title, and referrer, and the first touch, with the visitor's consent (Page, source, and UTM) |
| What you need beforehand | An active channel of type web_chat and its ID (in the dashboard: "Settings" → the "Web chat" channel → the "Embed code" block) |
| Domain | Your site's domain must be among those allowed to frame the chat (below) |
| Addresses | RU https://formula-cream.pro, EN https://wfacademy.org, ID https://wfacademy.id |
The script and the frame are served by the cell where the channel was created. A channel ID from one cell does not work on another.
#Ways to connect
| Way | What you insert | When to choose it |
|---|---|---|
| Direct tag | <script src="https://<cell>/widget.js" data-channel="…"> | You control the site template and want to set parameters explicitly |
| Global object | window.SapportWidget = {…} before the widget.js tag | The parameters are known in page code (a templating engine, an SPA) |
| Through the loader | The Bitrix/WordPress module or loader.js?k=… | The site is bound to Sapport, and you want the chat, counter, and consent in a single connection (Loader) |
The order of sources for each parameter is: tag data attribute → window.SapportWidget key → default value.
#Parameters: current (Available)
#Tag attributes and window.SapportWidget keys
| Attribute | Object key | Type | Required | Constraints | Default | Example |
|---|---|---|---|---|---|---|
data-channel | channelId | string (UUID) | yes | The ID of a web_chat channel. Without it the widget logs an error to the console and does not start | none | 00000000-0000-4000-8000-000000000000 |
data-position | position | string | no | Only bottom-right or bottom-left. Any other value breaks the placement | bottom-right | bottom-left |
data-theme | theme | string | no | light or dark. Only the value dark selects the dark window theme; the theme also affects the label above the button | light | dark |
data-url | url | string (URL) | no | The cell base URL without a trailing /. Determines where the frame and API are loaded from | the origin of the script address | https://formula-cream.pro |
data-bubble-text | bubbleText | string | no | The label above the button. Rendered as text (not HTML). One line, up to 240 px wide, does not wrap | empty | Need help? |
data-delay | delay | integer | no | Seconds before the widget starts. 0 means immediately | 0 | 10 |
Keys without a data attribute:
| Key | Type | Values | Default | Meaning |
|---|---|---|---|---|
window.SapportWidget.consent | boolean | Only false turns off the extended context; any other value counts as "consent given" | true | false: the frame receives only the page path (without the query, title, and referrer), and the first touch is not recorded. The key is read on every open, so it can be changed after the script has loaded |
window.SapportWidget.firstTouch | boolean | false turns off recording and reading of the first touch | enabled | Set by the loader together with consent |
window.sapportPage | object {type, id} | type: Latin letters, _, -, up to 20 characters, no digits; id: letters, digits, _, -, up to 40 characters | none | The type and ID of the page entity (product, course, article). Read on the first open of the chat |
window.SapportWidgetmust be set before thewidget.jstag. If you changeconsentlater, change the property of the same object rather than replacing the whole object.
#What is passed to the frame
When the window is first opened, the widget creates an iframe with the address https://<cell>/widget/<channel_id> and parameters.
| Address parameter | Source | Limit | Without consent |
|---|---|---|---|
theme | data-theme | none | passed |
page | the page address | 300 characters | origin and path only, no query |
title | document.title | 200 | not passed |
pt, pid | window.sapportPage | 20 / 40 | passed |
ref | document.referrer | 300 | not passed |
ft | first touch (JSON) | 1200 | not passed |
widget.js does not put a language in the frame address: the frame language comes from the channel setting locale, and if it is absent, from the visitor's browser language (ru, en, ms, and id are supported; the default is en).
#What the widget does
- Loads the channel settings with a
POST /api/widget/initrequest (this is the only route open to requests from other sites). - Draws a round 60×60 px button. The window (
iframe) is created only on the first click of the button: until then, the page address is not sent to the platform. - The window is 380×560 px above the button; on screens up to 440 px wide it takes almost the whole screen (width
100vw − 24 px, height100dvh − 100 px). - The container's
z-indexis2147483647. It cannot be changed with parameters. - Unread counter on the button: while the window is closed, each new message increases the number (
9+above nine). - The internal
sapport-*CSS classes are not a contract and may change.
#v2 parameters (Planned)
Status: Planned. The parameters below do not work in the current version of
widget.js: unknown attributes are ignored. Do not use them in production markup until they are announced in the changelog.
| Attribute | Object key | Type | Values | Default | Meaning |
|---|---|---|---|---|---|
data-locale | locale | string | ru, en, id (the set is being finalized) | from the channel or browser | Frame language |
data-open | open | string | auto, never, or after:N (N in seconds) | never (presumably) | Open the window automatically |
data-mode | mode | string | bubble, inline, button | bubble | bubble is a floating button; inline puts the window inside the page; button shows only the window, on demand |
data-container | container | string (CSS selector) | an element selector | none | Where to insert the window when mode=inline |
data-color | color | string | a #rrggbb color | the channel color | Accent color; overrides the channel setting |
data-greeting | greeting | string | text | the channel greeting | Greeting; overrides the channel setting |
data-launcher | launcher | string | default, none | default | none means do not draw the standard button: your code opens the chat |
data-z-index | zIndex | integer | a number | 2147483647 | Stacking order |
The design does not fix the defaults marked "presumably"; the final values will be in the reference when this ships.
#Channel settings in the dashboard
Part of the look can be changed without editing page code: the settings are stored in the channel and loaded by the init request.
| Dashboard setting | Channel key | Effect | Where it applies |
|---|---|---|---|
| Welcome message | greeting | The first message in the window. If not set, a greeting in the channel language is used | in the window, on every load |
| Widget color | color | The color of the button and window header (#6366f1 by default) | button and window |
| Avatar URL | avatar_url | The avatar in the window header | window |
| Position | position | bottom-right or bottom-left | only through the embed code |
| Theme | theme | light or dark | only through the embed code |
| Bubble text | bubble_text | The label above the button | only through the embed code |
| Display delay | delay | Seconds before the button appears | only through the embed code |
| Consent-to-terms text | consent_text | Up to 600 characters. If set, the visitor must tick the consent | window |
| Terms link | consent_url | An http(s) address, up to 500 characters | window |
Position, theme, bubble text, and delay reach the widget only as attributes of the code copied from the dashboard. If you connect the widget through the loader (Bitrix/WordPress), these four values are not applied: the loader passes only the channel ID, the address, and the consent flags.
Channel keys that the server reads but that have no fields in the dashboard: locale (frame language), pre_chat_form (the pre-chat form), and ai_daily_limit (the daily limit on AI replies). They are set by editing the channel record; there is no interface for them today.
The embed code in the dashboard is assembled by string substitution: quotes in the bubble text are not escaped. Do not use the
"character in the bubble text.
#Site domain and frame headers
The chat window is a page on the cell's domain, embedded in your site. The browser will show it only if your site is allowed by the Content-Security-Policy: frame-ancestors header.
The following are allowed:
- the cell itself;
- the tenant's domains (the tenant's
domainslist); - the domains of sites bound to the tenant through the connector (with
https://andhttps://www.); - on EN, additionally a static partner list.
Conditions: the channel is active; the tenant is active and has the "Promotion" module available. The response is cached for 60 seconds. If the database is unavailable, the header is built from 'self' only.
| Symptom | Cause | What to do |
|---|---|---|
| The button is there, but on click the window is empty or shows the browser error "refused to connect" | The site's domain is not among the allowed ones | Add the domain to the tenant or bind the site; wait up to a minute |
| There is no button at all | data-channel is not set, the script did not load, or the data-delay has not elapsed. If init returned an error (including 404), the button is still drawn with default settings | See diagnostics |
The
initrequest does not check the domain: the button may appear even where the window will be blocked.
#What the widget looks like
The mockups are schematic; exact spacing and colors are determined by the theme.
#Button
Closed: Unread messages: Open:
Need help? ┌────┐
┌─────────────┐ │ ✕ │
└─────────▽───┘ ┌─┐ └────┘
╭────────╮ ╭─┤3├────╮
│ ◯◯ │ 60×60 px │ └─┘ ◯◯ │ a cross instead of the icon
╰────────╯ ╰────────╯
the label above the button — red counter in the corner,
data-bubble-text "9+" above nine
#Window on desktop, 380×560
┌────────────────────────────────┐
│ ◯ Channel name ✕ │ header: avatar,
│ ● we reply instantly │ name, close
├────────────────────────────────┤
│ ┌───────────────────────┐ │
│ │ Hello! How can we │ │ greeting
│ │ help? │ │ (from channel settings)
│ └───────────────────────┘ │
│ ┌───────────┐ │
│ │ How much │ │ visitor's message
│ │ is it? │ │
│ └───────────┘ │
│ ◌ ● ● ● │ typing indicator
├────────────────────────────────┤
│ [ Type a message… ] [+] > │ input, attachment
├────────────────────────────────┤
│ Powered by Formula AI │ label in the frame
└────────────────────────────────┘
╭────────╮
│ ✕ │ 60×60 button in the corner, 20 px margin
╰────────╯
#Window on a phone, width ≤ 440 px
┌─────────────────────────────┐
│ ◯ Channel name ✕ │ width: 100vw − 24 px
│ ● we reply instantly │ height: 100dvh − 100 px
├─────────────────────────────┤ 84 px from the bottom edge,
│ ┌─────────────────┐ │ 12 px at the sides
│ │ Hello! │ │
│ └─────────────────┘ │
│ ┌────────────┐ │
│ │ Hi │ │
│ └────────────┘ │
├─────────────────────────────┤
│ [ Type a message… ] [+]> │
├─────────────────────────────┤
│ Powered by Formula AI │
└─────────────────────────────┘
╭────────╮
│ ✕ │
╰────────╯
#Pre-chat form
Shown if the channel has pre_chat_form enabled and the visitor has no session yet. Fields: name, email, phone; required sets which are required.
┌────────────────────────────────┐
│ ◯ Channel name ✕ │
├────────────────────────────────┤
│ (◯◯) │
│ Start a conversation │
│ Fill in the details below to │
│ begin. │
│ │
│ Name * │
│ [____________________________] │
│ Email * │
│ [____________________________] │
│ Phone │
│ [____________________________] │
│ │
│ [ Start chat ] │
└────────────────────────────────┘
The server uses only the name (sender_name) from the form. The email and phone are sent but not read; see the known limitation.
#Consent bar
Shown above the input field if the tenant set consent_text. Until the checkbox is ticked, the message is not sent.
├────────────────────────────────┤
│ ☐ I agree to the terms of │
│ the offer. Open the offer │ link from consent_url
│ Tick the consent to │ (if set)
│ write. │
├────────────────────────────────┤
│ [ Type a message… ] [+] > │
#Typing indicator
│ ◌ ● ● ● │ while the AI prepares a reply (a bubble with three dots)
│ ◯ ● ● ● Anna is typing… │ an operator is typing: avatar and name
#Operator with an avatar
After an operator joins, the header shows their name, and below the header there is a bar "You are talking with …".
┌────────────────────────────────┐
│ ◯ Anna ✕ │ the operator's name instead of the channel name
│ ● online │
├────────────────────────────────┤
│ (◯) You are talking with Anna │ bar with a 24 px avatar
├────────────────────────────────┤
│ Anna │ label above the operator's message
│ (◯) ┌──────────────────────┐ │ 26 px avatar next to the bubble
│ │ Of course, I'll tell │ │
│ └──────────────────────┘ │
#Ready-made blocks
In all blocks, replace 00000000-0000-4000-8000-000000000000 with your channel ID, and formula-cream.pro with your cell's domain (wfacademy.org for EN, wfacademy.id for ID). Files to copy are in examples/.
#Block 1. Floating button
Status: Available. File:
examples/floating-button.html.
The standard connection: a round button in the bottom-right corner.
<script
src="https://formula-cream.pro/widget.js"
data-channel="00000000-0000-4000-8000-000000000000"
async></script>
For the mockup, see the button and the window.
If the tag loads synchronously (without async), the widget starts after DOMContentLoaded; with async, right after the script loads.
#Block 2. Button on the left with a label
Status: Available.
<script
src="https://formula-cream.pro/widget.js"
data-channel="00000000-0000-4000-8000-000000000000"
data-position="bottom-left"
data-theme="dark"
data-bubble-text="Need help?"
async></script>
┌───────────────────────────────────────────┐
│ page │
│ │
│ │
│ Need help? │
│ ┌──────────────┐ │
│ └──▽───────────┘ │
│ ╭────────╮ │
│ │ ◯◯ │ │
│ ╰────────╯ │
└───────────────────────────────────────────┘
The equivalent through the global object:
<script>
window.SapportWidget = {
channelId: '00000000-0000-4000-8000-000000000000',
position: 'bottom-left',
theme: 'dark',
bubbleText: 'Need help?'
};
</script>
<script src="https://formula-cream.pro/widget.js" async></script>
#Block 3. Delayed start
Status: Available.
The button appears after the given number of seconds. Until then, the script draws nothing and does not contact the platform.
<script
src="https://formula-cream.pro/widget.js"
data-channel="00000000-0000-4000-8000-000000000000"
data-bubble-text="Questions about the course?"
data-delay="15"
async></script>
Fifteen seconds after the script loads, the widget requests the channel settings and draws the button. The window does not open by itself: automatic opening will arrive in v2 (data-open="after:N", Planned).
#Block 4. Product or course page context
Status: Available. File:
examples/product-page-context.html.
The widget passes the page address and title by itself. Set the entity type and ID (course, product, article) with the window.sapportPage object before the chat is first opened. To be sure the value is ready, set it before the widget tag.
<script>
window.sapportPage = { type: 'course', id: 'hydrolat-basics' };
</script>
<script
src="https://formula-cream.pro/widget.js"
data-channel="00000000-0000-4000-8000-000000000000"
async></script>
What the model receives (in the prompt, as browser data, not as instructions):
Visitor's current page: https://shop.example.com/courses/hydrolat-basics · "[course #hydrolat-basics] Hydrolat Basics"
| Field | Rule |
|---|---|
type | Latin letters, _, -, up to 20 characters. A value with digits or longer than 20 characters is not passed |
id | A string or number; letters, digits, _, -, up to 40 characters |
| Title | document.title, up to 150 characters including the type prefix |
The context is read once, when the window is first opened. Navigating to another page without a reload (an SPA) does not update the context;
Sapport.setPage()from the JS API (Planned) is intended for that. More: Page, source, and UTM.
The Bitrix and WordPress modules output window.sapportPage themselves.
#Block 5. Cookie consent through the loader
Status: Available. File:
examples/consent-banner.html.
Suited to a site bound to Sapport: the loader attaches the chat, the counter, and behavior collection, and itself waits for the visitor's consent. The chat channel is set by the site settings (chat, chat_channel_id), so the tag has no data-channel.
<!-- 1. Consent queue stub: BEFORE the loader -->
<script>
window.sapport = window.sapport || {
q: [],
consent: function (v) { this.q.push(['consent', !!v]); }
};
</script>
<!-- 2. The cell loader. k is the site's public key (not a secret) -->
<script async src="https://formula-cream.pro/api/v1/connector/loader.js?k=0123456789abcdef0123456789abcdef"></script>
<!-- 3. Your cookie banner calls consent -->
<script>
document.getElementById('cookie-accept').addEventListener('click', function () {
window.sapport.consent(true);
});
document.getElementById('cookie-decline').addEventListener('click', function () {
window.sapport.consent(false);
});
</script>
┌─────────────────────────────────────────────────────┐
│ We use cookies to understand where visitors come │
│ from and to offer suggestions in the chat. │
│ │
│ [ Accept ] [ Decline ] │
└─────────────────────────────────────────────────────┘
| Site mode | What the chat does before the visitor decides | After consent(true) |
|---|---|---|
wait | The button and chat work, but SapportWidget.consent=false: the frame receives only the page path, and the first touch is not recorded | The extended context and the first touch turn on |
immediate | Consent is considered given right away | none |
As an event: window.dispatchEvent(new CustomEvent('sapport:consent', { detail: true })); the form { granted: true } is also accepted. consent(false) returns to the minimal-context mode.
Consent in the chat is separate: it is not a cookie banner but consent to the tenant's terms (the consent bar).
A manual installation without the loader but with the same behavior:
<script>
window.SapportWidget = {
channelId: '00000000-0000-4000-8000-000000000000',
consent: false,
firstTouch: false
};
// after the visitor consents, on the same object:
function onCookieAccepted() {
window.SapportWidget.consent = true;
window.SapportWidget.firstTouch = true;
}
</script>
<script src="https://formula-cream.pro/widget.js" async></script>
The Bitrix/WordPress module installs this loader itself; you do not need to add the tag manually.
#Block 6. Inline block on a page
Status: Planned (
data-mode="inline",data-container). There is no supported interim option; below is a workaround that works according to the code but is not a contract.
The target v2 markup (does not work today):
<div id="sapport-chat" style="height:560px"></div>
<script
src="https://formula-cream.pro/widget.js"
data-channel="00000000-0000-4000-8000-000000000000"
data-mode="inline"
data-container="#sapport-chat"
async></script>
┌───────────────────────────────────────────────────────────┐
│ Questions about the course? │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ ◯ Channel name ✕ │ │
│ │ ┌───────────────────────┐ │ │
│ │ │ Hello! │ the chat window inside │ │
│ │ └───────────────────────┘ the page text │ │
│ │ [ Type a message… ] [+] > │ │
│ └───────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────┘
Workaround today. The chat page opens as a regular iframe:
<iframe
title="Chat"
src="https://formula-cream.pro/widget/00000000-0000-4000-8000-000000000000?theme=light"
style="width:100%;max-width:380px;height:560px;border:0"
allow="clipboard-write"></iframe>
Limitations of the workaround:
- your site's domain must be allowed by the
frame-ancestorsheader (above); - the frame does not receive the page address, title,
sapportPage, or first touch: onlywidget.jscollects them. If you need the context, add it to the address manually:&page=<address>&title=<title>(values inencodeURIComponent);pt,pid, andrefwork the same way; - inside the frame, the "Close" (✕) button remains. It sends the parent a
sapport:closemessage that nobody handles withoutwidget.js; - the frame does not stretch wider than 380 px or taller than 560 px by itself, and is centered;
- the workaround is not described as a public contract, and its behavior may change.
#Block 7. Your own "Ask a question" button
Status: Planned (
data-launcher="none"andSapport.open()). There is no supported way to open the window from your own code today.
The target v2 markup (does not work today):
<button id="ask">Ask a question</button>
<script
src="https://formula-cream.pro/widget.js"
data-channel="00000000-0000-4000-8000-000000000000"
data-launcher="none"
async></script>
<script>
document.getElementById('ask').addEventListener('click', function () {
window.Sapport.open();
});
</script>
┌───────────────────────────────────────────────┐
│ Course "Hydrolat Basics" │
│ [ Enroll ] [ Ask a question ] │ your button
│ │ │
│ ▼ │
│ ┌────────────────────┐ │
│ │ chat window 380×560│ │
│ └────────────────────┘ │
└───────────────────────────────────────────────┘
What you can do today.
- Use the standard floating button (blocks 1 to 3) and label it with
data-bubble-text="Ask a question". - For an "Ask a question" link, open the chat page in your own window or in a pop-up
iframe(the workaround from block 6). - Click the standard button programmatically (
document.querySelector('.sapport-bubble').click()). This is not recommended: the internalsapport-*class names are not a contract and may change without notice.
#Errors and diagnostics
| Symptom | Likely cause | What to check |
|---|---|---|
The console shows [Sapport] Missing channel ID… | data-channel and SapportWidget.channelId are not set | The tag attribute; window.SapportWidget before the tag |
| The button does not appear | The script did not load (your site's CSP forbids the cell's script-src), or the data-delay | The Network tab; the site's CSP |
| The button is there, but the window is empty | The site's domain is not allowed for the frame; your site's CSP forbids the cell's frame-src | Domain |
| The button is visible with the default color and greeting | The init request returned an error: the channel is off, the tenant is disabled, or the network is unavailable | The channel status in the dashboard; the Network tab |
A message is not sent, the response is 503 widget_not_configured | Session signing is not configured on the server | Contact the cell's support |
A message is not sent, 400 consent_required | The consent to the terms is not ticked | The consent bar |
429 Too many requests | The rate limit was exceeded | Limits |
| No counter appears on the button | The postMessage from the frame did not arrive: the page origin was not determined | Your site's referrer policy (Notes) |
In the browser's Network tab, look for the requests widget.js, /api/widget/init, and, after opening, /widget/<channel_id>.
#Known limitations
These items reflect gaps between expectation and behavior; the wording is neutral, and the source is the code as of 11 Oct 2026.
| No. | Limitation | What to do |
|---|---|---|
| 1 | Pre-chat form: the email and phone fields are sent, but the server does not read them; only the name reaches the contact card | If you need contact details, use the AI seller's text question or identify in v2 (Planned) |
| 3 | When connected through the loader, the chat ignores position, theme, bubble_text, and delay from the channel settings | For a non-standard position, use the direct tag |
| 4 | The fields pre_chat_form, locale, and ai_daily_limit have no interface in the dashboard; the frame language cannot be set in the embed code | Edit the channel record through support; data-locale arrives in v2 |
| 5 | init does not check the domain: the button may be visible where the window is blocked | Check the window after installation |
| 6 | The rules for the page type and ID differ between the modules and the widget. The Bitrix and WordPress modules accept a type of up to 32 characters, including digits, and an id of up to 64 characters; the widget accepts a type of up to 20 characters without digits and an id of up to 40 characters | Set type from Latin letters and _, -, up to 20 characters, and keep id to 40 characters or fewer: then both the module and the widget accept the value. Otherwise the widget silently drops the value |
| 7 | The close button inside the frame sends sapport:close with the target *; the frame's other messages go to the parent's exact origin | Do not trust the message content: it carries no data |
| 12 | The visitor session is stored in the frame's sessionStorage: a new tab starts a new conversation | Planned: visitor identification through identify |
| 22 | The embed code in the dashboard is assembled by string substitution: the bubble text (bubble_text) is not escaped | Do not use the " character in the bubble text; check the embed code before publishing |
The numbering matches the design's list of discrepancies.
#Notes
- Sizes: the button is 60×60 px; the window is 380×560 px; at widths ≤ 440 px, the window takes almost the whole screen.
- Referrer policy. The frame's messages (
sapport:new-message,sapport:close) go to the page origin. The frame determines the origin fromancestorOrigins(Chrome, Safari) or fromdocument.referrer(Firefox). If your site sendsReferrer-Policy: no-referrer, the unread counter on the button will not work, but the chat itself keeps working. - Hiding on pages. Load the tag only on the pages you need (for example, not on the checkout page): there is no "do not show here" parameter.
- Cache. The
widget.jsscript and theinitresponse are not versioned by address; updates are picked up under the browser's ordinary caching rules. - Label in the window. The bottom of the window shows the label "Powered by Formula AI"; there is no parameter in the code to turn it off.
- Changelog: 90-changelog.
#Open questions
- Where in the dashboard the tenant manages the
domainslist that determines the domains allowed to frame the chat: this could not be confirmed in the interface. For bound sites, the domain is added automatically. - Where in the interface the site's public key
kcomes from for a manual loader installation (it is issued at binding and is not shown in the dashboard). - The defaults of
data-openanddata-localein v2, and whether the listed parameters are needed in the first version, are not fixed. - Whether a parameter is needed to turn off the "Powered by Formula AI" label (not described in the design).