Guide · Tracking & analytics

Track Microsoft Bookings conversions in GA4 with GTM

Last updated 10 min read10 sources

This guide gets Microsoft Bookings into GA4 as far as the platform allows. You'll set up GA4 events for booking intent on your own site, and, if you need more, a GA4 key event for each completed appointment. It's written for marketers and developers who already run GA4 through Google Tag Manager and send visitors to a shared Microsoft Bookings page. Level 1 takes about an hour. Level 2 takes half a day including testing. The last section covers the fastest route, which is running the booking steps on your own page.

If you only want the short answer on why GA4 can't see Bookings, read how to track Bookings conversions in GA4 first. This guide is the build.

Before you start#

You need:

  • A GA4 property with a web data stream, and a GTM container on your site that already loads the GA4 Google tag.
  • Publish rights in GTM.
  • The link to your shared booking page, or the iframe you embed it with.
  • For level 2 only: the Administrator role on that Bookings calendar, access to Power Automate, and permission to create a Measurement Protocol API secret in GA4.

The three levels at a glance#

Microsoft hosts the booking page on outlook.office.com. You can't add a tag to it, and it doesn't redirect to your site after a booking. A Microsoft employee confirmed there is "no native support to redirect" in 2023. Embedding changes nothing, because a cross-origin iframe hides its contents from the parent page, and Microsoft doesn't document any postMessage events. The details are in GTM and the Bookings iframe.

LevelWhat GA4 recordsEffortReliability
1. GTM on your pagesClicks on Book links, the booking iframe coming into viewAbout 1 hourHigh, but measures intent only
2. Power Automate plus Measurement ProtocolOne event per booking created in BookingsHalf a dayDepends on RefID and a Preview connector
3. A booking widget on your own pageEvery step, including the confirmed bookingAbout 20 minutesHigh, because the events fire in your page

Level 1: measure booking intent in GTM#

Step 1: Turn on the click variables#

In GTM, go to Variables → Configure and tick Click URL and Click Text under Clicks.

  1. Triggers → New, type Click - Just Links.
  2. Choose Some Link Clicks.
  3. Condition: Click URL matches RegEx outlook\.office(365)?\.com/(book|owa/calendar)/|book\.ms/.
  4. Name it "Bookings link click" and save.

The regex covers the current /book/ address, the older /owa/calendar/ address (which Microsoft says redirects) and book.ms short links.

Step 3: Create an iframe visibility trigger#

  1. Triggers → New, type Element Visibility.
  2. Selection method: CSS Selector. Element selector: iframe[src*="outlook.office"].
  3. When to fire: Once per page.
  4. Minimum percent visible: 50. Optionally set a minimum on-screen duration of 3000 ms if you only want to count people who stopped to look.
  5. Tick Observe DOM changes if your CMS inserts the iframe after the page loads.

Step 4: Add two GA4 event tags#

TagEvent nameParametersTrigger
GA4 - Bookings clickbooking_clicklink_url = {{Click URL}}, link_text = {{Click Text}}Bookings link click
GA4 - Bookings viewbooking_widget_viewnone neededElement Visibility trigger

Use tag type Google Analytics: GA4 Event with your existing measurement ID. Publish the container.

If you mark booking_click as a key event, name it clearly (for example "Booking intent"). Many clicks end without a booking, so don't report it as a booking.

Level 2: send completed bookings with Power Automate#

This is the workaround that a 2026 Microsoft Q&A answer described. Store the visitor's GA4 client_id in the Bookings link, catch the new appointment in Power Automate, and post a GA4 event through the Measurement Protocol.

Text
Your page: read client_id  ->  link to Bookings with ?RefID=ga-123456789-1712345678
Bookings: appointment created  ->  Power Automate trigger (TrackingData = ga-123456789-1712345678)
Flow: rebuild client_id 123456789.1712345678  ->  POST to GA4 Measurement Protocol

