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
Serve your page 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 |