Walker Formulation Academy for developers API reference Error catalog OpenAPI
Sections

#Widget JS API

Status: Available — the window.Sapport object (methods open, close, toggle, send, setPage, on, off), the events ready, open, close, message, handoff, state, error, and the call queue before the script loads. Unavailable — the identify method: the call returns the error identify_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

  1. When to use it
  2. Connection and the call queue
  3. Methods
  4. Events
  5. postMessage messages and loader consent
  6. identify and identity signing (Planned)
  7. Single-page applications
  8. Errors
  9. Notes
  10. Open questions

#When to use it

TaskMethod
Open the chat from your own "Ask a question" buttonSapport.open() together with data-launcher="none" (Embedding, block 7)
Report that the visitor moved to another "page" without a reloadSapport.setPage()
React to an AI or operator message, a handoff to an operator, or a change of conversation stateSapport.on()
Send a prepared question on the visitor's behalfSapport.send()
Link the visitor to a user of your siteSapport.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.

MethodParametersDescription
Sapport.open()noneOpen the window. The frame is created on the first open
Sapport.close()noneClose the window
Sapport.toggle()noneToggle
Sapport.send(text)text is a non-empty string, at most 4000 charactersSend 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)noneUnavailable: returns {ok: false, error: 'identify_unavailable'} and emits the error event
Sapport.on(event, fn)event name, handlerSubscribe. 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

FieldTypeRequiredConstraintsExample
urlstringnohttp(s); a relative address is resolved against the current page; up to 300 characters. Defaults to the page addresshttps://example.com/course/hydrolat-basics
titlestringnoup to 200 characters. Defaults to document.titleHydrolat Basics
typestringnoas for window.sapportPage.type: Latin letters, _, -, up to 20 characters, no digits (see limitation 6)course
idstringnoletters, digits, _, -, up to 40 charactershydrolat-basics
utmobjectnoknown 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.

EventWhenData
readyThe widget is created (the button is drawn or the widget is ready for calls from code){}
openThe window opened{}
closeThe window closed{}
messageA message from the visitor, the AI, or an operator appeared in the window{id, sender, content, at, attachments?, cards?}
stateThe conversation state changed{state, operator}
handoffThe conversation left the AI: it joined the operator queue or was accepted by an operator{state, operator}
errorA method call failed or the frame reported an error{code, method, message}

Fields:

FieldValues
message.senderuser, ai, operator (system messages do not reach the event)
message.contenttext, up to 8000 characters
message.attachmentsup to 5 files: {name, type, size, url}; url is https only, the link is signed and lives about an hour
message.cardsup 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.stateai (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 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 the error event. 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.

FieldTypeRequiredConstraintsExample
external_idstringyesA stable user identifier in your systemuser-4815
signaturestring (hex)yesHMAC-SHA256(integration secret, external_id), a hexadecimal string9f2c…
namestringnoAnna
emailstringno[email protected]
phonestringno+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 identify signing algorithm is not fixed yet: the design names only "HMAC of external_id with 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 codeMethodWhen
invalid_textsendAn empty string or not a string
message_too_longsendMore than 4000 characters
consent_requiredsendThe visitor's consent to data collection is not given: the message is not sent
identify_unavailableidentifyThe method is not available yet
SituationBehavior
A call before the script loadsSapport is not defined, if there is no queue stub (Connection)
An unknown method name in the queueIgnored
An error in your event handlerWritten to the console; the other handlers are called

#Notes

#Open questions