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#

TypeScript
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
};
MethodWhat 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.
versionThe loader version, for example "1.0.0".

Options#

EmbedOptions are the attributes in camelCase, plus callbacks:

TypeScript
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#

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

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

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

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

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

JavaScript
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#

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

JSON
{
  "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#

EventFires when
widget_viewedThe widget has loaded and is shown. Once per widget load.
service_selectedThe visitor picks a service.
staff_selectedThe visitor picks a staff member, or "anyone".
date_selectedThe 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_selectedThe visitor picks a time.
form_startedThe visitor first types in the details form. Once per widget load.
booking_submittedThe visitor presses the book button.
booking_completedMicrosoft Bookings has confirmed the appointment. This is the conversion.
booking_failedThe booking could not be made, for example because the slot was just taken. Carries error.code and error.message.
no_availabilityThe 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:

TypeScript
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));
ExportUse
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.
TypesEmbedOptions, 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