Web Customer Service

cs-widget Integration Guide

TypeScript + Shadow DOM, about 12 KB gzipped, and zero runtime dependencies. CDN, npm, and standalone link entry all ship from the same build.

Integration overview

CDN asset
https://cs.sochatlive.com/widget.js
Standalone link
https://cs.sochatlive.com/
Target API base
https://api.sochatlive.com
Authentication
data-app-key + visitor JWT
Locale / transport
zh-CN · en-US · REST + WebSocket (8s polling fallback)
CDN embed
<script async src="https://cs.sochatlive.com/widget.js"
  data-app-key="sk_xxxxxxxxxxxxxxxxx"
  data-api-base="https://api.starim.io"></script>
npm install
pnpm add @starim-io/cs-widget
import StarIMCS from '@starim-io/cs-widget'
StarIMCS.init({
  appKey: 'sk_xxxxxxxxxxxxxxxxx',
  apiBase: 'https://api.starim.io',
})
Standalone link
# 站外渠道:对方页面无需嵌 script
https://cs.sochatlive.com/?k=sk_xxxxxxxxxxxxxxxxx&u=u_42&nick=%E8%AE%BF%E5%AE%A2%E6%98%B5%E7%A7%B0

# 生产建议 admin 代签 ?t=&s= → 见下方「链接跳转入口」

Live Demo

This page already loads https://cs.sochatlive.com/widget.js (data-mock="true"). Try the floating button in the lower-right corner. The panel below covers the imperative API and event subscriptions.

Open playground
Loading the interactive panel...

The floating button in the lower-right corner is the real widget. Use the Playgroundfor full appearance debugging.

Quick start (30 seconds)

Choose one path or combine them: CDN / npm embeds the floating launcher into your site, while standalone links distribute a full-screen visitor entry through off-site channels. After your open platform request is approved, you receive an appKey. The examples on this page can run with data-mock="true" first so you can validate the UI and imperative API before switching to production parameters.

A. Single <script> tag embed (recommended)

<script async
  src="https://cs.sochatlive.com/widget.js"
  data-app-key="sk_xxxxxxxxxxxxxxxxx"
  data-api-base="https://api.starim.io"
  data-locale="zh-CN"
  data-theme="auto"
  data-position="br"></script>

B. Query string (when data-* is unavailable)

<script async src="https://cs.sochatlive.com/widget.js?appKey=sk_xxxxxxxxxxxxxxxxx&apiBase=https%3A%2F%2Fapi.sochatlive.com&locale=en-US"></script>

C. Imperative init (Intercom-style)

<script async src="https://cs.sochatlive.com/widget.js" data-defer-init></script>
<script>
  // queue-style: commands are buffered while the script loads asynchronously
  window.StarIMCS = window.StarIMCS || function () {
    (window.StarIMCS.q = window.StarIMCS.q || []).push(arguments)
  }

  StarIMCS('init', {
    appKey: 'sk_xxxxxxxxxxxxxxxxx',
    apiBase: 'https://api.sochatlive.com',  // replace with your private API origin when self-hosting
    externalUserId: 'u_42',
    nickname: 'Alex Lee',
    extra: { orderNo: 'O-2026-001' },
    onReady: () => console.log('widget ready'),
    onMessage: (m) => console.log('msg', m),
  })
</script>

Commands are queued while the script loads asynchronously and replay automatically once the widget is ready. Use this when your host page already has its own initialization flow.

E. Standalone links (off-site channels)

Full guide ↓

Ideal for SMS, email, QR codes, and other off-site channels: visitors open the link and land directly in a full-screen support window. No script embed is required on the destination page, and there is no domain allowlist requirement. You can enable it alongside CDN or npm embeds.

Sample link
# Smallest possible link: appKey only
https://cs.sochatlive.com/?k=sk_xxxxxxxxxxxxxxxxx

# Include visitor identity (when your business user is already signed in)
https://cs.sochatlive.com/?k=sk_xxxxxxxxxxxxxxxxx&u=u_42&nick=Alex%20Lee

