Booking site analytics
The hosted booking site emits funnel events for analytics. This is the reference for developers and agencies wiring Google Tag Manager containers. For GA4 reporting guidance and a recommended GTM container setup see the help articles for Google Tag Manager and Google Analytics.
Event names are a public contract and will not change.
When events fire
Events fire only when the account has Google Tag Manager or Google Analytics configured in Makeplans under ‘Setup -> Booking site’. No events fire on payment pages.
Dispatch rules:
- Availability events (
view_slots,view_available_dates,no_availability): with GTM configured they are pushed towindow.dataLayer; otherwise, with Google Analytics configured, they are sent directly withgtag(). Strictly one or the other — GTM wins. booking_completedon the booking confirmation page: pushed todataLayerwhen GTM is configured, and sent withgtag()whenever Google Analytics is configured. With both integrations configured, both happen on the same page load. In that case do not also forwardbooking_completedfrom your GTM container to GA4, or every conversion is counted twice.booking_completedon the order confirmation page (cart checkout) follows the availability events’ rule instead: dataLayer with GTM configured, otherwisegtag()— never both.
Events
| Event | Fires when |
|---|---|
| view_slots | Slots page rendered with at least one bookable time slot |
| view_available_dates | Date-picker calendar view rendered with at least one available day |
| no_availability | Either view rendered with a count of 0 (fires instead of the two above) |
| booking_completed | Booking confirmation page. Also the order confirmation page after a paid cart checkout — once per booking in the order |
| sign_up | A new customer record was created via the booking site (first booking or registration); never on later bookings, never for staff-created customers |
| login | An existing customer authenticated (code/link or social login); not on remember-me cookie auto-login. A new registration fires sign_up only, never both |
| purchase | A payment was captured. Free bookings and unpaid "pay later"/invoice bookings fire booking_completed only |
Payload reference
All values are strings unless noted. Multi-value fields are comma-separated strings, since GA4 does not support array parameters. An empty string on the resource fields means the visitor did not filter.
view_slots, view_available_dates, no_availability
| Field | Type / example | Description |
|---|---|---|
| event | "view_slots" | dataLayer only. With gtag the event name is sent as the event, not as a field. |
| service_id | "1201" | Stable id, survives renames. |
| service_title | "Eye exam" | |
| selected_resource_ids | "88" or "" | Set only when the visitor filtered on one or more resources. |
| selected_resource_titles | "Oslo clinic" or "" | |
| slots_available | 3 (number) | Time slots in the rendered range. Slots view only. |
| dates_available | 5 (number) | Days with availability. Calendar view only — a different unit than slots_available, hence a different parameter. |
| from_date / to_date | "2026-09-14" | The rendered date range (ISO 8601). Differs on week-view templates. |
| next_available_date | "2026-10-02" or null | Set only when nothing was available. |
| days_to_next_available | 18 (number) or null | next_available_date minus from_date. |
no_availability carries whichever count field matches the view it replaced (slots_available on the slots view, dates_available on the calendar view), with the value 0.
booking_completed
| Field | Type / example | Description |
|---|---|---|
| booking_id | 4711 (number) | |
| booking_title | "Eye exam" | |
| booking_count | 1 (number) | Number of people. |
| resource_id | "88" | The booked resource. |
| resource_title | "Oslo clinic" |
sign_up and login
No parameters — the event name is the payload. No customer details are sent.
purchase
| Field | Type / example | Description |
|---|---|---|
| transaction_id | "booking-4711" or "order-321" | GA4's standard transaction field. Forward it unchanged — GA4 deduplicates purchases by it, covering legitimate re-fires such as revisiting the payment page of a paid booking. Dedup applies to revenue/purchase counts; raw event counts (Realtime, BigQuery) show every firing. |
| value | 750 (number) | The amount paid, in main currency units. |
| currency | "NOK" | ISO 4217 code. |
| items | array | GA4 standard e-commerce items: one entry per service line with item_id (service id, string), item_name (service title), price (unit price, number) and quantity (number). Single bookings send one item; cart checkouts send one per line item, gift cards included. |
With GTM configured the purchase push is nested in the standard e-commerce shape — {event: "purchase", ecommerce: {transaction_id, value, currency, items}} — so a GA4 event tag reads it with “Send Ecommerce data” enabled (data source: Data Layer). With gtag() the fields are sent directly as event parameters, items included, which GA4 accepts as-is. The items array is the standard GA4 e-commerce exception to the no-arrays rule above. Payments confirmed purely by a server-to-server webhook, where the visitor’s browser never returns to the site, are not emitted.
sign_up, login and purchase follow the availability events’ dispatch rule: dataLayer with GTM configured, otherwise gtag() — never both. As GA4 standard event names they need no renaming in a GTM container.
URL structure
The booking site URL structure identifies most funnel steps on its own, usable with plain GTM page-path triggers:
- Service list:
/services— and/categories/<id>-<slug>/servicesfor accounts using categories. Cover both. - Slots:
/services/<id>-<slug>/slots?selected_resources=<id>&from=<date> - The service id can be extracted from the path with the regex
^/services/(\d+). Never match on the slug — it changes when the service is renamed.
Custom JavaScript
You can add your own JavaScript to the booking site footer with the {% script %} mechanism under ‘Setup -> Booking site’. If you previously pushed a custom event inside the no availability text as a workaround, remove it — otherwise no_availability is emitted twice.