Vocantly Widget
A drop-in embeddable support chat widget built on the headless @vocantly/sdk. One
<script> renders a floating chat bubble + panel; your visitors chat with the AI bot (and human
agents) with zero UI work on your side.
dist/vocantly-widget.js is a single self-contained file (~63 KB) — the SDK, its deps, and the
UI are all bundled in. No import maps, no build step for the host.
The security model (important)
The widget runs in the browser, so it only ever holds an SDK token (scoped to one app + one visitor, short-lived). Your API key + secret never touch the browser. The flow:
Your backend (holds API key) Browser (this widget)
POST /apps/:id/support-sessions ── token + conversation_id ──▶ VocantlyWidget.init({...})
(X-Api-Key + X-Api-Secret)
Your server calls the provisioning endpoint, then hands only { token, conversation_id } to the page.
Install
Option A — script tag (any website, via CDN):
<!-- unpkg / jsDelivr serve the bundled IIFE; global: VocantlyWidget -->
<script src="https://unpkg.com/@vocantly/widget"></script>
<!-- pin a version: https://unpkg.com/@vocantly/[email protected] -->
Option B — npm (React / Vue / Next / bundlers):
npm install @vocantly/widget
import { init } from '@vocantly/widget';
init({ wsUrl, token, conversationId, /* … */ });
Both ship the SDK + deps bundled in — zero peer dependencies. Usage is the same either way:
<script src="https://unpkg.com/@vocantly/widget"></script>
<script>
VocantlyWidget.init({
wsUrl: 'wss://your-realtime/ws',
token: 'SDK_TOKEN_FROM_YOUR_BACKEND',
conversationId: 'CONVERSATION_ID_FROM_YOUR_BACKEND',
apiUrl: 'https://your-backend/api/v1', // optional — enables message history
title: 'Support',
subtitle: 'We reply in a few minutes',
primaryColor: '#195EFF',
position: 'bottom-right', // or 'bottom-left'
greeting: 'Hi! 👋 How can we help?',
autoOpen: false,
// Optional pre-chat contact capture (needs apiUrl):
collectContact: true, // ask name + email before chatting
requireEmail: false, // if true, email is mandatory to start
contactPrompt: 'Before we start — how can we reach you?',
});
</script>
Your backend (one call to provision)
// server-side — keep the secret here
const r = await fetch(`${BASE}/apps/${APP_ID}/support-sessions`, {
method: 'POST',
headers: { 'X-Api-Key': PUBLIC_KEY, 'X-Api-Secret': SECRET_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ external_user_id: yourUserId, name, email }),
});
const { token, conversation_id } = (await r.json()).data; // hand these to the browser
No backend? Anonymous embed with just your public key
For a static landing page with unauthenticated visitors and no server, use publicKey instead of
token/conversationId. The widget self-provisions an anonymous session — no secret in the browser:
<script src="https://unpkg.com/@vocantly/widget"></script>
<script>
VocantlyWidget.init({
publicKey: 'pk_…', // your app PUBLIC key (safe to expose)
wsUrl: 'wss://ws.vocantly.com/ws',
apiUrl: 'https://api.vocantly.com/api/v1', // required in this mode
collectContact: true,
});
</script>
One prerequisite — allow your site's origin. Anonymous embedding is off by default. Add your
domain(s) to the app's allowed_origins (dashboard, or via the API):
PATCH /apps/:id { "allowed_origins": ["https://acme.com", "https://www.acme.com"] }
The widget generates a stable visitor id (localStorage) and caches {token, conversationId} so a
reload resumes the same conversation. Requests are origin-checked server-side and rate-limited.
Provide either
publicKey(anonymous) ortoken+conversationId(backend-provisioned).
API
| Call | Effect |
|---|---|
VocantlyWidget.init(opts) | Mount + connect. Returns { open, close, send, destroy }. |
VocantlyWidget.open() / .close() | Toggle the panel. |
VocantlyWidget.destroy() | Disconnect + remove from the DOM. |
Pre-chat contact capture
With collectContact: true, the widget asks the visitor for their name + (optional) email before
chatting and saves it to their customer profile via PATCH /sdk/customers/me (authenticated by the
SDK token — no secrets in the browser). Agents then see the visitor's contact details in the inbox.
Requires apiUrl to be set. Use requireEmail: true to make email mandatory.
Build
npm run build # → dist/vocantly-widget.js (minified IIFE, global VocantlyWidget)
npm run build:dev # non-minified + sourcemap
The build bundles ../vocantly-js-sdk/dist/index.js. If you rebuild the SDK, rebuild the widget.
Demo
npm run serve:demo # serves this folder on :5173
# open http://localhost:5173/ (redirects to the demo)
Avoid ports Chrome blocks as "unsafe" (e.g. 5060 = SIP →
ERR_UNSAFE_PORT). 5173 is safe. The demo is a mock business page. Enter a workspace owner's email/password + a Development App ID and click Boot — it provisions via/apps/:id/support-sessions(dev login stands in for your backend) and mounts the widget. Requires backend:3001, Go realtime:8080, and the app to have the bot enabled + an active FAQ.
Known limitation
Bot replies are currently delivered live but not persisted server-side, so they won't appear in message history on reload (customer/agent messages do). Persisting bot messages is a backend follow-up.