D. npm install (Vue / React / Next / Nuxt)

Full guide ↓

ESM and CJS bundles ship with full TypeScript typings from the same source and build as the CDN asset, with the same ~12 KB gzipped footprint. For SSR frameworks, load the widget from a client-only hook.

Install
# pnpm
pnpm add @starim-io/cs-widget

# npm
npm install @starim-io/cs-widget

# yarn
yarn add @starim-io/cs-widget
Minimal usage
import StarIMCS from '@starim-io/cs-widget'

await StarIMCS.init({
  appKey: 'sk_xxxxxxxxxxxxxxxxx',
  apiBase: 'https://api.sochatlive.com',  // Replace with your private API origin when self-hosting
  externalUserId: currentUser?.id,
  nickname: currentUser?.name,
  onReady:   () => console.log('ready'),
  onMessage: (m) => console.log(m),
})

StarIMCS.open({ prefill: 'I want to ask about my order' })

Pass visitor identity

The widget uses a three-layer identity strategy: externalUserId (your stable business ID) > fingerprint (a low-entropy browser hash) > visitorId (a localStorage UUID fallback). Matching any layer treats the user as the same visitor, and the platform merges identity records by priority. Visitor identities stay isolated from your product account system and are only used for customer service context.

L1 · externalUserId (recommended)

A stable and unique identifier from your own business system. This has the highest priority, preserves conversation history across sites and devices, and merges earlier anonymous sessions after login.

L2 · fingerprint (auto-enabled since v0.1.1)

A low-entropy browser hash composed from UA, screen, timezone, canvas/WebGL, fonts, and similar signals. Even after clearing storage or switching to private mode, it still reconnects roughly 60-80% of visitors. The SDK uploads only the hash, never the raw fingerprint source data.

L3 · visitorId (fallback)

The widget auto-generates a v_<uuid> in localStorage. It is the most stable identifier within the same browser. After storage is cleared, the L2 fingerprint becomes the safety net.

Optional display fields

nickname, avatar, email, phone, and extra are all optional. extra accepts arbitrary JSON so you can pass order numbers, plans, or other business context.

Sync identity after SPA login

// Sync identity immediately after login; no shutdown + init required
StarIMCS.identify({
  externalUserId: currentUser.id,    // Stable unique ID from your own business system
  nickname: currentUser.name,
  avatar: currentUser.avatar,
  email: currentUser.email,
  extra: { orderNo: 'O-2026-001' },
})

// User sign out
StarIMCS.identify({ externalUserId: null })

Configuration parameters

Merge priority: StarIMCS.init() > data-* > query string > remote site configuration. All three channels can be layered freely.

Fielddata-* attributeType / defaultDescription
appKeyRequireddata-app-keystringThe site integration key issued when the admin console creates the site.
apiBasedata-api-basestring · defaults to the API subdomainThe backend base URL. Third-party websites usually set the API subdomain explicitly, such as https://api.starim.io, while private deployments override it with their own endpoint.
wsBasedata-ws-basestring · auto-derivedThe WebSocket base URL. It is derived from apiBase by default and only needs to be overridden when API and WS traffic use different domains or proxy chains.
externalUserIddata-external-user-idstringA stable business-side user ID (layer 1 in the three-level identity strategy).
nicknamedata-nicknamestringThe display nickname shown to agents.
avatardata-avatarstring · URLThe avatar image URL.
emaildata-emailstringThe visitor email address.
phonedata-phonestringThe visitor phone number.
extradata-extraJSON stringBusiness context such as order numbers or plans, visible to agents.
localedata-locale"zh-CN" | "en-US"Force a locale. When omitted, the widget falls back to localStorage and browser detection.
themedata-theme"light" | "dark" | "auto"Defaults to light. auto follows prefers-color-scheme.
positiondata-position"br" | "bl" | "tr" | "tl"Floating launcher position. The default is bottom-right (br).
defaultOpendata-default-openboolean · default falseOpen the panel automatically after the page finishes loading.
zIndexdata-z-indexnumber · default 2147483000The Shadow DOM container z-index. Adjust it only when the host page already uses an even higher overlay layer.
mockdata-mockboolean · default falseLocal mock mode. It skips the backend and is useful for demos or offline development.
data-defer-initattributeDisable auto-init. Must be paired with StarIMCS.init().

