Google Tag Manager
MakePlans can send visitor data from your booking site to Google Tag Manager. You only need to enter your GTM container ID. To find your container ID see Google's guide. Note that you only need to enter your container ID - not the code snippet (JavaScript).
Go to Setup > Booking site in your MakePlans account and enter your GTM container ID.
MakePlans then loads the GTM container on your booking site and pushes booking funnel events to the dataLayer. This article explains what the events contain and how to get them into Google Analytics 4.
Important: with GTM configured, events are pushed to the dataLayer and are not sent directly to GA4. With only Google Analytics configured, they are sent directly to GA4 instead - never both, to prevent double counting. The one exception is booking_completed on the booking confirmation page: it is always sent directly to GA4 when Google Analytics is configured, even if you also use GTM. If you have entered both a GTM container ID and a GA4 measurement ID, read the warning about double counting below before you configure conversion tracking.
How the dataLayer works
This is the most common misunderstanding, so read this first.
With GTM configured, MakePlans only pushes events to the page's dataLayer. The dataLayer is a queue on the page - nothing reaches Google Analytics until your GTM container is set up to forward the events. You need three things in your container:
- A Google tag with your GA4 Measurement ID.
- A Custom Event trigger for each event name you want to track: view_slots, no_availability, view_available_dates, booking_completed, sign_up, login, purchase.
- A GA4 Event tag for each trigger, mapping the dataLayer fields to GA4 event parameters using Data Layer Variables.
If you see no data in GA4, the missing piece is almost always in your GTM container, not in MakePlans. If an agency manages your GTM container, send them this article - the tables below contain everything they need.
The events
The booking site pushes these events:
- view_slots - a visitor sees available time slots for a service.
- view_available_dates - a visitor sees the date-picker calendar for a service.
- no_availability - a visitor looked for a time or date and none was available. This fires instead of the two events above when the count is 0.
- booking_completed - a booking was completed (the confirmation page). After a paid cart checkout it also fires on the order confirmation page, once per booking in the order.
- sign_up - a new customer registered (first booking or explicit registration). Does not fire on later bookings by the same customer, and never for customers created by staff.
- login - an existing customer logged in (code, link or social login). Not fired on automatic cookie login. A brand-new registration fires sign_up only, never both.
- purchase - a payment was captured. Free bookings and unpaid "pay later" or invoice bookings never fire it - they fire booking_completed only.
Event reference
All payload values are strings unless noted. Fields that can hold multiple values are comma-separated strings, because GA4 does not support array parameters. An empty string means the visitor has not filtered.
view_slots - the slots page rendered with at least one bookable slot:
| Field | Example | Notes |
|---|---|---|
| service_id | "1201" | Stable ID, does not change if the service is renamed. |
| service_title | "Eye exam" | |
| selected_resource_ids | "88" or "" | Only set when the visitor filtered on a resource. |
| selected_resource_titles | "Oslo clinic" or "" | Only set when the visitor filtered on a resource. |
| slots_available | 3 (number) | Time slots shown in the rendered date range. |
| from_date / to_date | "2026-09-14" | The rendered date range. Differs on week-view templates. |
| next_available_date | "2026-10-02" or null | Only set when nothing was available. |
| days_to_next_available | 18 (number) or null | Precomputed: next_available_date minus from_date. |
no_availability - same payload as the view the visitor was on, but the count is 0. This is your "demand exceeded capacity" signal.
view_available_dates - the date-picker calendar view. Same payload as view_slots, but with dates_available (number of days with availability) instead of slots_available. This is a different unit - days, not time slots - so it is deliberately a different parameter.
booking_completed - the confirmation page:
| Field | Example | Notes |
|---|---|---|
| booking_id | 4711 (number) | |
| booking_title | "Eye exam" | |
| booking_count | 1 (number) | Number of people, usually 1. |
| resource_id | "88" | The booked resource. |
| resource_title | "Oslo clinic" | The booked resource. |
After a paid cart checkout, booking_completed also fires on the order confirmation page - once per booking in the order, with the same fields. On that page it is sent either via GTM or directly to GA4, never both, so the double-counting warning below does not apply there.
sign_up and login carry no parameters - just the event itself. No customer details are sent.
purchase - a payment was captured:
| Field | Example | Notes |
|---|---|---|
| transaction_id | "booking-4711" or "order-321" | GA4's standard transaction field. |
| 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 order line with item_id (service ID, string), item_name (service title), price (unit price, number) and quantity (number). A single booking sends one item; a cart checkout sends one per order line - products and gift cards included. |
Because purchase carries value, currency and the standard items array, GA4's aggregate revenue reports (Total revenue, Purchases) and the item-level and product monetization reports work directly, in both the GTM and the direct GA4 path.
With GTM configured, purchase is the one event pushed nested in the GA4 e-commerce shape rather than flat:
{
"event": "purchase",
"ecommerce": {
"value": 750.0,
"currency": "NOK",
"transaction_id": "order-321",
"items": [
{"item_id": "1201", "item_name": "Eye exam", "price": 750.0, "quantity": 1}
]
}
}
All other events are flat pushes. With only Google Analytics configured, the same fields including items are sent directly as event parameters - GA4 accepts this as-is, so no setup change is needed.
Always forward transaction_id unchanged: GA4 uses it to deduplicate purchases, which covers legitimate re-fires such as a customer revisiting the payment page of a paid booking. The dedup applies to revenue and purchase counts - raw event counts (Realtime, BigQuery) still show every firing. Also verify that purchase is marked as a Key Event under Admin > Events - this is usually automatic but not guaranteed.
Recommended GTM setup
In your GTM container you decide what each dataLayer event is called when it is sent on to GA4. You can keep the names as they are or rename them. We suggest this mapping:
| dataLayer event | GA4 event | Parameters |
|---|---|---|
| view_slots | availability_checked | availability_status: "available", availability_count (from slots_available), service_title, selected_resource_titles |
| view_available_dates | availability_checked | availability_status: "available", availability_count (from dates_available - not slots_available), service_title, selected_resource_titles |
| no_availability | availability_checked | availability_status: "unavailable", days_to_next_available, service_title, selected_resource_titles |
| booking_completed | booking_completed (forward as-is) | resource_title, booking_title, booking_count |
| sign_up / login / purchase | forward as-is | All fields. These are GA4 standard event names already. For purchase, enable "Send Ecommerce data" with data source Data Layer - no manual parameter mapping is needed; transaction_id, value, currency and items are read from the ecommerce object. |
The only rename we suggest is merging the three availability events into one availability_checked event. One event with an availability_status parameter is easier to report on than separate events: the share of checks that found nothing becomes a single number you can chart and use in funnels.
To do the merge with one trigger, create a Custom Event trigger with "Use regex matching" and the event name ^(view_slots|view_available_dates|no_availability)$, and a Lookup Table variable on the built-in Event variable to set availability_status. If you prefer something simpler to copy, three separate triggers and tags work just as well.
Mark booking_completed as a Key Event in GA4. If you run Google Ads, also import it as a conversion (the category "Book appointment" fits).
Warning: if you have entered both Google Tag Manager and Google Analytics in MakePlans, do not forward booking_completed from GTM to GA4. The page already sends it directly to GA4, so forwarding it doubles every conversion in your reports.
URL-based tracking
This section is for developers and GTM users who prefer page-path triggers. The booking site URL structure identifies most funnel steps on its own, with no events needed:
- Service list: /services - and /categories/<id>-<slug>/services for accounts using categories. Cover both in your triggers.
- Slots page: /services/<id>-<slug>/slots?selected_resources=<id>&from=<date>
- The selected_resources query parameter can be read with GTM's built-in URL variable (component type "Query").
- The service ID can be extracted from the path with the regex ^/services/(\d+)
Two caveats. The slug changes when a service is renamed, so never match on the slug. And IDs need a manually maintained lookup table to become readable in reports - the events above carry titles precisely to avoid this.
Reporting in GA4
- Custom event parameters only appear in GA4 reports after you register them as event-scoped custom dimensions under Admin > Custom definitions. Register service_title, selected_resource_titles and availability_status. Do not register booking_id, from_date or other high-cardinality values.
- Use closed funnels in Funnel Explorations for completion-rate reports. Open funnels inflate the rates.
- Compare locations with rate metrics (completion percentage, no-availability percentage of views) and unique users, never raw event counts. One visitor paging through empty weeks fires many no_availability events.
Known limitations
- Location breakdown on the availability events only covers visitors who explicitly filtered on a resource. The default view combines all resources and sends empty resource fields. For per-location completion, use resource_title on booking_completed - it always has the booked resource.
- The events capture which day the visitor wanted, not which hour. "Wanted 9am, only 2pm was free" is not distinguishable.
- A visitor who saw slots that did not fit their schedule looks identical to a satisfied visitor.
- Payments confirmed purely by a server-to-server notification, where the customer's browser never returns to the booking site, do not emit purchase.
A note on sensitive data
Service titles and resource titles (often practitioner names) become analytics data in your Google account. Page URLs already contain service slugs, so the events add little extra exposure. But if you run a medical, health or therapy business you should consider using IDs with a GTM lookup table instead of the title parameters, and review whether Google Analytics belongs on your booking site at all. Google prohibits personally identifiable information in Analytics and restricts sensitive-category audiences.
Custom JavaScript
You can add your own JavaScript to the booking site footer with the {% script %} mechanism under Setup > Booking site. This still works as before.
Warning: if you previously added a custom dataLayer.push inside your no availability text as a workaround for tracking empty availability, remove it now. Otherwise no_availability fires twice.