◆ Sapport for developers API reference Error catalog OpenAPI
Sections

#Widget JS API

Status: Planned. The Sapport object and its methods do not exist in the current version of widget.js. Available are only two postMessage messages from the chat frame and the loader's window.sapport.consent key (the section "What exists today"). Do not use the planned methods in production markup: calling Sapport.open() today ends with the error Sapport 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

  1. When to use it
  2. What exists today (Available)
  3. Connecting (Planned)
  4. Methods (Planned)
  5. Events (Planned)
  6. identify and identity signing (Planned)
  7. Single-page apps (Planned)
  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"
Report that the visitor moved to another "page" without a reloadSapport.setPage()
Link the visitor to a user of your siteSapport.identify() with a signature
React to an AI message, a lead, or a handoff to an operatorSapport.on()
Send a prepared question on the visitor's behalfSapport.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)WhenWhat widget.js does
sapport:new-messageA new AI or operator message arrivedIf the window is closed, increases the counter on the button
sapport:closeThe visitor clicked ✕ in the window headerCloses the window

Properties:

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 for Sapport.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

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

MethodParametersDescription
Sapport.open()noneOpen the window. Creates the frame on the first call
Sapport.close()noneClose the window
Sapport.toggle()noneToggle
Sapport.send(text)text is a string, at most 4000 charactersSend 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 handlerSubscribe to an event

#Sapport.setPage

FieldTypeRequiredConstraintsExample
typestringnoas for window.sapportPage.type: Latin letters, _, -, up to 20 characters, no digits (see limitation 6)course
idstringnoletters, digits, _, -, up to 40 charactershydrolat-basics
titlestringnoup to 150 characters; sanitized before it enters the promptHydrolat 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.

EventWhenExpected data
readyThe widget has loaded and the channel settings were receivednone
openThe window openednone
closeThe window closednone
messageA message appeared in the window: from the visitor, the AI, or an operatorsender (user, ai, operator), content
leadA lead was created from the conversationthe lead ID is not passed to the visitor, so probably only the fact
handoffThe 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 lead event 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.

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 hash algorithm (SHA-256) and encoding (hex) were chosen by this document by analogy with the webhook signature; the design fixes only "HMAC of external_id with 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:

SituationExpected behavior
A call before the script loadsSapport is not defined: the design does not describe a queue
send without consent when the tenant requires itThe message is not sent; the window shows the consent bar
identify with an invalid signatureThe identity is not accepted; the visitor stays anonymous

This is not a contract; the exact behavior will be described at release.

#Notes

#Open questions