# Embed

`embed.js` puts the chat in an iframe on your page and talks to it. It is plain ES2020 with no dependencies and is under 8 KB gzipped.

## Before you start

- An app id, and the page's origin listed under **Origins** in the dashboard. See [Tenants and apps](/guides/tenants-and-apps/).
- A token endpoint on your server that answers a client token for the logged-in user, or an empty body when nobody is logged in. See [Sessions and tokens](/guides/sessions-and-tokens/).
- If your pages send a Content Security Policy, allow `https://app.mondochat.io` in `script-src` and `frame-src`, and allow the inline snippet or move it into a file of your own. embed.js sets styles through the CSSOM, so it needs no `style-src 'unsafe-inline'`.
- If your pages send a `Permissions-Policy`, it must allow `camera` and `microphone` for `https://app.mondochat.io`, or calls have no camera.

## Steps

1. Put the snippet on every page that shows the chat. Calls made before the script loads are queued.

   ```html
   <script>
     window.MondoChat = window.MondoChat || function () { (MondoChat.q = MondoChat.q || []).push(arguments) };
     MondoChat('init', { appId: 'APP_ID', getToken: async () => (await fetch('/mondochat-token')).text() || null });
   </script>
   <script async src="https://app.mondochat.io/embed.js"></script>
   ```

2. Open a conversation from your own button. `with` is the other user's id on your site:

   ```html
   <button onclick="MondoChat('open', { with: 'model-42' })">Chat</button>
   ```

   A user must exist before others can open a conversation with them. They exist after their first session, or you create them from your server. See [Users](/guides/users/). `MondoChat('open')` without `with` opens the inbox. After the script loads, `MondoChat.open({...})` and `MondoChat('open', {...})` are the same.

3. Listen for events, for example to draw your own unread badge:

   ```js
   MondoChat('on', 'unread', ({ count }) => { badge.textContent = count || '' })
   ```

4. Call `MondoChat('logout')` when the user logs out of your site.

### init options

Call `MondoChat('init', options)` once per page. A second call is ignored.

| Option | Type | Meaning |
|---|---|---|
| `appId` | string | Required. Your app id. |
| `getToken` | `() => Promise<string or null>` | Required. Returns a client token for the logged-in user, or `null` when nobody is logged in. It is called on start and whenever the session must be renewed, and the frame does not send a second request while one is waiting for its answer. Return a fresh token each time: a token handed out before may already be spent. |
| `lang` | `'it'`, `'en'`, `'es'`, `'fr'` | Interface language. Without it: the page's `<html lang>`, then the user's `lang`, then the browser's languages, then `i18n.default_lang`. Only languages in `i18n.languages` count. |
| `mode` | `'launcher'` or `'inline'` | `launcher` (default) floats a bubble in a corner. `inline` fills `container`. |
| `container` | Element | Where the iframe goes. Default `document.body`. |
| `onPurchase` | `(purchase) => Promise<{status, external_ref?}>` | Needed only with `commerce.payment_mode: page_callback`. See below. |
| `preload` | `true` | Create the iframe when the browser is idle instead of at once. |

With `getToken` returning `null`, the chat signs in a guest if `guests.enabled` is on, and otherwise stays signed out.

### Methods

| Method | Arguments | Effect |
|---|---|---|
| `open` | `{with?, vouch?}` | Shows the chat. `with` is an external id: it opens or starts the conversation with that user. Without it, it opens the inbox. |
| `close` | | Hides the chat. |
| `logout` | | Drops the guest token and the chat session. |
| `setLanguage` | `lang` | Switches the interface language. |
| `refresh` | | Refetches inbox, conversation and catalog, for example after a purchase made on your page. |
| `on`, `off` | `event, handler` | Subscribe and unsubscribe. |

### Events

| Event | Data | When |
|---|---|---|
| `ready` | `{}` | The chat connected to the page, once. |
| `open`, `close` | `{}` | The chat opened or closed. A full-screen call counts as open. |
| `unread` | `{count}` | The unread total changed. |
| `message` | `{conversation_id, from: {external_id, name}, preview}` | A message arrived. |
| `call.incoming` | `{from: {external_id, name}}` | Someone is calling. |
| `navigate` | `{url, reason, preventDefault()}` | The chat wants to leave to `url`. See below. |
| `error` | `{code}` | `code: 'unauthorized'` when the session ended and `getToken` gave no new token. |

### The navigate event

`reason` is one of `signup`, `login`, `profile`, `services`, `checkout`, `topup`, `conversation` or `notify`. The default is a top-level navigation to `url`. Call `preventDefault()` to handle it yourself.

- `profile` and `services` with no `url` carry the peer's `external_id`, so your page resolves the page itself. They have no `url` while `links.profile_url_template` or `links.services_url_template` is null.
- `topup` with no `url` (no `commerce.topup_url_template` and no `topup_url` from the charge callout) carries `purchase_id`, `amount_minor`, `currency` and the seller's `external_id`, so your site runs its own top-up.
- `notify` has no `url`. A guest asked to be told when the peer replies, and the event carries their own `external_id` and the `conversation_id`. Open your own page to collect and confirm an address, after checking who the viewer is by your session or by the stored guest token against `/v1/.well-known/jwks.json`. mondochat stores no end-user email. `notify` comes from the setting `guests.notify_prompt`.