JS API

All methods are exposed on global window.StarIMCS . The table below maps one-to-one to the button groups in the Live Demo .

MethodParamsDescription
init(cfg)InitConfigInitialize manually (only when data-defer-init is enabled).
identify(profile){ externalUserId?, nickname?, ... }Sync login state without reconnecting or losing unread messages.
open(opts?){ prefill?: string }Open the panel and optionally prefill the input box.
close()Close the panel.
toggle()Toggle between open and closed.
sendMessage(text)stringAppend and send a visitor message directly from the panel.
setLocale(locale)"zh-CN" | "en-US"Switch the locale and persist it to localStorage.
on(event, handler)WidgetEvent, fnSubscribe to widget events.
off(event, handler)WidgetEvent, fnUnsubscribe from widget events.
shutdown()Destroy the instance and release resources, usually on SPA sign-out.
versionRead-only field that exposes the current widget version.

Common calls

StarIMCS.open()                                  // Open the panel
StarIMCS.close()                                 // Close the panel
StarIMCS.toggle()                                // Toggle the panel
StarIMCS.open({ prefill: 'I need help with an order' }) // Open with prefilled text
StarIMCS.sendMessage('Hello')                    // Send a message directly
StarIMCS.identify({ externalUserId: 'u_42' })    // Sync the signed-in identity
StarIMCS.setLocale('en-US')                      // Switch locale and persist it
StarIMCS.shutdown()                              // Destroy the instance on SPA sign-out

Event subscriptions

StarIMCS.on('ready',   () => console.log('widget is ready'))
StarIMCS.on('open',    () => console.log('panel opened'))
StarIMCS.on('close',   () => console.log('panel closed'))
StarIMCS.on('message', (m) => {
  // m: { id, conversationId?, from: 'visitor' | 'agent' | 'system',
  //      text, agentName?, createdAt }
  console.log(`new ${m.from} message: ${m.text}`)
})
StarIMCS.on('error',   (e) => console.warn(e.message))

Event list: ready · open · close · message · error · identify · shutdown.

Internationalization

The widget currently ships with zh-CN and en-US . It shares the same SoChat client locale key localStorage['starim_locale'] , so signed-in visitors automatically inherit the host page locale when they open the chat panel.

Priority (high → low)

  1. StarIMCS.init({ locale }) / setLocale()
  2. data-locale="..."
  3. src="...?locale=..."
  4. localStorage['starim_locale'] (shared with the SoChat client so signed-in visitors inherit it automatically)
  5. Remote site configuration (the default locale configured for this integration in the open platform)
  6. navigator.languages: zh-* → Chinese, everything else → English
  7. Fallback: en-US

Switch locale

// Wire it to your own site language switcher:
StarIMCS.setLocale('en-US')
// → persists to localStorage['starim_locale'] and survives refreshes

// Or update the storage key directly without the widget helper:
localStorage.setItem('starim_locale', 'en-US')

Appearance customization

Full appearance customization is available from v0.2 onward. Pass the fields through init()data-* , or the query string. For a live preview, open /cs-demo in the Playground.

Brand colors

One line lets the launcher gradient and header background follow your brand. If you pass only primary, the accent gradient stop is derived automatically.

primaryColorprimaryColorAccent

Launcher button

Use sizes from 40-96 px, choose circle or pill mode, add a "Chat with us" label, and supply either inline SVG or an image URL for the icon.

buttonSizebuttonShapebuttonLabelbuttonIcon

Screen offsets

The default distance from the screen edge is 24 px and can be overridden independently on the X and Y axes to avoid fixed footers or other floating UI.

offsetXoffsetY

Desktop panel size

