#Widget JS API
Status: Available — the
window.Sapportobject (methodsopen,close,toggle,send,setPage,on,off), the eventsready,open,close,message,handoff,state,error, and the call queue before the script loads. Unavailable — theidentifymethod: the call returns the erroridentify_unavailable. Planned — signing the visitor's identity (section "identify").
Purpose: control the widget from your page's code: open and close the window, send a message on the visitor's behalf, tell the widget about a page change, and subscribe to events.
#Contents
- When to use it
- Connection and the call queue
- Methods
- Events
- postMessage messages and loader consent
- identify and identity signing (Planned)
- Single-page applications
- Errors
- Notes
- Open questions
#When to use it
| Task | Method |
|---|---|
| Open the chat from your own "Ask a question" button | Sapport.open() together with data-launcher="none" (Embedding, block 7) |
| Report that the visitor moved to another "page" without a reload | Sapport.setPage() |
| React to an AI or operator message, a handoff to an operator, or a change of conversation state | Sapport.on() |
| Send a prepared question on the visitor's behalf | Sapport.send() |
| Link the visitor to a user of your site | Sapport.identify() (unavailable) |
Access: no authentication is needed for calls in the browser.
#Connection and the call queue
The window.Sapport object is created by the widget.js script at the moment it runs. Inserting the script a second time does not draw a second widget.
To call methods before the script loads, put a queue stub before the widget.js tag. The commands in the queue run right after the script loads, in order:
<script>
// Stub: calls accumulate in the q array and run after widget.js loads
window.Sapport = window.Sapport || { q: [] };
window.Sapport.q.push(['setPage', { type: 'course', id: 'hydrolat-basics' }]);
window.Sapport.q.push(['open']);
</script>
<script
src="https://formula-cream.pro/widget.js"
data-channel="00000000-0000-4000-8000-000000000000"
data-launcher="none"
async></script>
A queue entry is an array: the first element is the method name, the rest are arguments. Only public method names are accepted; anything else is ignored. After the script loads, Sapport.q.push([...]) runs immediately.
You do not need to put Sapport.on(...) subscriptions in the queue: you can subscribe to an event at any moment after the script loads, and a handler subscribed to ready after the widget is ready is still called.
If the script is already loaded, you can call Sapport.open() and the other methods directly. Calls to open, close, toggle, and send made before the window is created are remembered and run when the widget is ready.
#Methods
Status: Available, except
identify.
| Method | Parameters | Description |
|---|---|---|
Sapport.open() | none | Open the window. The frame is created on the first open |
Sapport.close() | none | Close the window |
Sapport.toggle() | none | Toggle |
Sapport.send(text) | text is a non-empty string, at most 4000 characters | Send a message on the visitor's behalf and open the window. Requires the visitor's consent to data collection |
Sapport.setPage(page) | {url, title, type, id, utm} | Update the page context |
Sapport.identify(user) | none | Unavailable: returns {ok: false, error: 'identify_unavailable'} and emits the error event |
Sapport.on(event, fn) | event name, handler | Subscribe. Returns an unsubscribe function |
Sapport.off(event, fn) | event name, handler (optional) | Unsubscribe. Without a handler, removes all subscriptions to the event |
open, close, toggle, and setPage return nothing. send returns {ok: true} or {ok: false, error, message}; the error event is emitted at the same time.
#Sapport.setPage
| Field | Type | Required | Constraints | Example |
|---|---|---|---|---|
url | string | no | http(s); a relative address is resolved against the current page; up to 300 characters. Defaults to the page address | https://example.com/course/hydrolat-basics |
title | string | no | up to 200 characters. Defaults to document.title | Hydrolat Basics |
type | string | no | as for window.sapportPage.type: Latin letters, _, -, up to 20 characters, no digits (see limitation 6) | course |
id | string | no | letters, digits, _, -, up to 40 characters | hydrolat-basics |
utm | object | no | known tags only: source, medium, campaign, content, term, yclid, ysclid, fbclid, gclid (the utm_ prefix is also accepted); up to 200 characters | {source: 'newsletter'} |
Sapport.setPage({ type: 'course', id: 'hydrolat-basics', title: 'Hydrolat Basics' });
Without the visitor's consent (see Page, source, and UTM), only the page path is sent to the platform, without address parameters, the title, or tags. Consent is read at the moment the data is sent, not at the moment of the call.
#Sapport.send
var r = Sapport.send('How much does the course cost?');
if (!r.ok) console.warn(r.error);
The message goes by the same route as the visitor's input, through the client Chat API. If the message is sent before the frame is ready, it waits and is sent right after.
#Events
Status: Available. The handler receives a single object. An error inside your handler does not interrupt the others and is written to the console.
| Event | When | Data |
|---|---|---|
ready | The widget is created (the button is drawn or the widget is ready for calls from code) | {} |
open | The window opened | {} |
close | The window closed | {} |
message | A message from the visitor, the AI, or an operator appeared in the window | {id, sender, content, at, attachments?, cards?} |
state | The conversation state changed | {state, operator} |
handoff | The conversation left the AI: it joined the operator queue or was accepted by an operator | {state, operator} |
error | A method call failed or the frame reported an error | {code, method, message} |
Fields:
| Field | Values |
|---|---|
message.sender | user, ai, operator (system messages do not reach the event) |
message.content | text, up to 8000 characters |
message.attachments | up to 5 files: {name, type, size, url}; url is https only, the link is signed and lives about an hour |
message.cards | up to 10 cards: {code, name, description, image_url, price_text, badge, page_url, buttons}; up to 3 buttons, a button address is https or a path on your site |
state.state | ai (the AI is replying), queued (the conversation is in the operator queue), operator (an operator is handling it) |
state.operator | {name, avatar_url} or null; the photo is https only or null |
Sapport.on('message', function (m) {
if (m.sender === 'ai') console.log('The AI replied:', m.content);
if (m.cards) console.log('Cards:', m.cards.map(function (c) { return c.name; }));
});
Sapport.on('state', function (s) {
if (s.state === 'operator') console.log('The conversation is handled by', s.operator && s.operator.name);
});
var off = Sapport.on('handoff', function (h) {
// for example, send a goal to your analytics
if (window.ym) window.ym(12345678, 'reachGoal', 'chat_handoff');
});
// off() removes the subscription
A browser event is a convenience for your page's interface, not a reliable source of data: a browser can be closed or spoofed. To pass a lead and a conversation to your server, use webhooks (
lead.created,conversation.handoff_requested).
The widget has no lead event.
#postMessage messages and loader consent
#postMessage messages from the chat frame
The chat window is an iframe on the cell domain. It reports events to widget.js with window.postMessage messages. For your code there is Sapport.on(); the frame's messages are an internal protocol and not a contract. widget.js accepts messages only from its own frame (it checks source and origin), so a foreign page script cannot inflate the counter or close the chat.
The legacy string messages sapport:new-message (increase the counter on the button while the window is closed) and sapport:close (the visitor clicked ✕) still work, but listening to them in your own code is not recommended: use the message and close events.
#window.sapport.consent (loader)
If the widget is connected through the loader, the visitor's consent state is passed by a function:
window.sapport.consent(true); // consent given
window.sapport.consent(false); // consent not given / withdrawn
or by an event:
window.dispatchEvent(new CustomEvent('sapport:consent', { detail: true }));
// the form { granted: true } is also accepted
For details and ready-made banner markup, see Embedding the widget, block 5.
#identify and identity signing (Planned)
Status: Unavailable. The
Sapport.identify()method exists in the widget, but today it always returns{ok: false, error: 'identify_unavailable'}and emits theerrorevent. Identity signing will arrive together with the integration secret; the description below is the target one, and names and the algorithm may be refined.
identify tells the platform which user of your site is in the chat now. Without a signature, any visitor could impersonate another by substituting someone else's external_id in the browser console. So external_id is accepted only with a signature created by your server.
| Field | Type | Required | Constraints | Example |
|---|---|---|---|---|
external_id | string | yes | A stable user identifier in your system | user-4815 |
signature | string (hex) | yes | HMAC-SHA256(integration secret, external_id), a hexadecimal string | 9f2c… |
name | string | no | Anna | |
email | string | no | [email protected] | |
phone | string | no | +79990001122 |
The integration secret (in the design, the "integration secret") is stored only on your server. Never put it in page code or give it to the browser.
#Flow
Browser Your server Page
│ GET /page │
│ ─────────────────────────► │ signature = HMAC(secret, external_id)
│ ◄───────────────────────── │ external_id + signature go into the HTML
│ │
│ Sapport.identify({ external_id, signature, … }) ─────────► widget
#Signing in Node.js
const crypto = require('node:crypto');
// SECRET is the integration secret, from your server's environment variable
function signExternalId(externalId) {
return crypto
.createHmac('sha256', process.env.SAPPORT_IDENTIFY_SECRET)
.update(externalId)
.digest('hex');
}
// in the page handler:
// const signature = signExternalId(String(user.id));
#Signing in PHP
<?php
// $secret is the integration secret from the server configuration
function sapport_sign_external_id(string $externalId): string {
$secret = getenv('SAPPORT_IDENTIFY_SECRET');
return hash_hmac('sha256', $externalId, $secret);
}
$signature = sapport_sign_external_id((string)$user['id']);
#Signing in Python
import hashlib
import hmac
import os
def sign_external_id(external_id: str) -> str:
secret = os.environ["SAPPORT_IDENTIFY_SECRET"].encode()
return hmac.new(secret, external_id.encode(), hashlib.sha256).hexdigest()
signature = sign_external_id(str(user.id))
#Calling it in the browser
<script>
// your server substitutes the values when it builds the page
Sapport.identify({
external_id: 'user-4815',
signature: '0000000000000000000000000000000000000000000000000000000000000000',
name: 'Anna',
email: '[email protected]'
});
</script>
The signature value in the example is a placeholder, not a working signature.
The
identifysigning algorithm is not fixed yet: the design names only "HMAC ofexternal_idwith the integration secret". The hash and encoding above are a working assumption, to be settled at implementation. This does not apply to the webhook signature, which works differently.
#Single-page applications
The widget reads the page address and title when it is first opened. In an SPA the address changes without a reload, and the AI would receive stale context. Call Sapport.setPage() on a route change: the context is updated even after the window is open.
#React (React Router)
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
export function PageSync({ type, id, title }) {
const location = useLocation();
useEffect(() => {
if (!window.Sapport) return;
window.Sapport.setPage({ type, id, title: title || document.title });
}, [location.pathname, type, id, title]);
return null;
}
// Course page
<PageSync type="course" id={course.slug} title={course.title} />
#Vue (Vue Router)
// router.js
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({ history: createWebHistory(), routes });
router.afterEach((to) => {
if (!window.Sapport) return;
window.Sapport.setPage({
type: to.meta.pageType,
id: to.params.slug,
title: document.title
});
});
export default router;
#Next.js (App Router)
'use client';
import { usePathname } from 'next/navigation';
import { useEffect } from 'react';
export function RouteSync() {
const pathname = usePathname();
useEffect(() => {
(window as any).Sapport?.setPage({ title: document.title });
}, [pathname]);
return null;
}
In Next.js, attach the widget tag through next/script with strategy="afterInteractive":
import Script from 'next/script';
<Script
src="https://formula-cream.pro/widget.js"
data-channel="00000000-0000-4000-8000-000000000000"
strategy="afterInteractive"
/>
A setPage call made before the frame is created is remembered and applied when the frame opens, so you can also call it before the script loads, through the queue (Connection).
#Errors
The JS API runs in the browser and returns no HTTP errors. A failed send or identify call returns {ok: false, error, message} and emits the error event.
error code | Method | When |
|---|---|---|
invalid_text | send | An empty string or not a string |
message_too_long | send | More than 4000 characters |
consent_required | send | The visitor's consent to data collection is not given: the message is not sent |
identify_unavailable | identify | The method is not available yet |
| Situation | Behavior |
|---|---|
| A call before the script loads | Sapport is not defined, if there is no queue stub (Connection) |
| An unknown method name in the queue | Ignored |
| An error in your event handler | Written to the console; the other handlers are called |
#Notes
- Repeated
open()calls do not create a second frame. - Each browser tab starts a separate conversation: the session is stored in the frame's
sessionStorage(Client Chat API).identifyis intended, among other things, to link such conversations to a user. - For changes to the JS API, see the changelog.
#Open questions
- When
identifywill ship: it depends on the integration secret. For the owner. - The
identifysigning algorithm: the design names onlyHMAC(integration secret, external_id); the hash function and signature encoding are not fixed. It also does not say where the tenant obtains the "integration secret" or how to rotate it. Technical. - Whether
identifyshould merge a visitor's conversations from different tabs and devices, and what happens to the anonymous history already accumulated, is not described. For the owner.