Know the limits before you build#

  • The connector is in Preview. Its trigger is named "When a appointment is Created" (Microsoft's spelling). Only Bookings admins can create flows with it, Microsoft allows 5 flows per Bookings mailbox, and the connector isn't available in GCC, GCC High, DoD or 21Vianet.
  • Licensing. The Bookings connector is Standard, but the HTTP action is labelled Premium. Microsoft's licensing FAQ says users on Microsoft 365 plans "can only access standard connectors". For an automated flow, only the flow owner needs the Premium licence. Check what the Power Automate designer shows in your tenant.
  • RefID may fail. Microsoft's docs say the campaign ID can only contain "alphanumeric characters, underscore or hyphen" and tell you to test it. In November 2023, one organisation got Bad Request on 79 of its 80 pages. In 2024 and 2025, users reported a blank Tracking data column. Test on your own page first. See RefID not working.
  • The client_id contains a dot. Google defines it as two numbers joined by a period, and a dot isn't allowed in RefID. So you swap the dot for a hyphen on the way in and swap it back in the flow.
  • One RefID per link. If you also want campaign codes or a gclid in the link, see the lookup-key approach in passing UTM parameters into Bookings.

Step 1: Create a Measurement Protocol API secret#

In GA4, go to Admin → Data collection and modification → Data streams, open your web stream, then Measurement Protocol API secrets → Create. Google says the secret is private, so keep it in the flow and never put it in page code.

Add this in a GTM Custom HTML tag that fires on Window Loaded, or in your site's own scripts. It reads the _ga cookie, so it only runs once the visitor has allowed analytics and GA4 has set that cookie.

JavaScript
(function () {
  function gaClientId() {
    var m = document.cookie.match(/(?:^|;\s*)_ga=GA\d\.\d+\.(\d+\.\d+)/);
    return m ? m[1] : null;
  }

  function tagBookingLinks() {
    var cid = gaClientId();
    if (!cid) return;
    var refId = "ga-" + cid.replace(/\./g, "-"); // RefID allows A-Z a-z 0-9 _ - only
    var links = document.querySelectorAll('a[href*="outlook.office"]');
    links.forEach(function (a) {
      var url = new URL(a.href);
      if (!/\/(book|owa\/calendar)\//.test(url.pathname)) return;
      url.search = ""; // extra query strings have broken Bookings pages before
      url.searchParams.set("RefID", refId);
      a.href = url.toString();
    });
  }

  setTimeout(tagBookingLinks, 500);
})();

If you embed the page in an iframe, set the iframe src the same way before it loads, rather than changing it afterwards.

Step 3: Build the flow#

  1. In Power Automate, create an Automated cloud flow with the Microsoft Bookings trigger When a appointment is Created. Paste your booking page's SMTP address if it isn't in the dropdown.
  2. Add a Condition: Tracking data (from Dynamic content) starts with ga-.
  3. In the Yes branch, add a Compose action named client_id with this expression:
Text
replace(replace(triggerBody()?['TrackingData'], 'ga-', ''), '-', '.')

If your trigger lists the field under a different name, pick Tracking data from Dynamic content instead of typing the path.

  1. Add an HTTP action. Set the method to POST, the header Content-Type: application/json, and this URI:
Text
POST https://www.google-analytics.com/mp/collect?measurement_id=G-XXXX&api_secret=YOUR_API_SECRET
  1. Use this body, mapping the values from Dynamic content:
JSON
{
  "client_id": "123456789.1712345678",
  "events": [
    {
      "name": "booking_completed",
      "params": {
        "service_name": "Initial consultation",
        "booking_source": "microsoft_bookings",
        "appointment_id": "SelfServiceAppointmentId value",
        "engagement_time_msec": 100
      }
    }
  ]
}

client_id is the Compose output. service_name comes from the service name, and appointment_id from SelfServiceAppointmentId (the connector marks Id as deprecated). Don't send the customer's name, email or phone to GA4.

Leave out timestamp_micros. The flow runs within minutes of the booking, and Google only accepts backdating up to 72 hours. It also warns that events received after 48 hours may not be attributed as expected.

Step 4: Mark it as a key event#

Once the first booking_completed event arrives, mark it as a key event in GA4 (Admin, under Data display). From then on, GA4 counts completed bookings as conversions.

One caveat: this event has no session attached. Google's reference describes session_id as "required for several common use cases", and without it, session-scoped source and medium for these events can show as (not set). User-scoped acquisition still works through the client_id. If you need session attribution, store the session ID along with the client_id using a lookup key, which is covered in the UTM guide.

Testing#

  1. GTM Preview. Open Preview, click a Book link and check that "GA4 - Bookings click" fired on the Link Click event. Scroll the iframe into view and check for a gtm.elementVisibility event.
  2. GA4 DebugView. With Preview running, open DebugView in GA4 Admin. booking_click and booking_widget_view should appear with their parameters.
  3. The link. Hover a Book link and check it ends in ?RefID=ga- followed by digits and one hyphen. Open it. If you get Bad Request, stop here and read the RefID answer.
  4. The Measurement Protocol body. Send it once to https://www.google-analytics.com/debug/mp/collect with the same query string. The validation server returns validationMessages. Google notes that these events don't appear in reports.
  5. End to end. Make a real test booking through a tagged link. In the flow's run history, check that Tracking data is filled, the Compose output has a dot, and the HTTP action returned 2xx. Then check GA4's Realtime report for booking_completed.
  6. Cancel the test appointment in Bookings.

Common problems#

The click tag never fires. The Book button may be a button rather than a link, or JavaScript may add it after the page loads. Use a Click - All Elements trigger with a CSS selector on the button.

The visibility trigger never fires. The iframe's src might use a book.ms or old owa address, or it's injected late. Adjust the selector and tick Observe DOM changes.

The flow never runs. Check that you're a Bookings admin on that calendar, that the SMTP address is correct, and that you haven't hit the five-flow limit. Microsoft documents that going over the limit returns HTTP 429. See Power Automate trigger not firing.

Tracking data is empty. The visitor booked without the tagged link (from an email, a bookmark or a shared link), they declined analytics cookies, or RefID isn't being kept on your page. Blank values are a known report, not always your mistake.

GA4 shows the event but not the campaign. That's the missing session context described in step 4. Compare it with user-scoped first user source instead.

Bookings are counted twice. In a group booking, the second and later attendees fire the Updated trigger, not Created, so this flow shouldn't double count them. If you also fire a key event from level 3 below, send it from only one place.

Doing this with BookingsXP#

BookingsXP is the level 3 route. It puts a booking widget on your own page in front of your existing Bookings page, and appointments are still created in Microsoft Bookings. Because the steps run where GTM can see them, each one is pushed to window.dataLayer, and you need no RefID, flow or API secret.

In GTM:

  1. Triggers → New → Custom Event. Event name: bookingsxp.booking_completed. This trigger fires on: All Custom Events.
  2. Create Data Layer Variables for bookingsxp.booking.id, bookingsxp.service.name and bookingsxp.service.price.
  3. Add a GA4 Event tag named generate_lead with transaction_id set to the booking ID variable, fired by that trigger.

The push looks like this, with no customer name or email in it:

JavaScript
window.dataLayer.push({
  event: "bookingsxp.booking_completed",
  bookingsxp: {
    service: { name: "Initial consultation", price: 60, currency: "GBP" },
    staff: { name: "Sam Lee" },
    slot: { start: "2026-10-02T14:00:00Z" },
    booking: { id: "BXP-7K3M9Q" }
  }
});

Without GTM, listen for the DOM event instead:

JavaScript
window.addEventListener("bookingsxp:booking_completed", function (ev) {
  console.log("Booked", ev.detail.booking.id, ev.detail.service.name);
});

If you add your GA4 ID to the widget, it sends generate_lead and bookingsxp_booked itself. In that case, skip the GTM tag so each booking is counted once. See analytics and embedding. BookingsXP is independent and not affiliated with or endorsed by Microsoft.

Questions people also ask

Sources

  1. Microsoft Learn: Customize booking page (opens in a new tab) · learn.microsoft.com
  2. Microsoft Learn (opens in a new tab) · learn.microsoft.com
  3. Microsoft Learn: Power automate integration (opens in a new tab) · learn.microsoft.com
  4. Microsoft Learn: Power automate licensing (opens in a new tab) · learn.microsoft.com
  5. Microsoft Tech Community: Microsoft booking custom thank you page url (opens in a new tab) · techcommunity.microsoft.com
  6. Microsoft Tech Community: Ms bookings tracking data refid generate bad request page (opens in a new tab) · techcommunity.microsoft.com
  7. Microsoft Learn: Track which ads are leading to completed microsoft (opens in a new tab) · learn.microsoft.com
  8. developers.google.com: Sending events (opens in a new tab) · developers.google.com
  9. developers.google.com (opens in a new tab) · developers.google.com
  10. developers.google.com: Validating events (opens in a new tab) · developers.google.com