Guide · Tracking & analytics
Meta Pixel and Conversions API for Microsoft Bookings
Last updated 8 min read7 sources
This guide sets up Meta (Facebook and Instagram) conversion tracking for a business that takes appointments through Microsoft Bookings. It also covers the LinkedIn Insight Tag, which has the same limits. By the end you'll have pixel events for booking intent, and optionally a Conversions API event for each completed booking. It's for performance marketers and the developers who support them. The pixel part takes under an hour. The Conversions API relay needs a developer for about a day.
Why this is harder than usual is covered in Can I add a Meta pixel to Microsoft Bookings?. In short, Microsoft hosts the page, there's no pixel field, and there's no thank-you redirect. People who asked Microsoft Q&A about adding a pixel were sent to other forums without an answer.
Before you start#
- The Meta Pixel on your site, loaded by its base code or through GTM, and access to that pixel in Events Manager.
- For the Conversions API: an access token for the pixel, generated in Events Manager, somewhere to run a small HTTPS endpoint (Azure Functions, a serverless worker or your web backend), the Administrator role on the Bookings calendar, and Power Automate with access to the HTTP action.
- For LinkedIn: the Insight Tag on your site and a Campaign Manager account where you can create conversions.
Level 1: pixel events on your own pages#
The pixel can see what happens on your pages: someone viewing the booking section or clicking through to Microsoft's page. Record those under names that make clear they're intent, not bookings.
Create a GTM Custom HTML tag that fires on the "Bookings link click" trigger from the GA4 and GTM guide:
<script>
if (window.fbq) {
fbq('trackCustom', 'BookingClick', { link_url: {{Click URL}} });
}
</script>Add a second tag on the iframe visibility trigger from that guide if you want a view signal. The standard ViewContent event is fine for this.
Leave the standard Schedule event for a confirmed booking. If campaigns optimise on a click event, Meta learns to find people who click and leave. If you have to optimise on something today, build a custom conversion from BookingClick and treat its numbers as upper bounds.
Level 2: a Conversions API event for each booking#
How it fits together#
The Conversions API accepts events from your server. The hard part is telling Meta who booked, so it can match the event to an ad. Meta's rules for website events make this tricky with Bookings:
action_sourceshould bewebsite, andevent_source_urlshould be the page where the action happened.user_datamust includeclient_user_agentandclient_ip_addressfor website events.- Emails (
em) and phones (ph) must be normalised and hashed with SHA-256. event_timecan be at most 7 days old. If one event in a request is older, Meta rejects the whole request.
The Bookings connector gives you the customer's email and the appointment. It doesn't give you the visitor's browser, IP address or Meta cookies, and Microsoft's RefID can only hold letters, digits, underscores and hyphens. So you store those details on your side under a short key, and pass only the key through RefID:
Your page: key k3f9a... + _fbc/_fbp cookies, user agent, IP -> your endpoint stores them
Link: outlook.office.com/book/.../?RefID=k3f9a...
Bookings: appointment created -> Power Automate (TrackingData = k3f9a..., customer email)
Flow HTTP action -> your endpoint -> hash email, add stored details -> Conversions APIThe limits from the other guides apply here too. The Bookings connector is in Preview. Only Bookings admins can create these flows, and each mailbox allows five. The HTTP action is labelled Premium, and Microsoft says Microsoft 365 plans only include standard connectors. RefID has been reported to cause Bad Request pages and blank Tracking data, so test it on your page first. See RefID not working.
Step 1: Give each visitor a key and record it on click#
Run this only for visitors who have consented to marketing cookies.
(function () {
function cookie(name) {
var m = document.cookie.match(new RegExp("(?:^|;\\s*)" + name + "=([^;]+)"));
return m ? m[1] : undefined;
}
var key = sessionStorage.getItem("bk_key");
if (!key) {
key = "k" + crypto.randomUUID().replace(/-/g, "").slice(0, 15);
sessionStorage.setItem("bk_key", key);
}
document.querySelectorAll('a[href*="outlook.office"]').forEach(function (a) {
var url = new URL(a.href);
if (!/\/(book|owa\/calendar)\//.test(url.pathname)) return;
url.search = "";
url.searchParams.set("RefID", key);
a.href = url.toString();
a.addEventListener("click", function () {
navigator.sendBeacon("/api/booking-intent", JSON.stringify({
key: key, fbc: cookie("_fbc"), fbp: cookie("_fbp"), pageUrl: location.href
}));
});
});
})();The _fbc cookie is set by the pixel when a visitor arrives from an ad with fbclid. _fbp is the pixel's browser ID.
Step 2: The relay endpoint#
This sketch uses web-standard Request and Response, which run on Node 18+, Azure Functions v4 and most serverless platforms. Use any key-value store with a TTL of 7 days or less.
import { createHash } from "node:crypto";
const sha256 = (v) => createHash("sha256").update(v).digest("hex");
// POST /api/booking-intent (from the browser)
export async function recordIntent(req, store) {
const { key, fbc, fbp, pageUrl } = JSON.parse(await req.text());
await store.set(key, {
fbc, fbp, pageUrl,
ua: req.headers.get("user-agent"),
ip: (req.headers.get("x-forwarded-for") || "").split(",")[0].trim()
});
return new Response(null, { status: 204 });
}
// POST /api/booking-created (from Power Automate)
export async function sendSchedule(req, store) {
if (req.headers.get("x-flow-secret") !== process.env.FLOW_SECRET) {
return new Response("forbidden", { status: 403 });
}
const { refId, email, appointmentId } = await req.json();
const intent = await store.get(refId);
if (!intent) return new Response("unknown key", { status: 404 });
const payload = {
data: [{
event_name: "Schedule",
event_time: Math.floor(Date.now() / 1000),
event_id: "bookings-" + appointmentId,
action_source: "website",
event_source_url: intent.pageUrl,
user_data: {
em: [sha256(email.trim().toLowerCase())],
client_user_agent: intent.ua,
client_ip_address: intent.ip,
fbc: intent.fbc,
fbp: intent.fbp
}
}]
};
const url = "https://graph.facebook.com/" + process.env.META_API_VERSION + "/" +
process.env.META_PIXEL_ID + "/events?access_token=" + process.env.META_TOKEN;
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
return new Response(await res.text(), { status: res.status });
}Keep the Meta token on the endpoint, not in the flow. That also keeps it out of Power Automate run history.
Step 3: The flow#
- Trigger: When a appointment is Created (Microsoft Bookings) with your booking page's SMTP address.
- Condition: Tracking data starts with
k. - HTTP action: POST to
https://your-domain.example/api/booking-created, with headerx-flow-secretand this body:
{
"refId": "TrackingData value",
"email": "Customer email value",
"appointmentId": "SelfServiceAppointmentId value"
}Map the three values from Dynamic content. The connector marks Id as deprecated, so use SelfServiceAppointmentId.
De-duplication#
Meta treats a browser event and a server event as one when the pixel's eventID matches the API's event_id and the event names match, within 48 hours. With Microsoft's page, the pixel never fires Schedule, so nothing overlaps and the server event stands alone. If you later add a browser-side Schedule (see the last section), send the same ID from both sides.
LinkedIn Insight Tag#
LinkedIn follows the same pattern. The Insight Tag runs on your pages and can't run on Microsoft's.
What works natively: create a conversion in Campaign Manager that fires from code rather than from a page URL, copy its conversion ID, and fire it on the Book-link click from GTM:
<script>
if (window.lintrk) {
window.lintrk('track', { conversion_id: 1234567 });
}
</script>Name the conversion so reports make clear it's a click, for example "Booking click".
Completed bookings: LinkedIn also accepts server-side conversions, matched on hashed email or the LinkedIn click ID (li_fat_id). You could extend the relay above with a LinkedIn branch, storing li_fat_id from the landing URL alongside the Meta cookies. LinkedIn's payload and authentication are different from Meta's, so follow LinkedIn's current Conversions API documentation rather than reusing the Meta request.
Testing#
- Meta Pixel Helper (browser extension): click a Book link and confirm
BookingClickfires once, with the link URL. - Events Manager → Test events: open your site from the test link and check that browser events appear.
- Server events: temporarily add
"test_event_code": "TEST12345"next todatain the payload, using the code shown in Test events. Make a booking through a tagged link and check thatSchedulearrives with a match on email and browser data. Remove the code afterwards. Meta says it's only for testing. - Flow run history: confirm Tracking data held your key and the HTTP action got a 2xx. A 404 from your endpoint means the intent record wasn't stored or has expired.
- LinkedIn: after a test click, check the conversion's status in Campaign Manager. LinkedIn may take a while to show the first event.
Common problems#
Meta rejects the batch for an old event_time. You replayed failed runs more than 7 days later. Let those go.
Low match quality. The visitor wasn't from an ad (no _fbc), blocked cookies, or the email hash wasn't lowercased and trimmed before hashing.
Unknown key (404). The visitor opened Bookings from a link without a key (a bookmark or a forwarded email), or took longer to book than your store keeps records.
Bad Request on Microsoft's page. Strip every other query string and test a plain RefID. See the UTM guide for what Microsoft's page does with parameters.
Privacy review. You're sending customer emails to your endpoint and to Meta. Update your privacy notice and respect consent choices before you turn this on.
Doing this with BookingsXP#
BookingsXP runs the booking in a widget on your own page, in front of your existing Bookings page, so the pixel sees the real outcome. When the booking is confirmed, it fires fbq('track', 'Schedule', {}, { eventID: 'BXP-7K3M9Q' }), where the event ID is the booking reference, and it fires ViewContent when the widget is shown. With a LinkedIn conversion ID set, it calls lintrk on each confirmed booking. Each fires only if the tag is already on your page and its ID is set on the widget.
BookingsXP doesn't send Conversions API events itself. If you want server events as well, send Schedule yourself from a booking.created webhook, using the booking reference as event_id, and Meta counts the booking once. Webhooks need Store bookings in BookingsXP switched on. The widget also writes the booking's source, including which ad click ID was present (such as fbclid or li_fat_id), into the booking notes in Microsoft Bookings. See analytics and pricing for plan details. BookingsXP is independent and not affiliated with or endorsed by Microsoft.
Questions people also ask
Sources
- Microsoft Learn: Microsoft bookings help conversion pixel (opens in a new tab) · learn.microsoft.com
- Microsoft Tech Community: Microsoft booking custom thank you page url (opens in a new tab) · techcommunity.microsoft.com
- Microsoft Learn: Customize booking page (opens in a new tab) · learn.microsoft.com
- Microsoft Learn (opens in a new tab) · learn.microsoft.com
- Microsoft Learn: Power automate licensing (opens in a new tab) · learn.microsoft.com
- developers.facebook.com: Using the api (opens in a new tab) · developers.facebook.com
- developers.facebook.com: Deduplicate pixel and server events (opens in a new tab) · developers.facebook.com