The [settings overview](/settings/overview/) lists every URL template and what happens while it is unset.

### Vouch tokens

A policy row with `require_vouch: true` lets a user start a conversation only with a vouch for that peer: a proof from your server that it sent this user to that peer, for example from the peer's profile page. There are two ways:

- Put `peer: '<external id>'` in the client token. It applies for the whole session.
- Sign a vouch token and pass it to `open`: `MondoChat('open', { with: 'bob', vouch })`.

A vouch token is HS256 with the server secret and header `typ: vouch+jwt`. Its claims are `iss` (the app id), `peer`, an optional `sub` (the user it is for; without it, whoever holds it, guests included), `iat`, and `exp` at most 600 seconds later.

```js
function vouchToken(peer, sub) {
  const iat = Math.floor(Date.now() / 1000)
  const claims = { iss: APP_ID, peer, ...(sub && { sub }), iat, exp: iat + 300 }
  const body = `${b64url(JSON.stringify({ alg: 'HS256', typ: 'vouch+jwt' }))}.${b64url(JSON.stringify(claims))}`
  return `${body}.${createHmac('sha256', SECRET).update(body).digest('base64url')}`
}
```

`b64url`, `APP_ID` and `SECRET` are as in the Node server of the [quickstart](/get-started/quickstart/). A vouch token cannot be used as a client token.

### Purchases paid by your page

With `commerce.payment_mode: page_callback`, a purchase in the chat calls your `onPurchase` with the purchase (`{id, item, price_minor, currency, ...}`). Take the payment on your page, then resolve with `{status: 'paid' | 'failed' | 'cancelled' | 'pending', external_ref?}`. A rejected promise counts as `failed`.

The browser's answer only updates what the buyer sees. The purchase is settled when your server confirms it with [`POST /v1/server/purchases/{id}/confirm`](/api/operations/confirm-purchase/):

```sh
curl -X POST https://app.mondochat.io/v1/server/purchases/$PURCHASE_ID/confirm \
  -H "Authorization: Bearer $MONDOCHAT_SERVER_SECRET" -H "Mondochat-App-Id: $MONDOCHAT_APP_ID" \
  -H 'Idempotency-Key: confirm-'"$PURCHASE_ID" -H 'content-type: application/json' \
  -d '{"status": "paid", "external_ref": "order-1234"}'
```

The other payment modes need nothing on the page: `server_charge` goes through the `charge` callout and `checkout_url` through a redirect. See [Commerce](/guides/commerce/).

### Get the app

With `pwa.enabled`, the embed offers a seller a dismissible Get the app card at the top of the inbox, and a Get the app row in settings. Buyers and guests never see either. See [Installed app](/guides/installed-app/).

### What embed.js does to your page

- Adds one iframe with `src="https://app.mondochat.io/frame/?app=<appId>"`, `referrerpolicy="origin"`, `allow="camera; microphone; autoplay; fullscreen; picture-in-picture"`, `allowfullscreen`, a translated `title` and no `sandbox`. With `mode: 'inline'` the address gets `&mode=inline`.
- Sizes the iframe for the hidden, launcher, open and full-screen call states. The launcher is wider for a few seconds while a message toast shows beside the bubble. On a touch screen up to 768 px wide or 520 px tall an open chat fills the screen. Elsewhere it is a 400 by 620 panel above the bubble, which stays on the page and closes it.
- While the launcher shows, only the bubble, its badge and a toast take taps. The rest of the iframe passes them to your page.
- When the chat is closed and its own sound is locked (iPhone), plays the arrival chime on your page, primed by the visitor's first tap or key press there.
- Stores one key, `localStorage['mondochat:<appId>:guest']`, which holds the guest token. It sets no cookies.
- Talks to the iframe only over a `MessageChannel` set up after checking the iframe's origin. See [Security](/guides/security/) and the [frame protocol](/reference/frame-protocol/).

## Check it works

- A launcher bubble appears in the corner once `getToken` returns a token.
- `MondoChat('on', 'ready', () => console.log('ready'))` logs once the chat connects.
- `MondoChat('open', { with: '<another user id>' })` opens that conversation.

## When it fails

| Symptom | Cause |
|---|---|
| Nothing appears and the console shows a `frame-ancestors` error | The page's origin is not listed under **Origins** for the app. |
| The `error` event fires with `unauthorized` | The session ended and `getToken` returned no new token. |
| `getToken` returns a token that is refused | A token with `jti` works once, and a token handed out before may already be spent. Sign a fresh one on every call. |
| Calls have no camera | Your `Permissions-Policy` does not allow `camera` and `microphone` for `https://app.mondochat.io`. |
| The script or the frame is blocked | Your Content Security Policy does not allow `https://app.mondochat.io` in `script-src` and `frame-src`. |
| Opening a chat fails with 404 | The other user has never signed in and was not created from your server. |

## Limits

- `init` runs once per page, and the frame sends one `getToken` request at a time.
- `onPurchase` is used only with `commerce.payment_mode: page_callback`.
- An app lists at most 20 origins, and together they take at most 1024 bytes.
- The only thing stored on your page is the guest token.

## Related topics

- [Frame protocol](/reference/frame-protocol/)
- [Settings reference](/reference/settings/)
- [Confirm a purchase](/api/operations/confirm-purchase/)
- [Errors](/errors/)
- [Sessions and tokens](/guides/sessions-and-tokens/)