Widget SDK
The Maison chat widget is loaded via a single script tag and initialised with window.initMcbs(). This page documents every configuration option, control method, and callback available on the returned object. For platform-specific setup instructions, see the Installation Guides.
Basic usage
<script src="https://agent.maison-labs.com/agent-inject.bundle.js" data-cfasync="false" data-no-minify="1" data-no-optimize="1" data-no-defer="1" data-noptimize="1"></script>
<script>
var mcAgent = window.initMcbs({
clientId: 'YOUR_CLIENT_UUID'
});
mcAgent.showIcon();
</script>Configuration options
Pass these properties in the object argument to initMcbs():
| Property | Type | Required | Description |
|---|---|---|---|
| clientId | string | Yes | Your Client UUID from the Business Console. |
| sessionId | string | No | An existing session ID to restore a previous conversation. If omitted, the widget falls back to an mcbsid parameter in the page URL (set by verification-email links), and only when neither is present is a new session created. An ID the server does not know (expired or stale) is answered 404, and the widget then starts a new session instead. |
| locale | string | No | BCP-47 locale code (e.g. en_US, fr_FR). Defaults to the browser locale. |
| iconPosition | object | No | Initial icon placement. Properties: top, bottom, left, right — each a number of pixels. Values must be plain numbers (e.g. 20), not strings with units (e.g. '20px'). The chat window opens aligned with the icon's left or right offset on screens wider than 767px, clamped so the window never extends past the edge of the screen; on narrower screens the window is full width and ignores the offset. |
| welcome | object | No | Opens a new chat with one of the hotel's custom phrases instead of its default opening messages, e.g. { key: 'summerSpa' } on a spa page. It takes precedence over any page welcome the hotel has set up for that page. The phrase is shown in the chat's locale, else the hotel's default language, and is never machine-translated. A restored session keeps its own history and gets no welcome, whether it was named by sessionId or by an mcbsid parameter in the page URL (a verification-email link). An ID the server does not know (expired or stale) is answered 404, and the new session the widget starts instead does get the welcome, and raises its welcome event. A key that is not one of the hotel's custom phrases, or has no wording in either language, is ignored and the chat opens as usual. Maison's own consent-card and welcome-back wording (memoryConsentYes, welcomeContextGeneric and the like) is never used as a welcome, even though the hotel edits it alongside its custom phrases. |
| onSessionUpdate | function | No | Callback fired when a session is created or restored. Receives the session ID as its first argument and, as a second argument, a meta object { clientId } naming the property the chat is bound to. For a hotel group, that clientId changes when the guest is handed off to one of the properties — persist the pair if you restore sessions across page loads. |
| onLocaleUpdate | function | No | Callback fired when the chat language changes. Receives the new locale string. |
{ bottom: 20, right: 20 }, not { bottom: '20px', right: '20px' }. The widget interprets each value as pixels automatically.Control methods
The object returned by initMcbs() exposes these methods:
| Method | Description |
|---|---|
| showChat() | Open the chat interface |
| hideChat() | Close the chat interface |
| showIcon() | Display the chat icon |
| hideIcon() | Hide the chat icon |
| hideSuggestion() | Hide the suggestion prompt |
| updateLocale(locale) | Change the chat language at runtime |
| updateIconPosition(position) | Move the icon to a new position |
| composeMessage(options) | Put text in the composer without sending it |
| sendMessage(options) | Send a message to the chat as the guest |
Driving the chat from your site
sendMessage() and composeMessage() let your own code put a message into the chat — a “Book now” button, a keyboard shortcut, a hot link, or any trigger you choose. Maison does not supply the wording: you decide what each trigger says.
// Send as the guest, opening the chat as part of the same call.
document.querySelector("#book-now").addEventListener("click", () => {
mcbs
.sendMessage({
message: "I'd like to book a room.",
locale: "en-US", // what language you wrote that in
openChat: true, // default false — sending and opening are separate
trigger: "book-now-hero", // shows up in analytics so you can tell triggers apart
})
.catch((err) => console.warn(err.code, err.message));
});
// Or stage a message the guest can edit before sending.
mcbs.composeMessage({ message: "I'd like a table for two", position: "replace" });Options
message— a string, or an array of{ message, locale }giving the same message in several languages. Mutually exclusive withkey.locale— optional. The languagemessageis written in, e.g."fi"or"fi-FI". Supplying it is what enables translation. Omit it and your text is sent exactly as written — nothing is translated and no translation request is made.key— a key from your Maison translation catalogue, as an alternative to writing the wording inline.position—"replace"(default),"append"or"prepend", relative to what the guest has already typed.openChat—sendMessageonly. Defaultfalse.trigger— a label for your own analytics.
Sending in the guest's language
A guest may switch language part-way through a conversation, and the agent answers in whatever they use — including languages you never translated. So the language that matters is the one the chat is in at the moment of the click, not the one the page loaded with. Declare what language you wrote in and the widget handles the rest:
// One wording. Translated on the fly if the guest is chatting in another language.
mcbs.sendMessage({ message: "I'd like to book a room.", locale: "en-US" });
// Several wordings. The one matching the guest is used as written — no round trip.
mcbs.sendMessage({
message: [
{ message: "I'd like to book a room.", locale: "en-US" },
{ message: "Haluaisin varata huoneen.", locale: "fi" },
{ message: "部屋を予約したいです。", locale: "ja" },
],
});Supply variants for the languages you care about and they are used verbatim, instantly and for free. Anything you did not cover is machine-translated from the wording you did supply. Region is ignored where it does not change the words — en-GB into an en-US chat is not translated — but script is not: zh-CN into zh-TW is.
locale is optional, and leaving it out is a real choice rather than an oversight: with no source language there is nothing to compare against, so your text is used as-is, no translation request is made, and the call costs nothing extra. Translation is opt-in — you get it by declaring what you wrote in.
Translation is best effort. If it fails, the original wording is still sent and the result comes back with translated: false — refusing a guest's booking click because a translation service was unreachable is the worse outcome.
Using your Maison catalogue instead
If you already maintain translations in Maison, pass key and skip the wording altogether. It resolves against the guest's current language, falls back to your default language and translates from there, and failing that to any language that has the key. Only a key that exists in none of them rejects, with MISSING_KEY — and nothing is shown to the guest in that case, because a raw key in someone's transcript is worse than a button that reports failure.
// The hotel writes the wording; you only name the phrase.
mcbs.sendMessage({ key: "bookRoomPhrase", openChat: true });
// The result tells you which language the guest actually saw, and whether
// the wording came from the catalogue or had to be machine-translated.
document.querySelector("#book-now").addEventListener("click", async() => {
try {
const r = await mcbs.sendMessage({
key: "bookRoomPhrase",
openChat: true,
trigger: "book-now-hero",
});
console.log(r.locale, r.translated); // e.g. "fi_FI" false
} catch(err) {
if(err.code === "MISSING_KEY") {
// The phrase is not configured for this hotel. Nothing was shown to the
// guest, so fall back to your own wording.
await mcbs.sendMessage({ message: "I'd like to book a room.", locale: "en-US" });
}
}
});The phrases themselves are configured per hotel, under Localization › Custom Phrases in the Maison console — a key is added once and given wording in each language the hotel supports. Ask the hotel which keys they have set up, or agree the names with them up front.
A phrase may hold several messages: in the console, a blank line between them starts a new chat bubble. sendMessage() and composeMessage() put one message in the chat, so a phrase like that arrives as one message: its first ten parts, separated by a blank line. As a welcome, each message is its own bubble — whether you name the phrase with the welcome option above, or the hotel points a page at it under Localization › Page Welcome, so a visitor on /restaurant is greeted differently from one on /rooms. The welcome is first chosen for the page the visitor is on when the chat session is created, which happens as the widget loads — normally the page they land on. On a single-page site it then follows the visitor to another page until the chat starts (nothing typed, no reply yet, no welcome-back greeting shown), unless the welcome option named the phrase or the session was resumed. A change to the query or hash alone does not move it.
What the promise means
It resolves when the chat API has accepted the message — not when the agent has replied. Waiting for a reply would add several seconds to a button click. The resolved value carries the text actually used, the locale it is in, and whether it was translated. Rejections carry a code you can branch on: BUSY (the agent is still replying), EMPTY (nothing to send), MISSING_KEY, NOT_READY, or INVALID_ARGUMENT.
Rate limits
The chat and translation endpoints are rate limited. The limit is 10 requests per minute per chat session, counted per session rather than per IP address, so one busy device does not affect other guests. A second, much looser limit applies per IP address; normal traffic does not approach it.
Over the limit, the request is refused with 429 Too Many Requests.
What a 429 looks like from your code
sendMessage() resolves once the chat has accepted the message, which happens before the network round trip — so a rate-limited turn does not reject the promise. The guest sees “Unable to process your request at this time” in the transcript instead. If a rate limit is something you need to react to, throttle at the source: disable the button while a turn is in flight.
const button = document.querySelector("#book-now");
button.addEventListener("click", async() => {
button.disabled = true; // one click, one message
try {
await mcbs.sendMessage({ key: "bookRoomPhrase", openChat: true });
} catch(err) {
console.warn(err.code, err.message); // BUSY, EMPTY, MISSING_KEY, ...
} finally {
setTimeout(() => (button.disabled = false), 3000);
}
});Translation is separate and fails soft: if the translate endpoint is rate limited or unavailable, your original wording is still sent and the result comes back with translated: false. A booking click is never dropped over a translation.
Calling before the widget has loaded
You do not need to wait. A guest can click your button the moment the page paints, so calls made before the widget is ready are queued and sent in order once it is — the same way showChat() already defers.
Hot links need one extra step
If you attach this to a link that also navigates, the page can unload before the message is sent and it will be lost. Prevent the navigation and do it once the promise settles:
link.addEventListener("click", (e) => {
e.preventDefault();
mcbs
.sendMessage({ message: "Tell me about the spa" })
.finally(() => (window.location.href = link.href));
});How these messages appear in your data
A message sent this way is recorded with isAuto: true and the location (the page URL it was triggered from), so chat logs distinguish it from something the guest typed. Analytics events chat_automated_message_sent, chat_automated_message_composed and chat_automated_message_blocked carry your trigger label and, for a blocked one, the reason. A successful send or compose also carries the locale the guest was shown and whether it was translated.
When a message is machine-translated the widget additionally reports chat_message_translated, or chat_message_translation_failed with a reason if it could not be. Both carry the source and target language and the character count — never the message text.
Full example
All configuration options together:
<script src="https://agent.maison-labs.com/agent-inject.bundle.js" data-cfasync="false" data-no-minify="1" data-no-optimize="1" data-no-defer="1" data-noptimize="1"></script>
<script>
function getQueryParam(param) {
var queryString = window.location.search;
var urlParams = new URLSearchParams(queryString);
return urlParams.get(param);
}
var sessionId = getQueryParam('mcbsid');
var mcAgent = window.initMcbs({
clientId: 'YOUR_CLIENT_UUID',
sessionId: sessionId,
locale: 'en_US',
iconPosition: { top: 0, bottom: 20, left: 0, right: 20 },
onSessionUpdate: function(id, meta) { console.log('Session:', id, 'Property:', meta && meta.clientId); },
onLocaleUpdate: function(locale) { console.log('Locale:', locale); }
});
mcAgent.showIcon();
</script>