Skip to content

Chat widget

Add a bubble to an existing website with widget.js. It loads the published chat in a Studio iframe; the script does not add React to your host page. For a chat rendered directly by your React application, see React chat toolkit.

Publish and allow the website

Publish the Chat page, enable anonymous and/or third-party (app) access, and add the website's exact origin under Sites that may embed this chat. For example, allow https://shop.example.com; localhost HTTP origins are also accepted for development. Other origins cannot frame the page.

<script src="https://studio.example.com/c/widget.js"
  data-slug="support" data-language="en" data-position="right" async></script>

The original data-slug snippet remains valid. The bubble keeps the anonymous visitor HMAC on your website, under superstep-chat:<slug>. Other scripts on that website can access that identity, so embed it on sites you trust. Blocked storage limits persistence to the current page. A plain iframe instead uses its own browser-partitioned storage, which can be cleared by the browser.

Queue calls before the script loads

<script>
  window.Superstep = { q: [
    ['init', { slug: 'support', open: true, language: 'en', position: 'right' }],
    ['on', 'ready', function (event) { console.log(event.slug) }]
  ] }
</script>
<script src="https://studio.example.com/c/widget.js" async></script>

An array of tuples also works: window.Superstep = [['init', { slug: 'support' }]]. The queue runs once in order; register handlers before the event you need. Use at most 100 queued commands, then the live API. Repeated script loads reuse the installed API. Data attributes initialize before queued commands. For an inline queue, apply your site's CSP nonce/hash policy as usual.

Sign in your own users

Your backend signs a short-lived JWT from its authenticated session. The app secret stays on the server. See Third-party authentication for claims, signing helpers, rotation and errors.

There are two separate origin settings:

Setting Example for this widget Purpose
Chat embed_origins https://shop.example.com Allow the parent website to frame the chat and use the checked bridge
App key allowed origins https://studio.example.com Allow HTTP/socket requests made inside the Studio iframe

A key allowing only the parent website refuses the iframe's requests. Direct React/headless clients instead allow their own application's origin. Empty key origins retain the unrestricted/native policy. Same-origin GETs that omit Origin need same-origin Fetch Metadata and an own-origin referrer; configure STUDIO_PUBLIC_URL correctly behind a proxy and preserve that request metadata.

After the live API is installed and your host login succeeds:

await window.Superstep.identify(async function () {
  const response = await fetch('/api/auth/chat-token', {
    method: 'POST', credentials: 'same-origin',
  })
  const body = await response.json()
  if (!response.ok || !body.success) throw new Error('Token request failed')
  return body.data.token
})

// On host logout, clear the widget's app identity too:
await window.Superstep.logout()

The sample endpoint uses an envelope; adapt the return expression for your own endpoint. It must authenticate and protect the mint request, derive user identity server-side and return Cache-Control: no-store. Never put a secret, PAT or workflow token in HTML, a Vite variable, browser storage or a public token endpoint.

identify accepts a JWT string or a function returning a JWT/promise. Prefer a provider so the SDK can obtain fresh credentials and retry an expired token once. Call identify again whenever the signed-in person changes. Its promise resolves after that person's history loads, and rejects with a safe code on failure. logout cancels the previous identity and restores this host's anonymous history when anonymous access is enabled. Pending providers cannot restore the previous user's messages after logout/switch. Provider/bridge waits time out after 10 seconds with HTTP_ERROR; catch identity promises in your application. JWTs and callbacks stay in memory and never enter URLs, storage or public event metadata. The private bridge validates origin, exact window, document channel and identity epoch; do not send tokens with your own wildcard postMessage bridge.

API and appearance

API Result
init(options) Create or update the widget; changing slug/baseUrl clears app identity
open(), close(), toggle() Control the panel; the bubble remains available
identify(tokenOrGetToken) Switch to an app user and await history
logout() Clear app identity and await anonymous history
on(event, handler) Subscribe; returns an unsubscribe function
off(event, handler) Remove that subscription

init requires slug. baseUrl defaults to the script's HTTP(S) origin. Options and data attributes are:

Option Attribute Values
open data-open true starts open
language data-language (data-lang alias) en, vi; otherwise browser language
position data-position left, right
color data-color A CSS color usable in the iframe and host
logo data-logo HTTP(S) image URL or PNG/JPEG/WebP base64 data URL, up to 350,000 characters

Appearance changes keep the current chat. Empty color/logo values restore defaults; invalid appearance values are ignored. Unlike the React provider, a host CSS variable does not cross the iframe: pass a concrete color. The iframe follows the browser's light/dark preference. Image/script/frame/style loading must be allowed by your host's CSP; the bubble uses scoped inline styles.

Events

Event Payload and timing
ready { slug } after history initialization and successful identity changes
open, close { slug } when visibility changes; initial init({open:true}) sets visibility without an event
message, answer { slug, sessionId, turn: { id, role, text, status? } } for new persisted user/assistant turns after a send or Human answer
error { slug, code } on transport/identity initialization failures

Restored history does not replay new-turn events. Payloads exclude auth frames, user metadata and attachment credentials. A throwing subscriber cannot break another handler. Example:

const unsubscribe = window.Superstep.on('answer', function (event) {
  console.log(event.sessionId, event.turn.id)
})
// Later:
unsubscribe()

Try the sample host

Run scripts/chat_host_e2e.sh from the repository root. The shared loopback server serves / (headless SDK), /react/ and /widget.html. The widget page reuses the demo portal's host controls and opaque login session; its bubble and iframe come from the built Studio's widget.js. Replace the demo sign-in stub before deploying your own application. Package publication is separately approved.

The e2e job covers anonymous chat, server-issued tokens, two users/history, Human answers, streaming events and logout restoring the guest identity. The headless terminal revocation case runs last on a fresh fixture. It also checks origin refusals, files and ratings. Screenshots en/vi/light/dark remain temporary CI artifacts. The /c/<slug>?embed=1 page exposes the API inside its frame for future native wrappers; platform-specific guides remain separate work.

Edit a sent message

Message actions reuse the shared editing flow: confirm a fresh execution in an independent conversation branch, keep the original, and use the source link to return. Human replies are not editable roots. For custom interfaces, useChat().edit(turnId, text, operationId) requires the same explicit rerun confirmation; retain the operation ID when recovering an interrupted attempt. See the API contract for admission and recovery.

Regeneration uses the same confirmed revision flow: Regenerate answer on the latest terminal answer opens an independent branch and asks Human/reviewer questions again. useChat().regenerate(answerId, operationId?) returns the normal chat result. See chat usage and API admission/recovery.