Find sections across all documentation pages
Choose from three embedding methods based on your website layout and user experience preferences.
Best for: Dedicated booking pages, sites with plenty of vertical space, simple implementation, WordPress, Wix, Squarespace, and other CMS platforms.
The widget loads directly on the page in an iframe. Customers can see the full booking interface immediately.
data-height attribute if needed for your layoutPlatform-Specific Instructions
WordPress: Use the "Custom HTML" block and paste the code
Wix: Add an "Embed a Site" element and paste the iframe URL
Squarespace: Use a "Code Block" and paste the embed code
Shopify: Edit your theme and paste the code in the desired template
Best for: Sites where you want to preserve existing layout, adding booking to multiple pages via a button, mobile-optimized booking experience, similar UX to Calendly or OpenTable.
A button appears on your page. When clicked, the widget opens in a slide-out panel from the right side of the screen. The widget loads only when needed (faster page load).
</body> tag)You can add multiple booking buttons on the same page. Each button works independently with its own slide-out panel. Add multiple div elements with different settings — only include the script tag once.
Example: Multiple Buttons
<!-- Button 1: Haircut -->
<div data-bookerkit-widget="your-account"
data-mode="button"
data-button-text="Book Haircut">
</div>
<!-- Button 2: Massage -->
<div data-bookerkit-widget="your-account"
data-mode="button"
data-button-text="Book Massage"
data-background-color="#10b981">
</div>
<!-- Only include the script once -->
<script src="https://www.bookerkit.com/embed.js" defer></script>Best for: Developers who want full control over the trigger button styling, using existing design systems, or integrating with frameworks like React, Vue, or Tailwind CSS.
Instead of using the default styled button, you can place your own HTML element inside the container. The embed script will detect your custom element and use it as the trigger, giving you complete styling control.
<!-- Custom button trigger -->
<div data-bookerkit-widget="your-account" data-mode="button">
<button class="my-custom-button">
Book Appointment
</button>
</div>
<script src="https://www.bookerkit.com/embed.js" defer></script>Any clickable element can be used as a trigger:
<button> — Standard button element<a> — Link styled as a button<div> — Custom styled container<span> — Inline trigger element<!-- Tailwind styled button -->
<div data-bookerkit-widget="your-account" data-mode="button">
<button class="bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 px-6 rounded-lg shadow-md transition-colors">
Schedule Now
</button>
</div><!-- Link trigger -->
<div data-bookerkit-widget="your-account" data-mode="button">
<a href="#" class="text-blue-600 underline hover:text-blue-800">
Click here to book →
</a>
</div>Customize the embed behavior using data attributes on the container element. These override your default widget settings per-embed.
| Attribute | Description | Example |
|---|---|---|
data-bookerkit-widget | Your account ID or slug (required) | "your-account" |
data-mode | Embed mode: "iframe" or "button" | "button" |
data-button-text | Button label text | "Book Now" |
data-background-color | Button background color | "#3b82f6" |
data-text-color | Button text color | "#ffffff" |
data-border-radius | Button corner rounding | "8px" |
data-height | iframe height (iframe mode only) | "800px" |
data-sheet-title | Override the widget header title | "Book a Session" |
data-sheet-description | Override the widget header description | "Limited time offer" |
data-service-id | Pre-select a service (skips service selection step) | "f57bc93b-7e5c-472d-aa03-b8de3e044cfb" |
data-service-category | Custom category for tracking and webhooks | "Hair Services" |
data-service-details | Override the service description shown in the date/time step | "60min Swedish Massage - $120" |
data-show-promos | Show or hide the promo banner (overrides widget settings) | "true" or "false" |
data-show-location-name | Show the location name in the widget header (single-location embeds) | "true" or "false" |
data-consults-only | Only show consultation services. Adds to the "show only consultations" widget setting — it can enable the filter, never disable it. Filters the service selection list, so pairing it with data-service-id still books that service. | "true" |
data-form-only | Lead-capture mode: show only the personal info form (identity + verification), then a success screen. No service, provider, date/time, or booking step. The booking_started webhook fires as usual. | "true" |
data-promo-title | Promo identifier included in webhook payload. Pre-selects a promo: a title matching your Widget Settings promo list also becomes the Promo line on the Zenoti appointment note and guest profile note | "dysport-guest" |
data-promo-description | Promo offer detail included in webhook payload | "$4/unit" |
data-promo-code | Campaign attribution code saved to the guest record, included in webhook payloads, and written to the Zenoti appointment note and guest profile note (unrelated to the promo banner; never guest-identifying data) | "SUMMER25" |
Pre-Selecting a Service
Use data-service-id to skip the service selection step entirely. Pair with data-promo-title and data-promo-description on promo landing pages to include promo context in webhook payloads:
data-promo-code is a separate, unrelated attribute — it carries a campaign attribution code, while data-promo-title and data-promo-description control the promo banner. Where each one ends up is described below.
data-promo-code is also written into the Zenoti appointment as a Code (from link): line on the booking note, and onto the guest's Zenoti profile — the label marks the wording as arriving with the visit rather than coming from BookerKit. data-promo-title pre-selects a promotion for the guest: if the title exactly matches one in your Widget Settings promo list it becomes the Promo: line on both notes (BookerKit sends its own copy of your title, never the attribute text), and if it matches nothing on that list it changes the banner only. See what BookerKit writes to your Zenoti appointments for the full note.
Do not put guest-identifying information — names, phone numbers, email addresses, or dates of birth — in data-promo-code. It accepts letters, numbers, and . _ - up to 100 characters. On top of that format check, BookerKit drops any value holding an @ or a run of seven or more digits — counting through spaces, dots, hyphens, parentheses and underscores, so 5558675309, 555-867-5309, (555) 867-5309 and 555_867_5309 are all rejected. Slashes are not counted, so a date written 09/21/2026 is not caught by this rule. A rejected value never reaches the guest record, your webhook payloads or Zenoti. That is a safety net for the two shapes a CRM merge tag usually renders, not a guarantee: the value is not read for a name, an address, or anything else personal. Whatever does get through reaches three places — the guest record, your webhook payloads, and the note on the Zenoti appointment and guest profile — so a value that turns out to hold personal data has to be cleaned up in all of them. Take particular care if your landing page fills the attribute from a CRM merge tag — the value also travels in the widget's iframe URL, so even a value that gets rejected before it reaches the guest record or a webhook can still appear in ordinary server access logs.
promoCode and offerCode are different fields and can both appear in the same booking_completed payload. offerCode is typed by the guest during booking when the offer-code field is enabled in widget settings; promoCode is set by you on the embed and is also saved to the guest record. Map them separately in your CRM.
<!-- Promo landing page with pre-selected service -->
<div data-bookerkit-widget="your-account"
data-mode="button"
data-button-text="Claim Offer"
data-service-id="your-service-uuid"
data-promo-title="dysport-guest"
data-promo-description="$4/unit">
</div>Form-Only Lead Capture
Add data-form-only="true" to reduce the widget to a lead-capture form: the guest enters their details, verifies by text or email, and lands on a short success screen. There is no service, provider, date/time, or booking step, and no appointment is created. The submission still matches or creates a guest record in your connected system, exactly as the booking flow does — no appointment is booked, but the person is recorded.
data-sheet-title and data-sheet-description still customize the header, including on the success screen. Leave them off and the header uses lead-capture wording instead of the booking wording.
A form-only embed always shows your business name in the header, whether or not data-show-location-name is set, so the guest can see who is collecting their details before they type them. If you bypass the embed script and hand-write the iframe URL with headerless=true, the header and that name go with it — in that case make sure the surrounding page identifies your business, because otherwise the name first appears on the success screen, after the details have been submitted.
data-service-id, data-service-category, and data-promo-code have no routing effect in form-only mode — there is no step for them to skip — but they keep their tracking role and still appear in the webhook payload.
The booking_started webhook fires when the guest submits their details, before verification completes. That timing is deliberate: if the guest abandons the verification code, you still receive the lead.
That payload carries "mode": "form_only", where the booking flow sends "mode": "booking". Both events always include the key, so a CRM automation can branch on it — without it, a lead and a real about-to-book guest are indistinguishable and appointment reminders fire for people with no appointment.
Re-capture your webhook trigger before you deploy this
mode is new. A GoHighLevel inbound-webhook trigger locks its field list to the first payload it received, so on any workflow that was already live mode arrives on the wire but will not appear in your field picker — you cannot select it in a condition or a filter until you delete the trigger and re-capture a sample. Do that before deploying a data-form-only embed on a widget whose booking_started webhook is already connected to a workflow, or leads will flow into your booking automation looking correct and unbranchable.
If your widget requires marketing consent, form-only still asks for it but does not require it to submit — there is nothing being purchased in this mode, so agreeing to marketing texts is not made the price of getting in touch. A ticked box is recorded as consent. An untouched one records nothing at all, rather than a "no" — on a gated booking a stored answer is always a deliberate act, but here it would only be silence, and a stored "no" would hide the checkbox from that guest on every future widget for your account and keep them out of your Zenoti campaigns.
A form-only submission leaves BookerKit through three independent sinks, so turning one off does not turn off the others:
booking_started webhook to your CRM, controlled by the booking-start webhook toggle in widget settings;bookerkit_booking_started event pushed to your page's dataLayer, which carries the guest's email and reaches whatever tags and pixels the host page loads. This is not governed by either widget toggle — it is controlled by what you install on the page.With all of them off the lead is still recorded in BookerKit and visible in your dashboard.
data-cc-auth-form — and a card-capture link's token — take precedence over data-form-only. The card flows render before the form-only branch is consulted, so combining them runs the card flow.
<!-- Lead-capture form, no booking -->
<div data-bookerkit-widget="your-account"
data-form-only="true"
data-sheet-title="Request a Consultation"
data-sheet-description="Tell us how to reach you">
</div>Open and close widgets from your own JavaScript code. Useful for custom navigation, single-page apps, or triggering the widget from non-standard UI elements.
// Shortcut: open/close the first widget on the page
window.openBookerKitWidget()
window.closeBookerKitWidget()
// Access a specific widget by instance ID
window.bookerkitWidgets[1].open()
window.bookerkitWidgets[1].close()
// Access via the container element
document.querySelector('[data-bookerkit-widget]').bookerkitWidget.open()The data-bookerkit-ready attribute is added to each container element when initialization is complete. You can listen for this before calling programmatic methods.
The widget automatically captures UTM parameters from your page URL for marketing attribution. If a customer lands on your site via a Google Ad or Facebook campaign, those parameters will be tracked with the booking session.
Supported Parameters:
In addition to UTM parameters, the widget captures ad platform click IDs (gclid, fbclid, msclkid) automatically. All tracking data is included in webhook payloads and stored with the booking session. See the Analytics & Session Tracking section for the full list of captured data.