# Web widget: talk to your AI from a web page

> Put a talk button on any page with the <morevoice-talk-button> web component, or build your own call UI with the MoreVoiceCall client from @morevoice/web: attributes, events, theming, Hebrew and RTL, and how a visitor's call is authorised.

`@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"`).

## Add the button

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

```html title="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:

```ts title="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 };
```

> **Not on npm yet**
>
> `@morevoice/web` is published to npm with the API beta. Until then, build it from the SDK repository (`npm run build` in `packages/web`) and serve `dist/morevoice-web.js` from your own site.

### Attributes

| 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. |

### Events, properties and methods

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`](https://docs.morevoice.ai/guides/outbound-calls/#metadata) on an API call) and `auth` (below) as properties. `start()` and `hangup()` do what the buttons do, and `call` is the current `MoreVoiceCall`.

### Theming

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`.

```html
<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.

## Who may call

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). |

### Create a client token

`POST /v1/client_tokens` · scope `web_calls:write` · [API reference](https://docs.morevoice.ai/api/operations/client_tokens_create/)

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. |

**cURL**

```sh
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
}'
```

**Node.js**

```ts
import MoreVoice from "@morevoice/sdk";

const mv = new MoreVoice(); // MOREVOICE_API_KEY from the environment

const clientToken = await mv.clientTokens.create({
	assistant_id: "asst_8tRPaZp5hLMbrGqdJ9AmNa",
	metadata: {
		crm_contact_id: "0031x00000AbCdE",
	},
	origin: "https://shop.example",
	ttl_s: 300,
});
console.log(clientToken);
```

**Python**

```python
from morevoice import MoreVoice

client = MoreVoice()  # MOREVOICE_API_KEY from the environment

client_token = client.client_tokens.create({
    "assistant_id": "asst_8tRPaZp5hLMbrGqdJ9AmNa",
    "metadata": {
        "crm_contact_id": "0031x00000AbCdE",
    },
    "origin": "https://shop.example",
    "ttl_s": 300,
})
print(client_token)
```

### Start a web call

`POST /v1/web_calls` · scope `web_calls:write` · [API reference](https://docs.morevoice.ai/api/operations/web_calls_create/)

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. |

**cURL**

```sh
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"
  }
}'
```

**Node.js**

```ts
import MoreVoice from "@morevoice/sdk";

const mv = new MoreVoice(); // MOREVOICE_API_KEY from the environment

const webCall = await mv.webCalls.create({
	assistant_id: "asst_8tRPaZp5hLMbrGqdJ9AmNa",
	metadata: {
		page: "/pricing",
	},
});
console.log(webCall);
```

**Python**

```python
from morevoice import MoreVoice

client = MoreVoice()  # MOREVOICE_API_KEY from the environment

web_call = client.web_calls.create({
    "assistant_id": "asst_8tRPaZp5hLMbrGqdJ9AmNa",
    "metadata": {
        "page": "/pricing",
    },
})
print(web_call)
```

## Build your own call UI

`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:

```ts title="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.

## Browsers and privacy

- 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](https://docs.morevoice.ai/guides/react/); to follow the call from your server while it runs, see [real-time events](https://docs.morevoice.ai/guides/realtime/).
