Skip to main content
Version: v1

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.

index.html
<!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

AttributeRequiredDefaultDescription
data-public-keyyes*Your pk_ publishable key (public, safe in HTML).
data-api-basenohttps://api.monoverse.techSet only if you self-host the API.
data-tenant-tokenyes*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.

<script
src="https://api.monoverse.tech/widget/widget-voice.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 a destroy().
  • window.VoiceBot.version — the loaded bundle version.
Read-only by default

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 HS256 tenant_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-token is present the widget takes the host-minted path; otherwise it uses data-public-key and 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

SymptomCauseFix
Widget never appears; console public-token exchange failed (403)current origin not on the pk_ allow-listadd the exact origin (with www/subdomain/port) to the key's allow-list
400 origin_requiredrequest 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 originpk_ is wrong or revokedre-issue / verify the pk_
Widget mounts but the WebSocket closes 4403token claim origin ≠ real browser originthe origin in the allow-list and in the browser must match exactly