Skip to content

Web widget: talk to your AI from a web page

@morevoice/web puts a voice call with your assistant on a web page. It has two parts:

  • <morevoice-talk-button>, a web component: one click and the visitor is talking to your assistant, with a live status, mute, hang-up, captions and the AI notice. It works in any page and any framework, in Hebrew (right to left) and English, in light and dark.
  • MoreVoiceCall, the call client the button is built on, for your own interface: start a call, follow its events, mute, send keys and hang up.

The call runs over WebRTC from the visitor’s microphone to MoreVoice, and the same call object, transcript, recording and webhooks follow as for a phone call (with direction: "browser").

Load the script once, and place the element where the button should be:

index.html
<!doctype html>
<html lang="he" dir="rtl">
<head>
<meta charset="utf-8" />
<title>Acme Insurance</title>
<script src="https://cdn.jsdelivr.net/npm/@morevoice/web/dist/morevoice-web.js" defer></script>
</head>
<body>
<h1>שאלות על הפוליסה? פשוט לשאול.</h1>
<morevoice-talk-button
server="https://voice.example.com"
assistant="3cYbE6uYvGkH8w4ZK1rTqd"
org-name="Acme Insurance"
lang="he"
captions
></morevoice-talk-button>
</body>
</html>

The script registers the element and exposes window.MoreVoice (MoreVoiceCall, MoreVoiceCallError, defineTalkButton) for pages without a bundler. With a bundler, install the package and import the element instead:

talk-button.ts
import "@morevoice/web/define"; // registers <morevoice-talk-button>
import type { MoreVoiceTalkButton } from "@morevoice/web/talk-button";
const button = document.querySelector<MoreVoiceTalkButton>("morevoice-talk-button");
if (button) button.metadata = { page: location.pathname };
Attribute What it does
server Your MoreVoice server’s address, where your team signs in. By default, the page’s own origin.
assistant The assistant to talk to: its ID in the dashboard.
lang he or en. By default, the page’s language. Hebrew lays the button out right to left.
label The button’s text, instead of “Talk to us” / “דברו איתנו”.
org-name Your business’s name, in the AI notice: “You are talking with an AI agent of Acme Insurance. The call is recorded and transcribed.”
captions Show what is said, live, under the button.
variant="floating" A floating button in the page’s corner (the end side: bottom-left in Hebrew), instead of inline.
theme light or dark. By default, the visitor’s system setting.
notice="off" Hide the AI notice, but only after the server confirmed that this assistant says it is an AI at the start of the call. The first call always shows it before the microphone opens.

The element sends events that bubble out of its shadow root, each with the details in event.detail:

Event When
morevoice-status The call’s own status changed: starting (asking for the microphone), connecting, active, ending, ended.
morevoice-answer MoreVoice answered: callId is the call’s ID, for your logs and for the API.
morevoice-state The assistant is listening, thinking, using a tool or speaking.
morevoice-transcript A line of the conversation, partial or final, with who said it.
morevoice-ended The call ended, with a reason.
morevoice-error Something went wrong: code is mic_denied, mic_unavailable, unsupported, webrtc_failed, ws_closed or server_error.

Set metadata (string key–values kept on the call, like metadata on an API call) and auth (below) as properties. start() and hangup() do what the buttons do, and call is the current MoreVoiceCall.

The button takes your brand’s colours from CSS custom properties on the element: --mv-accent, --mv-accent-2 (the gradient), --mv-on-accent, --mv-bg, --mv-fg, --mv-muted, --mv-line, --mv-bad, --mv-font and --mv-radius. For more, style its parts with ::part(): button, bar, orb, status, timer, mute, end, notice, caption, error and info.

