Installation
The one-tag embed
Put this anywhere in <body> (or in <head> with async). Replace pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx with the key
issued for this site's origin.
<!doctype html>
<html lang="uk">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Example store</title>
</head>
<body>
<h1>Example store</h1>
<script
src="https://api.monoverse.tech/widget/widget-voice.js"
data-public-key="pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
async
></script>
</body>
</html>
The assistant button appears once the script loads. The widget renders into its own Shadow DOM host, so it never collides with your page's styles.
Attributes
| Attribute | Required | Default | Description |
|---|---|---|---|
data-public-key | yes* | — | Your pk_ publishable key (public, safe in HTML). |
data-api-base | no | https://api.monoverse.tech | Set only if you self-host the API. |
data-tenant-token | yes* | — | Alternative to data-public-key — a host-minted JWT (see below). |
* Exactly one of data-public-key or data-tenant-token must be present on the tag.
The widget loader defaults the audio worklet to {api-base}/pcm-worklet-processor.js, which the
backend serves at the root — you do not set a worklet URL.
Voice or chat
The bundle name in the src picks the variant. Swap the file; everything else is identical.
- Voice
- Chat
<script
src="https://api.monoverse.tech/widget/widget-voice.js"
data-public-key="pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
async
></script>
<script
src="https://api.monoverse.tech/widget/widget-chat.js"
data-public-key="pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
async
></script>
The window.VoiceBot global
Once the bundle loads it exposes a small global for imperative control:
interface VoiceBotGlobal {
version?: string;
init?: () => void;
destroy?: () => void;
}
window.VoiceBot.destroy()— tear down the widget instance (closes the WebSocket, releases the microphone). Call this if you remove the widget from a single-page app.window.VoiceBot.init()— re-initialise after adestroy().window.VoiceBot.version— the loaded bundle version.
The plain script tag is a read-only consultant grounded in your catalog. To let the bot act on
the page (add to cart, pick a variant, apply a filter) you wire the host bridge — set
window.VoiceBotSyncBridge before the widget boots. See
Capabilities & host actions and
Custom site integration.
SPA / route changes / re-mount
The widget is a singleton with a re-entrancy guard: re-evaluating the script, or an
inject → remove → inject cycle in a single-page app, never leaves two WebSockets or two microphones
open — the previous controller receives destroy() before a new mount. If you tear down a view that
held the widget, call window.VoiceBot?.destroy?.() explicitly to close the WebSocket and audio
immediately.
Alternative: host-minted JWT (data-tenant-token)
If you already run a backend and prefer to mint the session token yourself (as the WordPress plugin
does), use data-tenant-token instead of data-public-key:
<script
src="https://api.monoverse.tech/widget/widget-voice.js"
data-tenant-token="<origin-locked JWT issued by your backend>"
async
></script>
- Your backend makes a server-to-server HMAC request to
POST /api/v1/widget/issue-token(body{site_url, lang}) and receives an origin-locked HS256tenant_token. - Use this when you already have a backend and don't want to expose the mint in the browser, need a custom TTL/claims flow, or integrate through a ready-made plugin.
- The two attributes are mutually exclusive on one tag: if
data-tenant-tokenis present the widget takes the host-minted path; otherwise it usesdata-public-keyand self-mints from the browser.
The data-public-key path is the default for self-built sites with no backend; the host-minted JWT
path is the same byte-for-byte flow the WordPress plugin uses.
Run the example locally
The reference page is examples/embed/vanilla/index.html.
Serve it with any static server, e.g. npx serve ., then open the page (its origin must be on the
key's allow-list — register http://localhost:3000 or whichever port you serve on).
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Widget never appears; console public-token exchange failed (403) | current origin not on the pk_ allow-list | add the exact origin (with www/subdomain/port) to the key's allow-list |
400 origin_required | request had no Origin (non-browser context) | load the embed in a normal browser context |
public-token exchange failed (429) | rate limit (per-IP or per-pk_) | wait for Retry-After; do not re-inject the tag in a loop |
403 with a known origin | pk_ is wrong or revoked | re-issue / verify the pk_ |
Widget mounts but the WebSocket closes 4403 | token claim origin ≠ real browser origin | the origin in the allow-list and in the browser must match exactly |