#Widget JS API
Status: Planned. The
Sapportobject and its methods do not exist in the current version ofwidget.js. Available are only twopostMessagemessages from the chat frame and the loader'swindow.sapport.consentkey (the section "What exists today"). Do not use the planned methods in production markup: callingSapport.open()today ends with the errorSapport is not defined.
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 about the visitor's identity, and subscribe to events.
#Contents
- When to use it
- What exists today (Available)
- Connecting (Planned)
- Methods (Planned)
- Events (Planned)
- identify and identity signing (Planned)
- Single-page apps (Planned)
- 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" |
| Report that the visitor moved to another "page" without a reload | Sapport.setPage() |
| Link the visitor to a user of your site | Sapport.identify() with a signature |
| React to an AI message, a lead, or a handoff to an operator | Sapport.on() |
| Send a prepared question on the visitor's behalf | Sapport.send() |
Access: no authentication is needed for calls in the browser. The signature for identify is created by your server with the integration secret; the secret never reaches the browser.
#What exists today (Available)
Status: Available.
#postMessage messages from the chat frame
The chat window is an iframe on the cell domain. It reports two events to the host page with window.postMessage messages. widget.js itself receives them; your page can listen to them too.
| Message (string) | When | What widget.js does |
|---|---|---|
sapport:new-message | A new AI or operator message arrived | If the window is closed, increases the counter on the button |
sapport:close | The visitor clicked ✕ in the window header | Closes the window |
Properties:
- The message is a string, not an object, and has no payload (the message text is not passed).
- The frame sends
sapport:new-messageto the host page's exact origin. The origin is determined fromlocation.ancestorOrigins[0]or, if that is absent, fromdocument.referrer. If your site disabled the referrer (Referrer-Policy: no-referrer) and the browser does not provideancestorOrigins(Firefox), the message will not arrive. - The close button sends
sapport:closeto the target*. Do not trust it as a source of data; checkevent.sourceandevent.origin.
Listening on your page:
<script>
window.addEventListener('message', function (e) {
// the origin of the cell that widget.js is loaded from
if (e.origin !== 'https://formula-cream.pro') return;
if (typeof e.data !== 'string') return;
if (e.data === 'sapport:new-message') {
console.log('New message in the chat');
}
if (e.data === 'sapport:close') {
console.log('The visitor closed the chat window');
}
});
</script>
These messages are an internal protocol between the frame and
widget.js, not a promised contract: the set of messages may change. For a durable integration, wait forSapport.on().
#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.
#What does NOT exist today
- The
window.Sapportobject and all its methods. - The events
ready,open,close,message,lead, andhandoff. - A way to open the window from your own code. For workarounds, see block 7.
- A way to tell the widget about an SPA route change:
window.sapportPageanddocument.titleare read once, when the window is first opened.
#Connecting (Planned)
Status: Planned. The target form; names and signatures may be refined at publication.
The window.Sapport object is created by the widget.js script. Call the methods after it loads, for example from the tag's onload handler:
<script
src="https://formula-cream.pro/widget.js"
data-channel="00000000-0000-4000-8000-000000000000"
data-launcher="none"
async
onload="Sapport.on('ready', function () { Sapport.open(); })"></script>
The design does not describe a call queue before the script loads; do not rely on one. The order guarantee is the ready event.
#Methods (Planned)
Status: Planned. All the methods below are a design. Return values, except where stated, are not fixed.
| Method | Parameters | Description |
|---|---|---|
Sapport.open() | none | Open the window. Creates the frame on the first call |
Sapport.close() | none | Close the window |
Sapport.toggle() | none | Toggle |
Sapport.send(text) | text is a string, at most 4000 characters | Send a message on the visitor's behalf, as if they typed it. Requires consent if the tenant enabled it |
Sapport.setPage(page) | {type, id, title} | Update the page context: the entity type and ID, and the title |
Sapport.identify(user) | {external_id, name, email, phone, signature} | Link the visitor to a user of your site (below) |
Sapport.on(event, fn) | event is the event name, fn is the handler | Subscribe to an event |
#Sapport.setPage
| Field | Type | Required | Constraints | Example |
|---|---|---|---|---|
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 |
title | string | no | up to 150 characters; sanitized before it enters the prompt | Hydrolat Basics |
Sapport.setPage({ type: 'course', id: 'hydrolat-basics', title: 'Hydrolat Basics' });
The page address is taken from location; without consent (see Page, source, and UTM) only the path enters the prompt.
#Sapport.send
Sapport.send('How much does the course cost?');
The message goes by the same route as the visitor's input, through the client Chat API. The AI reply appears in the window; to get the reply in code, subscribe to the message event.
#Events (Planned)
Status: Planned. The design does not fix the contents of
detail(the event fields); the set below is a proposal of this document and must be verified at implementation.
| Event | When | Expected data |
|---|---|---|
ready | The widget has loaded and the channel settings were received | none |
open | The window opened | none |
close | The window closed | none |
message | A message appeared in the window: from the visitor, the AI, or an operator | sender (user, ai, operator), content |
lead | A lead was created from the conversation | the lead ID is not passed to the visitor, so probably only the fact |
handoff | The conversation was handed to an operator (an operator joined or the conversation entered the queue) | none |
Sapport.on('message', function (m) {
if (m.sender === 'ai') console.log('The AI replied:', m.content);
});
Sapport.on('lead', function () {
// For example, send a goal to your analytics
if (window.ym) window.ym(12345678, 'reachGoal', 'chat_lead');
});
Sapport.on('handoff', function () {
console.log('An operator is joining');
});
The
leadevent shows that a lead was created on the platform. To pass the lead to your server, use webhooks (lead.created, and for a handoff to an operator,conversation.handoff_requested) rather than a browser handler: a browser can be closed or spoofed.
#identify and identity signing (Planned)
Status: Planned.
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 hash algorithm (
SHA-256) and encoding (hex) were chosen by this document by analogy with the webhook signature; the design fixes only "HMAC ofexternal_idwith the integration secret". To be settled at implementation.
#Single-page apps (Planned)
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.
#React (React Router)
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
export function SapportPageSync({ 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
<SapportPageSync 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 SapportRouteSync() {
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"
/>
Today (before setPage ships), the workaround for an SPA is not to rely on automatic context and to set window.sapportPage before the first open; after the window opens, the context cannot be updated (block 4).
#Errors
The JS API runs in the browser and returns no HTTP errors. The design does not describe the conditions under which a call has no effect. The expectations are:
| Situation | Expected behavior |
|---|---|
| A call before the script loads | Sapport is not defined: the design does not describe a queue |
send without consent when the tenant requires it | The message is not sent; the window shows the consent bar |
identify with an invalid signature | The identity is not accepted; the visitor stays anonymous |
This is not a contract; the exact behavior will be described at release.
#Notes
- Calls to
open()before the widget is ready must not create a second frame: wait forready. - Each browser tab starts a separate conversation: the session is stored in the frame's
sessionStorage(Client Chat API). In the design,identifyis intended, among other things, to link such conversations to a user. - For the publication status of the JS API and v2 parameters, see the changelog.
#Open questions
- The order in which the JS API and widget v2 parameters ship is not fixed. For the owner.
- The exact
detailfields of themessage,lead, andhandoffevents, the call queue before loading, and the methods' return values are not specified in the design. Technical. - 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.