Skip to content

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.

  • An app id, and the page’s origin listed under Origins in the dashboard. See 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.
  • 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.
  1. Put the snippet on every page that shows the chat. Calls made before the script loads are queued.

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

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

    MondoChat('on', 'unread', ({ count }) => { badge.textContent = count || '' })
  4. Call MondoChat('logout') when the user logs out of your site.

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.

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

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 lists every URL template and what happens while it is unset.

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.

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. A vouch token cannot be used as a client token.

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:

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

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.

  • 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 and the frame protocol.
  • 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.
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.
  • 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.