Control panel width (280-720), height (360-900), and radius (0-32). Mobile always falls back to 100vw / 100vh sizing.

panelWidthpanelHeightpanelRadius

Header text and background

Override the default site name plus the "Online · replies within 30 seconds" subtitle, and swap in a solid color, gradient, or image independently from primaryColor.

headerTitleheaderSubtitleheaderBackground

Panel background image

Provide a single URL and the widget renders it with cover sizing and an overlay on the panel ::before layer so message readability stays intact.

backgroundImage

Full field reference

Fielddata-* attributeType / defaultDescription
primaryColordata-primary-colorstring · colorBrand primary color (#RGB / #RRGGBB / hsl(...) / rgb(...) / named colors all work). It drives the launcher gradient start and header background. Default: #2f54eb.
primaryColorAccentdata-primary-color-accentstring · colorGradient end color. By default it is derived from primaryColor with a +12% brightness shift (for #RGB / #RRGGBB only). Pass it explicitly for precise control.
offsetXdata-offset-xnumber · default 24Horizontal distance from the screen edge in px. Valid range: 0-200, with automatic clamping.
offsetYdata-offset-ynumber · default 24Vertical distance from the screen edge in px. Valid range: 0-200.
panelWidthdata-panel-widthnumber · default 380Desktop chat panel width in px. Valid range: 280-720. Mobile width falls back to calc(100vw - 32px).
panelHeightdata-panel-heightnumber · default 580Desktop chat panel height in px. Valid range: 360-900. Mobile height falls back to calc(100vh - 48px).
panelRadiusdata-panel-radiusnumber · default 16Outer panel radius in px. Valid range: 0-32.
buttonSizedata-button-sizenumber · default 56Launcher size in px. Valid range: 40-96. This is the diameter for circles and the height for pills.
buttonShapedata-button-shape"circle" | "pill" · default circleLauncher shape. pill mode can render a text label through buttonLabel.
buttonLabeldata-button-labelstringText label shown only when buttonShape="pill". For example: "Chat with us".
buttonIcondata-button-iconstring · SVG string or image URLCustom launcher icon. Values starting with <svg are treated as inline SVG; everything else is handled as an image URL. The default is the built-in chat bubble SVG.
headerTitledata-header-titlestringHeader title that overrides the site name or default localized copy.
headerSubtitledata-header-subtitlestringHeader subtitle that overrides the default "Online / offline · replies within 30 seconds" style copy.
headerBackgrounddata-header-backgroundstring · color / gradient / urlHeader background. Pass any valid CSS background value. The default follows the primaryColor gradient.
backgroundImagedata-background-imagestring · URLBackground image URL for the full chat panel, rendered with cover sizing and a translucent overlay.

Example: rose theme + pill launcher + custom header

This setup renders a rose-colored gradient launcher with a "Chat with us" label in pill mode, a 420×640 desktop panel with a 20 px radius, and a header that shows "Customer Support" plus a business-hours subtitle. The CDN and npm paths produce identical results.

A. CDN (data-*)

<script async
  src="https://cs.sochatlive.com/widget.js"
  data-app-key="sk_xxxxxxxxxxxxxxxxx"
  data-api-base="https://api.sochatlive.com"

  data-primary-color="#f43f5e"
  data-button-shape="pill"
  data-button-label="Chat with us"
  data-button-size="60"

  data-panel-width="420"
  data-panel-height="640"
  data-panel-radius="20"
  data-offset-x="32"
  data-offset-y="32"

  data-header-title="Customer Support"
  data-header-subtitle="Business hours 09:00 - 22:00 (GMT+8)"></script>

B. npm (init config)

import StarIMCS from '@starim-io/cs-widget'

await StarIMCS.init({
  appKey: 'sk_xxxxxxxxxxxxxxxxx',
  apiBase: 'https://api.sochatlive.com',

  // v0.2 appearance customization
  primaryColor: '#f43f5e',                  // // Rose tone instead of the default blue gradient
  buttonShape: 'pill',                      // // Pill launcher
  buttonLabel: 'Chat with us',            // // Launcher text label
  buttonSize: 60,
  panelWidth: 420,
  panelHeight: 640,
  panelRadius: 20,
  offsetX: 32,
  offsetY: 32,
  headerTitle: 'Customer Support',
  headerSubtitle: 'Business hours 09:00 - 22:00 (GMT+8)',
  // backgroundImage: 'https://your-cdn.com/bg.jpg', // // Optional
  // buttonIcon: '<svg viewBox="0 0 24 24">...</svg>', // // Optional
})
Implementation notes
  • CSS variable driven: every appearance field compiles to CSS variables such as --cs-primary and --cs-panel-w on the Shadow DOM root, layered on top of the default theme without leaking into host-page CSS.
  • Automatic gradient derivation: when only primaryColor is provided, primaryColorAccent is derived with a +12% brightness delta (for #RGB / #RRGGBB only) so the gradient remains balanced. Pass both values when you need precise control.
  • Clamped bounds: size, spacing, and radius fields all enforce upper and lower bounds. For example, buttonSize stays within 40-96 and panelWidth stays within 280-720, so out-of-range values do not break layout.
  • Mobile fallback: panelWidth and panelHeight only affect desktop mode. Mobile always falls back to calc(100vw - 32px) / calc(100vh - 48px) to avoid overflow.
  • Custom icon safety: buttonIcon is treated as inline SVG when it starts with <svg; otherwise it is wrapped as an image URL. Make sure the source stays under your control.
  • Background image overlay: backgroundImage is rendered with cover sizing and a 0.18 opacity overlay on the panel ::before layer so chat bubbles remain readable over light or dark artwork.

npm install and ESM / TypeScript usage

This path is ideal for projects that already have a build pipeline, such as Vue, React, Next, Nuxt, Remix, or Astro. The SDK ships ESM + CJS bundles with full TypeScript typings and shares the same core implementation as the CDN asset. Named exports work better for tree-shaking, so unused APIs such as shutdown stay out of your bundle.

TypeScript · BrowserPublished
Browser ES2018+

@starim-io/cs-widget npm v0.2.1

ESM + CJS bundles · bundled typings · IIFE auto-bootstrap build in the same dist output · zero runtime dependencies.

Latest version

v0.2.1

Fetched live from the npm registry

Size (gzipped)

~12 KB

37 KB unpacked

Runtime dependencies

0

Browser built-ins only

License

MIT

Commercial use allowed

Install

# pnpm
pnpm add @starim-io/cs-widget

# npm
npm install @starim-io/cs-widget

# yarn
yarn add @starim-io/cs-widget

ESM / TypeScript (default export)

import StarIMCS from '@starim-io/cs-widget'

await StarIMCS.init({
  appKey: 'sk_xxxxxxxxxxxxxxxxx',
  apiBase: 'https://api.sochatlive.com',  // Replace with your private API origin when self-hosting
  externalUserId: currentUser?.id,
  nickname: currentUser?.name,
  onReady:   () => console.log('ready'),
  onMessage: (m) => console.log(m),
})

StarIMCS.open({ prefill: 'I want to ask about my order' })

Named exports (better tree-shaking)

import { init, identify, open, on, version } from '@starim-io/cs-widget'

await init({
  appKey: 'sk_xxxxxxxxxxxxxxxxx',
  apiBase: 'https://api.starim.io',
})
on('message', (m) => console.log(m))
console.log('widget version', version)

CommonJS

const { init, identify, open, on, version } = require('@starim-io/cs-widget')

await init({
  appKey: 'sk_xxxxxxxxxxxxxxxxx',
  apiBase: 'https://api.sochatlive.com',  // Replace with your private API origin when self-hosting
  externalUserId: currentUser?.id,
  nickname: currentUser?.name,
})

console.log('StarIM CS SDK version:', version)

SSR frameworks (Next.js / Nuxt)

cs-widget accesses window / document / localStorage, so it must load on the client only; otherwise SSR will throw window is not defined. Here are the standard patterns for two common frameworks:

Nuxt 3

// inside app.vue or layouts/default.vue
// or wrap with <ClientOnly>
import { onMounted } from 'vue'
import { init } from '@starim-io/cs-widget'

onMounted(() => {
  init({ appKey: 'sk_xxxxxxxxxxxxxxxxx' })
})

Next.js (App Router)

// inside app/layout.tsx
'use client'

import { useEffect } from 'react'
import { init } from '@starim-io/cs-widget'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    init({
      appKey: 'sk_xxxxxxxxxxxxxxxxx',
      apiBase: 'https://api.sochatlive.com',  // Replace with your private API origin when self-hosting
    })
  }, [])

  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}

