Embed
JS API & events
Control BookingsXP widgets from JavaScript with render, open, on and scan, and listen for booking events as DOM CustomEvents or callbacks.
The loader script defines window.BookingsXP. Use it to render widgets from code, open a popup from your own button, and react to what visitors do in the widget. Everything here also works when the widget came from plain HTML.
window.BookingsXP#
window.BookingsXP = {
version: "1.0.0",
render(target: Element | string, options: EmbedOptions): WidgetHandle | null,
open(options: EmbedOptions): WidgetHandle, // opens a popup
on(eventName: WidgetEventName | "*", cb: (e: WidgetEvent) => void): () => void,
scan(): void, // look for new [data-bookingsxp] elements
};| Method | What it does |
|---|---|
render(target, options) | Renders a widget into an element or CSS selector. Returns a handle, or null with a console warning if the target is not found. Calling it again on the same element replaces the previous widget. |
open(options) | Opens a popup dialog immediately, without a button on the page. Returns a handle. |
on(name, cb) | Subscribes to one event from every widget on the page, or to all events with "*". Returns an unsubscribe function. |
scan() | Finds [data-bookingsxp] elements added since the last scan and removes widgets whose element left the page. The loader already does this on DOM changes; call it yourself after large client-side renders if you want it to happen immediately. |
version | The loader version, for example "1.0.0". |
Options#
EmbedOptions are the attributes in camelCase, plus callbacks:
type EmbedOptions = {
widget?: string; // "w_…"
bookingUrl?: string; // link-only mode
mode?: "inline" | "popup" | "floating";
buttonText?: string;
template?: "classic" | "compact" | "minimal" | "split" | "week" | "stepper" | "cards";
theme?: "light" | "dark" | "auto";
accent?: string;
radius?: number;
service?: string;
staff?: string;
locale?: string;
prefill?: { name?: string; email?: string; phone?: string; notes?: string };
redirectUrl?: string;
minHeight?: number;
lazy?: boolean;
hideHeader?: boolean;
onEvent?(e: WidgetEvent): void; // every event from this widget
onBooked?(e: WidgetEvent): void; // booking_completed only
baseUrl?: string; // default: the origin of the script
};Handles#
type WidgetHandle = {
element: HTMLElement;
iframe: HTMLIFrameElement | null;
prefill(p: { name?: string; email?: string; phone?: string; notes?: string }): void;
open(): void; // popup and floating only
close(): void;
destroy(): void; // removes the iframe, dialog and button
};Examples#
Render into a container and prefill from a form you already have:
const widget = BookingsXP.render("#booking", {
widget: "w_8fk2m1qz",
template: "compact",
onBooked(e) {
console.log("Booked", e.booking.id, e.service.name, e.slot.start);
},
});
document.querySelector("#lead-form").addEventListener("submit", (ev) => {
const data = new FormData(ev.target);
widget?.prefill({ name: data.get("name"), email: data.get("email") });
});Open a popup from any button:
document.querySelector("#book-demo").addEventListener("click", () => {
BookingsXP.open({ widget: "w_8fk2m1qz", service: "demo", accent: "#4f46e5" });
});Calling the API before the script loads#
The script is async, so your code may run first. Queue calls on a stub; the loader runs them in order once it starts.
<script>
window.BookingsXP = window.BookingsXP || { q: [] };
BookingsXP.q.push(["render", "#booking", { widget: "w_8fk2m1qz" }]);
BookingsXP.q.push(["on", "booking_completed", (e) => console.log("Booked", e.booking.id)]);
</script>
<script src="https://bookingsxp.com/embed/v1.js" async></script>After the loader starts, BookingsXP.q.push([...]) still works and runs the call immediately.
Listening for events#
There are three ways to hear about events. Use whichever suits your code; each event reaches all three.
1. DOM events. Each event is dispatched as a CustomEvent named bookingsxp:<name> on the widget element. It bubbles, so you can listen on the element, document or window. The event data is in detail.
window.addEventListener("bookingsxp:booking_completed", (ev) => {
const e = ev.detail;
console.log(`Booked ${e.service?.name} at ${e.slot?.start}, ref ${e.booking?.id}`);
});Window listeners receive each event once, with ev.target set to the widget element. If the element is no longer in the document (a standalone open() popup, for example), the event is dispatched on window.
2. BookingsXP.on() for events from every widget on the page:
const off = BookingsXP.on("*", (e) => console.log(e.name, e.widgetId));
// later
off();3. onEvent and onBooked in the options of render() or open(), for that widget only.
The loader also pushes each event to window.dataLayer and fires GA4 and ad pixels. See GTM, GA4 and ad conversions.
The event object#
type WidgetEvent = {
name: WidgetEventName;
widgetId: string | null; // null in link-only mode
businessName: string;
service?: { id: string; name: string; durationMinutes: number; price?: number; currency?: string };
staff?: { id: string; name: string };
slot?: { start: string; end: string; timeZone: string }; // ISO 8601 UTC + the visitor's IANA zone
booking?: { id: string; manageUrl?: string; joinUrl?: string }; // id is the BXP-XXXXXX reference
error?: { code: string; message: string };
emailSha256?: string; // booking_completed only, when enhanced conversions is on
tracking: { // tracking IDs from the dashboard
dataLayer: boolean;
ga4MeasurementId?: string;
googleAds?: { conversionId: string; conversionLabel: string };
metaPixelId?: string;
linkedin?: { partnerId: string; conversionId: string };
redirectUrl?: string;
};
};Fields are present once they are known: service after a service is chosen (or preselected), slot after a time is picked, booking only on booking_completed.
A booking_completed event looks like this:
{
"name": "booking_completed",
"widgetId": "w_8fk2m1qz",
"businessName": "Contoso Physio",
"service": { "id": "a1b2c3", "name": "Initial consultation", "durationMinutes": 45, "price": 60, "currency": "GBP" },
"staff": { "id": "d4e5f6", "name": "Sam Lee" },
"slot": { "start": "2026-10-02T14:00:00Z", "end": "2026-10-02T14:45:00Z", "timeZone": "Europe/London" },
"booking": { "id": "BXP-7K3M9Q", "manageUrl": "https://outlook.office.com/book/…" },
"tracking": { "dataLayer": true, "ga4MeasurementId": "G-ABC123XYZ" }
}Event reference#
| Event | Fires when |
|---|---|
widget_viewed | The widget has loaded and is shown. Once per widget load. |
service_selected | The visitor picks a service. |
staff_selected | The visitor picks a staff member, or "anyone". |
date_selected | The visitor picks a day. Carries date as YYYY-MM-DD. The widget opens on the first free day, so a visitor can pick a time without firing this. |
slot_selected | The visitor picks a time. |
form_started | The visitor first types in the details form. Once per widget load. |
booking_submitted | The visitor presses the book button. |
booking_completed | Microsoft Bookings has confirmed the appointment. This is the conversion. |
booking_failed | The booking could not be made, for example because the slot was just taken. Carries error.code and error.message. |
no_availability | The widget found no free times in the range it checked. |
The funnel in the dashboard uses widget_viewed, date_selected, slot_selected, form_started and booking_completed.
ESM package#
The loader is also available as an ES module, for bundlers and framework integrations:
import {
createWidget,
loadEmbedScript,
collectAttribution,
toElementAttributes,
type EmbedOptions,
type WidgetEvent,
} from "@bookingsxp/embed";
// Inject https://bookingsxp.com/embed/v1.js once; resolves with window.BookingsXP.
const api = await loadEmbedScript();
api?.on("booking_completed", (e: WidgetEvent) => console.log(e.booking?.id));| Export | Use |
|---|---|
loadEmbedScript(baseUrl?) | Adds the script tag once and resolves with window.BookingsXP. Resolves null on the server, so it is safe in SSR code. |
createWidget(target, options) | The function behind render(), without the global. |
collectAttribution() | Returns the attribution the loader would send for the current page. |
toElementAttributes(options) | Converts camelCase options to the attribute map for bookingsxp-widget. |
| Types | EmbedOptions, WidgetEvent, WidgetEventName, WidgetHandle, Attribution, Prefill, BookingsXPApi. |
If you use React, Vue, Svelte or Astro, the framework components wrap all of this.
Edit or question? hello@bookingsxp.com