<style>
morevoice-talk-button { --mv-accent: #0f766e; --mv-accent-2: #14b8a6; --mv-radius: 12px; }
morevoice-talk-button::part(notice) { font-size: 12px; }
</style>

The button respects reduced motion and Windows high-contrast mode, keeps keyboard focus where the visitor expects it, and announces the call’s state to screen readers.

Every call is authorised by the element’s auth property, or a function that returns it per call (a one-time credential must be fresh each time):

auth For
{ kind: "cookie" } (the default) People signed in to MoreVoice on the same site: internal tools and test pages on your MoreVoice domain.
{ kind: "clientToken", value } Your website’s visitors: a short-lived client token your server mints for one call.
{ kind: "ticket", value } MoreVoice’s own apps (the softphone, the browser extension).

POST /v1/client_tokens · scope web_calls:write · API reference

Mint, on your server, a short-lived token a browser uses to start one call to one assistant with @morevoice/web (MoreVoiceWeb.start({ token })). The token works once, until expires_at; bind it to your site with origin. It carries the call’s ID, so you can follow the call (webhooks, GET /v1/calls/{id}/events) before it starts.

Field Required What it does
agent_user_id The agent whose softphone the token opens (the embeddable softphone; /ws/call refuses agent tokens).
assistant_id The assistant the browser will talk to.
metadata Up to 50 key/value pairs (keys up to 40 characters, values up to 500) you attach to an object. Returned as sent.
origin The only website the token works from (https://shop.example): the browser’s Origin must match. Strongly recommended. http is accepted for localhost only.
ttl_s How long the token can be used to start the call, in seconds (300–900, default 300). The call itself may run longer.
Terminal window
curl -X POST https://api.morevoice.ai/v1/client_tokens \
-H "Authorization: Bearer $MOREVOICE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"assistant_id": "asst_8tRPaZp5hLMbrGqdJ9AmNa",
"metadata": {
"crm_contact_id": "0031x00000AbCdE"
},
"origin": "https://shop.example",
"ttl_s": 300
}'

POST /v1/web_calls · scope web_calls:write · API reference

From the browser with a publishable key (mv_live_pk_…), or from your server with a secret key: returns the call ID and a single-use client token for /ws/call. With a publishable key the assistant must be public, the page must be one of the key’s allowed origins (and of the assistant’s widget origins, when it lists any), and each visitor may start 10 web calls a minute. The org’s concurrent-call quota applies.

Field Required What it does
assistant_id Yes The assistant to talk to. With a publishable key it must be public (widget settings).
metadata Up to 50 key/value pairs (keys up to 40 characters, values up to 500) you attach to an object. Returned as sent.
Terminal window
curl -X POST https://api.morevoice.ai/v1/web_calls \
-H "Authorization: Bearer $MOREVOICE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"assistant_id": "asst_8tRPaZp5hLMbrGqdJ9AmNa",
"metadata": {
"page": "/pricing"
}
}'

MoreVoiceCall is the button without the button: under 8 kB gzipped, no dependencies, with typed events. Use it for a call screen of your own, or inside a framework component:

call.ts
import { MoreVoiceCall, MoreVoiceCallError } from "@morevoice/web";
const call = new MoreVoiceCall();
call.on("answer", ({ callId }) => console.log("call", callId));
call.on("state", ({ state }) => console.log("assistant is", state));
call.on("transcript", (line) => {
if (line.final) console.log(`${line.role}: ${line.text}`);
});
call.on("ended", ({ reason }) => console.log("ended:", reason));
call.on("error", ({ code, message }) => console.warn(code, message));
document.querySelector("#talk")?.addEventListener("click", async () => {
try {
await call.start({ server: "https://voice.example.com", assistantId: "3cYbE6uYvGkH8w4ZK1rTqd", metadata: { page: location.pathname } });
} catch (err) {
if (err instanceof MoreVoiceCallError && err.code === "mic_denied") alert("Allow the microphone to talk to us.");
}
});
document.querySelector("#mute")?.addEventListener("click", () => call.mute(!call.muted));
document.querySelector("#hangup")?.addEventListener("click", () => call.hangup());

sendDtmf("1") presses a key for an IVR or a flow’s keypad menu. localStream and remoteStream give you the audio for a level meter, and audio.output in start() plays the assistant through an <audio> element of yours.

  • The page must be served over HTTPS (or from localhost): browsers only open the microphone on secure pages.
  • The visitor is asked for the microphone when they click, never before. If they block it, the button tells them how to allow it and try again.
  • The AI notice is shown before the microphone opens. Keep it, unless your assistant says it is an AI at the start of every call (notice="off" checks that).
  • Leaving the page, or removing the element (a single-page app’s navigation), ends the call: an open microphone nobody is watching is worse than a dropped call.

For React apps, see React; to follow the call from your server while it runs, see real-time events.