Exports map

ConditionTargetDescription
import./dist/index.jsESM entry used automatically by Vite, Webpack 5, Rollup, and other modern bundlers.
require./dist/index.cjsCommonJS entry for Node 18+, Webpack 4, Jest, and similar environments.
types./dist/index.d.ts / .d.ctsFull TypeScript declarations, including InitConfig, ChatMessage, WidgetEvent, and more.
unpkg / jsdelivr./dist/widget.jsIIFE auto-bootstrap bundle for direct CDN <script> usage.
./widget.js./dist/widget.jsExplicit subpath for bundlers that need to reference the IIFE asset directly.

unpkg / jsdelivr fields point to dist/widget.js and can be used as CDN-equivalent mirrors: unpkg / jsDelivr

How to choose between the three entry paths
  • CDN (<script>): best for landing pages, marketing sites, WordPress, and similar setups where one line of script gets you online fast without a build step.
  • npm (import): best for Vue, React, Next, Nuxt, and other apps with an existing bundler so you get typings and tree-shaking.
  • Standalone links: best for SMS, email, IM, QR codes, or ticket follow-ups. Visitors open a full-screen support window without embedding anything into the destination page.
  • Use either CDN or npm on the same page, not both, to avoid duplicate init calls. Standalone links can coexist with embedded widgets and are configured independently in the admin console.

