SupaTalker Docs
ChannelsHost sites

Code integration

For website developers — the one-line script for anonymous embed, the entry ticket and member data for member embed, the widget's language, and automatic translation of help articles

This page is for the developer responsible for the website: where to put the embed code and code snippets copied from the admin, and how to adapt them. The website's URL must first be registered in Host sites; on a website that is not registered, the embed code does not work.

Two ways to embed; the difference is whether a backend is needed

  • Anonymous embed — just add one <script> line to the page; static pages work too
  • Member embed — after a member signs in, the website backend signs an "entry ticket" and puts it in that <script> line. Suits websites that already have member sign-in and member data

In the examples below, https://your-domain.example.com stands for the URL of the SupaTalker admin, and the Host ID and secret are placeholders; always use the values copied from the admin.

Anonymous embed

After registering the URL in the "Anonymous embed" section of the admin, click "Copy embed code" and paste this line into every page where the chat widget should appear, before </body>:

index.html
<!doctype html>
<html lang="en">
  <body>
    <!-- The website's existing content -->

    <script src="https://your-domain.example.com/embed.js"></script>
  </body>
</html>

A bubble button then appears at the bottom right of the page, and visitors click it to open the chat widget. Here is how it looks embedded on the SupaTalker website:

After embedding, a bubble button appears at the bottom right of the website

Clicking the bubble button opens the chat widget

The bubble's color, shape, position and name are set centrally in the admin's Settings; you do not add any attributes to this line. Changes in the admin take effect in about a minute, with no need to paste the code again.

Member embed

Suits websites that already have member sign-in and are built with a backend language such as Node, PHP or Python. How it works:

  1. After a member signs in to the website, the website backend signs an entry ticket with the Host ID and secret
  2. The entry ticket goes into the data-member-token attribute of the <script>, output on the page
  3. The chat widget uses the ticket to confirm the member's identity, so the visitor chats as a member and the conversation history is kept with the member

After creating the site in the "Member embed" section of the admin, that site's "Copy Node snippet", "Copy PHP snippet" and "Copy Python snippet" copy code snippets with the Host ID and secret already filled in, as follows:

Node.js
const crypto = require('node:crypto')
const appOrigin = "https://your-domain.example.com"
const hostId = "embed_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
const secret = "<secret>"
const base64url = (value) => Buffer.from(value).toString('base64url')
const now = Math.floor(Date.now() / 1000)
const payload = { v: 1, iss: hostId, sub: currentUser.id, jti: crypto.randomUUID(), name: currentUser.name, email: currentUser.email, iat: now, exp: now + 120 }
const signingInput = base64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' })) + '.' + base64url(JSON.stringify(payload))
const token = signingInput + '.' + crypto.createHmac('sha256', secret).update(signingInput).digest('base64url')
res.setHeader('Cache-Control', 'private, no-store')
res.write('<script src="' + appOrigin + '/embed.js" data-member-token="' + token + '"><\/script>')
PHP
<?php
$appOrigin = "https://your-domain.example.com";
$hostId = "embed_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
$secret = "<secret>";
$base64url = fn($value) => rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
$now = time();
$payload = ['v' => 1, 'iss' => $hostId, 'sub' => $currentUser['id'], 'jti' => bin2hex(random_bytes(16)), 'name' => $currentUser['name'], 'email' => $currentUser['email'], 'iat' => $now, 'exp' => $now + 120];
$signingInput = $base64url(json_encode(['alg' => 'HS256', 'typ' => 'JWT'])) . '.' . $base64url(json_encode($payload));
$token = $signingInput . '.' . $base64url(hash_hmac('sha256', $signingInput, $secret, true));
header('Cache-Control: private, no-store');
echo '<script src="' . htmlspecialchars($appOrigin, ENT_QUOTES) . '/embed.js" data-member-token="' . htmlspecialchars($token, ENT_QUOTES) . '"><\/script>';
Python
import base64, hashlib, hmac, json, time, uuid
app_origin = "https://your-domain.example.com"
host_id = "embed_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
secret = "<secret>"
def base64url(value): return base64.urlsafe_b64encode(value).rstrip(b'=').decode()
now = int(time.time())
payload = {'v': 1, 'iss': host_id, 'sub': current_user.id, 'jti': str(uuid.uuid4()), 'name': current_user.name, 'email': current_user.email, 'iat': now, 'exp': now + 120}
signing_input = base64url(json.dumps({'alg': 'HS256', 'typ': 'JWT'}, separators=(',', ':')).encode()) + '.' + base64url(json.dumps(payload, separators=(',', ':')).encode())
signature = base64url(hmac.new(secret.encode(), signing_input.encode(), hashlib.sha256).digest())
token = signing_input + '.' + signature
response.headers['Cache-Control'] = 'private, no-store'
response.write('<script src="' + app_origin + '/embed.js" data-member-token="' + token + '"><\/script>')

A few variables in the snippet need to be replaced with your website's own:

Name in the snippetReplace with
currentUser (current_user in Python, $currentUser in PHP)The member currently signed in; see the next section
res / responseYour web framework's response object
res.write(...) / echo / response.write(...)Change it to output this <script> before </body> on the page, for example in the page template

