◆ Sapport for developers API reference Error catalog OpenAPI
Sections

#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

  1. When to use it
  2. Access and requirements
  3. Ways to connect
  4. Parameters: current (Available)
  5. v2 parameters (Planned)
  6. Channel settings in the dashboard
  7. Site domain and frame headers
  8. What the widget looks like
  9. Ready-made blocks
  10. Errors and diagnostics
  11. Known limitations
  12. Notes
  13. Open questions

#When to use it

#Access and requirements

AuthenticationNot needed. channel_id is a public channel identifier, not a secret
PaidAI replies are charged to the tenant's wallet (Overview)
Personal dataThe page address, title, and referrer, and the first touch, with the visitor's consent (Page, source, and UTM)
What you need beforehandAn active channel of type web_chat and its ID (in the dashboard: "Settings" → the "Web chat" channel → the "Embed code" block)
DomainYour site's domain must be among those allowed to frame the chat (below)
AddressesRU 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

WayWhat you insertWhen 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 objectwindow.SapportWidget = {…} before the widget.js tagThe parameters are known in page code (a templating engine, an SPA)
Through the loaderThe 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

AttributeObject keyTypeRequiredConstraintsDefaultExample
data-channelchannelIdstring (UUID)yesThe ID of a web_chat channel. Without it the widget logs an error to the console and does not startnone00000000-0000-4000-8000-000000000000
data-positionpositionstringnoOnly bottom-right or bottom-left. Any other value breaks the placementbottom-rightbottom-left
data-themethemestringnolight or dark. Only the value dark selects the dark window theme; the theme also affects the label above the buttonlightdark
data-urlurlstring (URL)noThe cell base URL without a trailing /. Determines where the frame and API are loaded fromthe origin of the script addresshttps://formula-cream.pro
data-bubble-textbubbleTextstringnoThe label above the button. Rendered as text (not HTML). One line, up to 240 px wide, does not wrapemptyNeed help?
data-delaydelayintegernoSeconds before the widget starts. 0 means immediately010

Keys without a data attribute:

KeyTypeValuesDefaultMeaning
window.SapportWidget.consentbooleanOnly false turns off the extended context; any other value counts as "consent given"truefalse: 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.firstTouchbooleanfalse turns off recording and reading of the first touchenabledSet by the loader together with consent
window.sapportPageobject {type, id}type: Latin letters, _, -, up to 20 characters, no digits; id: letters, digits, _, -, up to 40 charactersnoneThe type and ID of the page entity (product, course, article). Read on the first open of the chat

window.SapportWidget must be set before the widget.js tag. If you change consent later, 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 parameterSourceLimitWithout consent
themedata-themenonepassed
pagethe page address300 charactersorigin and path only, no query
titledocument.title200not passed
pt, pidwindow.sapportPage20 / 40passed
refdocument.referrer300not passed
ftfirst touch (JSON)1200not 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

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

AttributeObject keyTypeValuesDefaultMeaning
data-localelocalestringru, en, id (the set is being finalized)from the channel or browserFrame language
data-openopenstringauto, never, or after:N (N in seconds)never (presumably)Open the window automatically
data-modemodestringbubble, inline, buttonbubblebubble is a floating button; inline puts the window inside the page; button shows only the window, on demand
data-containercontainerstring (CSS selector)an element selectornoneWhere to insert the window when mode=inline
data-colorcolorstringa #rrggbb colorthe channel colorAccent color; overrides the channel setting
data-greetinggreetingstringtextthe channel greetingGreeting; overrides the channel setting
data-launcherlauncherstringdefault, nonedefaultnone means do not draw the standard button: your code opens the chat
data-z-indexzIndexintegera number2147483647Stacking 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 settingChannel keyEffectWhere it applies
Welcome messagegreetingThe first message in the window. If not set, a greeting in the channel language is usedin the window, on every load
Widget colorcolorThe color of the button and window header (#6366f1 by default)button and window
Avatar URLavatar_urlThe avatar in the window headerwindow
Positionpositionbottom-right or bottom-leftonly through the embed code
Themethemelight or darkonly through the embed code
Bubble textbubble_textThe label above the buttononly through the embed code
Display delaydelaySeconds before the button appearsonly through the embed code
Consent-to-terms textconsent_textUp to 600 characters. If set, the visitor must tick the consentwindow
Terms linkconsent_urlAn http(s) address, up to 500 characterswindow

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:

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.

SymptomCauseWhat 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 onesAdd the domain to the tenant or bind the site; wait up to a minute
There is no button at alldata-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 settingsSee diagnostics

The init request 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.

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"
FieldRule
typeLatin letters, _, -, up to 20 characters. A value with digits or longer than 20 characters is not passed
idA string or number; letters, digits, _, -, up to 40 characters
Titledocument.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.

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 modeWhat the chat does before the visitor decidesAfter consent(true)
waitThe button and chat work, but SapportWidget.consent=false: the frame receives only the page path, and the first touch is not recordedThe extended context and the first touch turn on
immediateConsent is considered given right awaynone

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:

#Block 7. Your own "Ask a question" button

Status: Planned (data-launcher="none" and Sapport.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.

  1. Use the standard floating button (blocks 1 to 3) and label it with data-bubble-text="Ask a question".
  2. 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).
  3. Click the standard button programmatically (document.querySelector('.sapport-bubble').click()). This is not recommended: the internal sapport-* class names are not a contract and may change without notice.

#Errors and diagnostics

SymptomLikely causeWhat to check
The console shows [Sapport] Missing channel ID…data-channel and SapportWidget.channelId are not setThe tag attribute; window.SapportWidget before the tag
The button does not appearThe script did not load (your site's CSP forbids the cell's script-src), or the data-delayThe Network tab; the site's CSP
The button is there, but the window is emptyThe site's domain is not allowed for the frame; your site's CSP forbids the cell's frame-srcDomain
The button is visible with the default color and greetingThe init request returned an error: the channel is off, the tenant is disabled, or the network is unavailableThe channel status in the dashboard; the Network tab
A message is not sent, the response is 503 widget_not_configuredSession signing is not configured on the serverContact the cell's support
A message is not sent, 400 consent_requiredThe consent to the terms is not tickedThe consent bar
429 Too many requestsThe rate limit was exceededLimits
No counter appears on the buttonThe postMessage from the frame did not arrive: the page origin was not determinedYour 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.LimitationWhat to do
1Pre-chat form: the email and phone fields are sent, but the server does not read them; only the name reaches the contact cardIf you need contact details, use the AI seller's text question or identify in v2 (Planned)
3When connected through the loader, the chat ignores position, theme, bubble_text, and delay from the channel settingsFor a non-standard position, use the direct tag
4The fields pre_chat_form, locale, and ai_daily_limit have no interface in the dashboard; the frame language cannot be set in the embed codeEdit the channel record through support; data-locale arrives in v2
5init does not check the domain: the button may be visible where the window is blockedCheck the window after installation
6The 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 charactersSet 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
7The close button inside the frame sends sapport:close with the target *; the frame's other messages go to the parent's exact originDo not trust the message content: it carries no data
12The visitor session is stored in the frame's sessionStorage: a new tab starts a new conversationPlanned: visitor identification through identify
22The embed code in the dashboard is assembled by string substitution: the bubble text (bubble_text) is not escapedDo not use the " character in the bubble text; check the embed code before publishing

The numbering matches the design's list of discrepancies.

#Notes

#Open questions