Standalone link entry

Ideal for off-site channels such as SMS / email / IM / QR codes / support business cards / ticket follow-ups where you cannot inject a <script>. Visitors tap the link and land directly in a dedicated full-screen support window, and the destination domain does not need to be allowlisted. It coexists with embedded integration and can be enabled at the same time.

Entry mode

The landing page defaults to https://cs.<your-im-domain>/ (the legacy /c/?k=... path is still supported). There is no floating launcher, the ChatPanel fills the viewport, and iOS safe areas are handled automatically.

Authentication strategy

Requests carry X-CS-Entry: link, so the backend skips originAllowList and validates against the site-level linkEntry configuration instead. You can optionally enable HMAC-SHA256 signatures + TTL to prevent forged externalUserId values.

The standalone base URL is configured in admin

The sample links below use https://cs.sochatlive.com as the current brand customer-service landing origin (see brand.yaml → website.csUrl). The effective URL prefix always comes from the admin setting "System Settings → Customer Service → Standalone Link Base URL": the preview link in the admin "Site → Standalone Link" dialog, the url field returned by the server-side signing API, and the cs-widget standalone.html landing page all use that configuration automatically. For private deployments or multi-brand setups, each brand only needs to update its admin setting once.

Sample link (unsigned)

# Smallest possible link: appKey only
https://cs.sochatlive.com/?k=sk_xxxxxxxxxxxxxxxxx

# Include visitor identity (when your business user is already signed in)
https://cs.sochatlive.com/?k=sk_xxxxxxxxxxxxxxxxx&u=u_42&nick=Alex%20Lee

Use this for internal testing or tightly controlled channels. Production should enable signatures (admin → Site → Standalone link → Enforce signature validation).

Signed link (recommended for production)

https://cs.sochatlive.com/?k=sk_xxxxxxxxxxxxxxxxx&u=u_42&nick=Alex%20Lee&t=1716268800&s=8f0a...