The secret must only be on the website backend

The secret is this site's key, so it must never appear anywhere the browser can see (front-end JavaScript, HTML source), and should not be committed to version control. Read it from an environment variable instead. If the secret leaks, click "Rotate secret" in the admin to get a new one, then update the website.

Passing member data

Member data goes in the entry ticket's content (the payload in the snippets above):

FieldRequiredDescription
v✓Always 1
iss✓The Host ID shown in the admin
sub✓The member's permanent ID on your website. Starts with a letter or digit, uses only letters, digits and . _ : -, up to 128 characters
jti✓A random value that differs for every ticket; the snippet already generates it
iat, exp✓Issued-at and expiry times (Unix seconds). The expiry can be at most 120 seconds after issuing
nameOptionalThe member's name, up to 120 characters
emailOptionalThe member's email, up to 254 characters
  • sub decides "is this the same member": the same sub is treated as the same member, and the conversation history all goes under the same visitor. Use the member number from your database, not the email (an email contains @, which does not fit the format, and members may change their email)
  • name and email are written only once: they are filled into the admin's visitor data only while those two fields are still empty; later changes on your website do not overwrite values already in the admin
  • Sign again on every page load: a ticket can only be used once and expires within 120 seconds. So this <script> must be signed fresh every time the page is generated and must not be cached; the Cache-Control: private, no-store in the snippet is there for this

For example, in Node.js (Express), turn the snippet into a function and output it on pages after the member signs in:

server.js
const crypto = require('node:crypto')

const appOrigin = 'https://your-domain.example.com'
const hostId = process.env.SUPATALKER_HOST_ID
const secret = process.env.SUPATALKER_EMBED_SECRET
const base64url = (value) => Buffer.from(value).toString('base64url')

function memberChatScript(member) {
  const now = Math.floor(Date.now() / 1000)
  const payload = {
    v: 1,
    iss: hostId,
    sub: String(member.id),
    jti: crypto.randomUUID(),
    name: member.name,
    email: member.email,
    iat: now,
    exp: now + 120,
  }
  const signingInput =
    base64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' })) +
    '.' +
    base64url(JSON.stringify(payload))
  const token =
    signingInput + '.' + crypto.createHmac('sha256', secret).update(signingInput).digest('base64url')
  return `<script src="${appOrigin}/embed.js" data-member-token="${token}"></script>`
}

app.get('/account', requireLogin, (req, res) => {
  res.setHeader('Cache-Control', 'private, no-store')
  // req.user is the member data provided by your website's own sign-in
  res.render('account', { chatScript: memberChatScript(req.user) })
})

The page template outputs chatScript before </body> (using your template syntax, without letting it be escaped as text).

Pages where the member is not signed in

URLs registered in the member section can also embed the anonymous chat directly, without registering them again. So while the member is not signed in yet, output the anonymous embed line; after they sign in, output the version with data-member-token instead.

The widget's language

The chat widget's interface and AI replies support three languages: Traditional Chinese, English and Japanese. The language is decided in this order:

  1. The data-locale attribute of the <script>
  2. The value of the page's <html lang>
  3. The "Chat widget default language" of that host site in the admin
  4. The site default language in Settings

For the first two, zh-TW, zh-Hant and zh-HK count as Traditional Chinese, values like en and en-US as English, and ja as Japanese; any other language (including Simplified Chinese) falls through to the next item.

If your website is multilingual, you usually do not need to set anything; the widget follows <html lang>, and when a single-page app switches language by changing only <html lang>, the widget switches too. To fix a language, add data-locale to the <script>:

<script src="https://your-domain.example.com/embed.js" data-locale="ja"></script>

With data-locale set, the widget no longer follows changes to <html lang>.

The screenshot below is the widget opened after the SupaTalker website was switched to Japanese: the interface changes to Japanese, the popular questions show Japanese translations, and "自動翻訳" (auto-translated) is shown at the top right (see the next section).

The website switched to Japanese, and the widget follows

Other: automatic translation of help articles

The articles in the widget's "Help" tab and in "Popular questions" on Home come from the knowledge base categories checked in Settings. You only need to write these articles in one language; the system automatically translates them into the widget's other two supported languages.

The Help tab in the Japanese interface: below the expanded answer is "自動翻訳 · 原文を表示"

  • The original language is the site default language, and it is translated automatically into the other two
  • When the widget is shown in another language, article titles and answers use the translation; popular questions on Home are labeled "Auto-translated", and below an expanded answer in the Help tab it says "Auto-translated · Show original", where visitors can click "Show original" to switch back (in the Japanese interface above it is "自動翻訳 · 原文を表示")
  • Translation runs once an hour (at half past), only for articles that are new or changed, so new or just-edited articles show the original until the next translation is done
  • Articles that fail to translate keep showing the original, and an event is recorded in Security

Only published knowledge base articles in the checked categories are translated. The AI's replies in conversations are not affected by this; the AI already answers in the widget's current language.

On this page