t is the Unix timestamp in seconds and s is the HMAC-SHA256 signature in hex. Once the site TTL expires (300 seconds by default), the signature becomes invalid.

Signing algorithm

# Compute t / s with the same algorithm in your Node.js / Python / Go backend
t = floor(Date.now() / 1000)                       # Unix timestamp in seconds
payload = appKey + "\n" + t + "\n" + (externalUserId || "") + "\n" + (nickname || "")
s = HMAC_SHA256(signSecret, payload).digest("hex") # Put this value into ?s=...

# Node.js example
import crypto from 'node:crypto'
const t = Math.floor(Date.now() / 1000)
const payload = `${appKey}\n${t}\n${externalUserId || ''}\n${nickname || ''}`
const s = crypto.createHmac('sha256', signSecret).update(payload).digest('hex')

signSecret is returned by admin in plaintext only once, so store it securely. If you lose it, reset it and all previously issued signed links become invalid immediately.

Server-side signing (recommended)

Do not want to implement HMAC in your own backend? The admin console can generate signed links directly from "Site → Standalone Link → Generate signed link", or you can call:

# Admin Gateway · requires admin.cs.site.link.manage
POST /api/v1/admin/cs/sites/{siteId}/link-entry/sign
Authorization: Bearer <admin-jwt>
Content-Type: application/json

{ "externalUserId": "u_42", "nickname": "Alex Lee" }

# Response
{ "appKey": "sk_xxxx", "t": 1716268800, "s": "8f0a...",
  "ttlSec": 300, "expiresAt": "2026-05-21T07:53:20.000Z" }

The response already contains t / s / expiresAt, so you can append them back to the URL directly.

URL paramShort keyRequiredDescription
appKeykYesSite appKey issued from the admin console. Use either the long key or the short key.
externalUserIduNoStable business-side user ID (identity layer L1).
nicknamenickNoVisitor display nickname.
tNoUnix timestamp in seconds. Required together with s when the site enforces signature validation.
sNoHMAC-SHA256 signature in hex. Used together with t.
apiBaseNoBackend API base URL. Defaults to the landing page origin.
localeNozh-CN / en-US. Overrides the site default locale.
themeNolight / dark / auto.
primaryColorNoBrand primary color. Overrides site theme.primaryColor.

Operational notes

  • Platform-wide switch: feature.cs.link_entry.enabled (admin → System config). When disabled, every request carrying X-CS-Entry: link returns 503.
  • Site-level switch: CsSite.linkEntry.enabled (admin → Site details → Standalone link). This lets you disable a single site temporarily.
  • Landing-page branding: configure landingBrand inside "Standalone Link" to override the embedded site theme with a custom title, logo, primary color, and welcome copy.
  • Permission gate: admins need admin.cs.site.link.manage to configure standalone links; otherwise the related button stays hidden.

CSP / Security

The widget does not use eval / new Function or inline style injection; all DOM and CSS are isolated within the Shadow DOM. If your site enforces CSP, the minimum required configuration is below:

Recommended Content-Security-Policy

Content-Security-Policy:
  script-src 'self' https://cs.sochatlive.com;
  connect-src 'self' https://api.sochatlive.com wss://api.sochatlive.com;
  img-src 'self' data: https://api.sochatlive.com https://cs.sochatlive.com;
Site Whitelist: Each widget site maintains a whitelist of allowed origins. Handshake requests must match an Origin on this list, preventing unauthorized embedding. Please submit all domains you plan to use when applying for a site.

REST Endpoints (for deep integration only)

95% of integrations do not need to worry about these endpoints; the widget already handles handshakes, JWT renewal, message pushing, and fallback mechanisms. If you are building a deep integration (e.g., reusing visitor identity during SSR), please refer to the table below.

MethodPathAuthDescription
POST/api/v1/cs/handshakeappKey + OriginIssues a visitor JWT, valid for 30 minutes.
POST/api/v1/cs/identifyvisitor JWTUpdates visitor profile (nickname, email, extra, etc.).
POST/api/v1/cs/conversationsvisitor JWTVisitor sends a message (automatically creates conversation on first message).
GET/api/v1/cs/conversations/currentvisitor JWTRetrieves current active conversation.
GET/api/v1/cs/conversations/:id/messagesvisitor JWTMessage list (supports incremental fetch via since).
POST/api/v1/cs/conversations/:id/leavevisitor JWTVisitor leaves conversation.
WS/ws (subprotocols: sochat-cs, jwt.<token>)visitor JWTPushes conversation events / CS messages.

All REST requests automatically include X-Locale: <current_locale> to help the server return localized error messages.

Error Codes & Troubleshooting

When the widget encounters issues, check the browser console first. This table covers 99% of common problems.

SymptomPossible CauseFix
Floating button does not appearMissing data-app-key; or script blocked by ad-blockerCheck console for [StarIMCS] no appKey; try offline verification in mock mode.
Button appears but unclickableHost page CSP blocks connect-srcAdd the CS API domains (HTTPS and WSS) to the CSP whitelist.
Handshake 403 origin deniedCurrent domain is not in the site's allowed embed listContact Open Platform support to add your domain to the site's whitelist.
Handshake returns available=falseCS feature not yet enabled for this siteContact Open Platform support to enable CS feature or adjust site status.
WebSocket failedBrowser / Proxy blocks wssNo action needed. Automatically falls back to 8s short polling, transparent to users.
Button disappears after route changeSPA framework unmounted the nodewidget >= 0.1.1 has built-in MutationObserver auto-recovery; consider upgrading to v0.2+ for appearance customization.
Still old nickname after user switchDid not call identifyAlways call StarIMCS.identify({ externalUserId, nickname }) when login state changes.
Styles overridden by host pageShadow DOM isolation failed (theoretically should not happen)Submit an issue with a minimal reproducible page.

FAQ

Why can I still chat normally in mock mode?
In mock mode, messages are not sent to the backend. All responses are simulated locally. It is designed for offline verification of frontend UI, calling methods, and event subscriptions. You will receive a [mock] echo after 1.4 seconds.
Can I embed it in multiple subdomains simultaneously?
Yes. Each site has one appKey and an independent domain whitelist. Just add all subdomains to the whitelist. If different business lines need full isolation, you can create multiple sites under the "Access Domain".
What information will the CS see?
The externalUserId, nickname, avatar, email, phone, and extra fields uploaded by the Widget, plus browser model, current URL, Referer, IP, and timezone, helping CS quickly understand the context.
Will the visitor JWT expire?
Yes, default is 30 mins. The Widget auto-renews before expiration. After tab switch / refresh, it retrieves the same visitor via externalUserId / fingerprint / visitorId (even if cache is cleared, fingerprint hits 60-80%).
Can it be embedded in both SPA and multi-page apps?
Yes. Widget auto-recovers via MutationObserver: if SPA routing accidentally deletes the container, it remounts next frame. Multi-page apps do a fresh load every time.
Do I need to do anything on my backend?
The CS Widget runs entirely on the frontend. If you want to link "visitor -> your membership system", you can use externalUserId to query the platform's csVisitorId on your backend (contact support for API access), but it is not needed for most scenarios.
Can I customize the button style?
Since v0.2, full appearance customization is supported: primaryColor, button shape (circle|pill), text label, custom SVG/image icon, margins, panel width/height/radius, header title/subtitle/background, panel background image. All can be passed via init() or <script data-*>. See the "Appearance Customization" section. The /cs-demo page provides a live Playground.
Is the widget URL versioned?
Yes. /cs/widget.js is always the latest stable version (5 min CDN cache). The versioned directory /cs-static/widget-<hash>.js uses long caching for precise rollbacks.

Next Steps

First, test the frontend interaction using mock mode. Once the Open Platform creates your widget site and issues an AppKey, replace mock mode with the official appKey to start integration. The production Widget URL and the demo on this page share the same build artifact.