Technical

The iframe problem: why you can't track Boulevard widget bookings in the browser

We tried every browser-side trick to catch Boulevard bookings. None of them work, and here is exactly why.

Hamza El Asri Hamza El AsriFounder & lead strategist, RM Click PublishedNovember 10, 2026 Reading time9 min read AudienceDevelopers

If you're trying to fire a Facebook, Google Ads or TikTok conversion when someone books through Boulevard's embedded widget, you'll find that no browser-side event ever tells you the booking happened.

Short answer: the widget runs in a cross-origin iframe, window.blvd has no event API, and the iframe doesn't send useful postMessage events. You can't detect the booking from the parent page. You need Boulevard's webhooks and a server that sends the conversion to each platform.

This is from a pipeline we built for a US salon using Boulevard, running ads on Meta, Google Ads, TikTok, Snapchat and Nextdoor. Here's everything we tried, so you don't have to.

Based on our hands-on build in 2026. Boulevard can change its widget at any time, so re-check these behaviors against your own setup.

How the Boulevard widget loads

The standard install adds Boulevard's loader script to your page. Calling blvd.init() with your business ID sets it up, and a Book Now button calls blvd.openBookingWidget(), which shows the booking flow as an overlay.

That overlay is an iframe served from Boulevard's domain. The browser's same-origin policy means your page can't read the iframe's DOM, its form fields or its network calls. Your pixels only run on your page.

Attempt 1: listen for widget events

The natural first idea is an event hook, something like blvd.on('booking.complete', …). We inspected the object at runtime:

// Run in the browser console on a page with the Boulevard widget loaded
console.log(Object.keys(window.blvd));
// In our build: init, continueInit, loadDesktop, openBookingWidget, ...
console.log(typeof window.blvd.on);
// "undefined": there is no event API

window.blvd only exposed loader and config methods. There was no .on(), no callback option and no booking event to subscribe to.

Attempt 2: listen for postMessage

Iframes can talk to their parent with window.postMessage. If Boulevard broadcast a "booking created" message, the parent could catch it. So we logged everything:

// Diagnostic only: log every message coming from Boulevard's iframe
window.addEventListener('message', (event) => {
  // Replace with the origin you see in the iframe's src
  if (!event.origin.includes('YOUR_BOULEVARD_WIDGET_DOMAIN')) return;
  console.log('[blvd message]', event.origin, event.data);
});

We kept this logger running in production as a diagnostic. It never showed a booking-completed message we could use. Messages related to sizing and loading may appear, but nothing that says "appointment created" with an ID.

Attempt 3: pass tracking data into the booking

The next idea: pass UTMs or click IDs into the widget so they end up on the appointment, then read them back later from Boulevard's API.

In our testing, URL parameters passed to the widget were not saved onto the booked appointment. We also looked for a referralSource field on the client through the Admin GraphQL API. In our build, that's a dashboard feature, not a queryable field. So there was nothing to read back.

Attempt 4: detect the iframe opening

If you can't see the booking, maybe you can at least track intent: fire AddToCart when the widget iframe appears. We did, and it was wrong.

Boulevard preloads a hidden iframe when blvd.init() runs. The iframe exists on every page view, before anyone clicks anything. Our "AddToCart" fired on every page load and flooded the platforms with fake intent. We removed it.

If you want a funnel event, fire it on the Book Now click instead, which you control:

// Fire a funnel event on the Book Now click, not on iframe insertion
document.querySelectorAll('[data-book-now]').forEach((btn) => {
  btn.addEventListener('click', () => {
    const eventId = crypto.randomUUID(); // reuse this ID server-side for dedup if you can
    if (window.fbq) fbq('track', 'InitiateCheckout', {}, { eventID: eventId });
    if (window.ttq) ttq.track('InitiateCheckout', {}, { event_id: eventId });
    window.blvd.openBookingWidget();
  });
});

Why this is not only a Boulevard problem

Even with your own booking form, browser pixels lose events. iOS privacy features, Safari's Intelligent Tracking Prevention, ad blockers and cookie restrictions all cut into what the pixel sees. With an iframe on another domain, you lose the most important event entirely.

What works: capture on the page, convert on the server

Since the parent page can't see the booking, split the job in two:

  1. On your page: capture the click. Read the click IDs from the landing URL and store them server-side.
  2. On your server: receive the booking. Boulevard sends a webhook on APPOINTMENT_CREATED and APPOINTMENT_COMPLETED. Your server verifies it, fetches the appointment from the Admin API, matches it to a stored click and sends it to each platform's Conversions API.

Here's a minimal version of the capture step:

// Capture ad click IDs + UTMs on landing and send them to your server
(function () {
  const params = new URLSearchParams(location.search);
  const keys = ['fbclid', 'gclid', 'gbraid', 'wbraid', 'ttclid', 'ScCid', 'ndclid',
                'utm_source', 'utm_medium', 'utm_campaign', 'utm_content'];
  const data = {};
  keys.forEach((k) => { if (params.get(k)) data[k] = params.get(k); });
  if (!Object.keys(data).length) return;

  data.landing_url = location.href;
  data.ts = Date.now();

  fetch('https://YOUR_WORKER_DOMAIN/track/landing', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
    keepalive: true,
  }).catch(() => {});
})();

We ran the server side on a Cloudflare Worker with KV storage. The Worker acknowledges each Boulevard webhook immediately and does the work asynchronously, because Boulevard expects a 2xx response within seconds and retries otherwise.

The hard part nobody mentions: matching

Here's the catch. The webhook tells you an appointment was created. It does not tell you which visitor created it, because the booking happened in the iframe. Your server has click IDs on one side and appointments on the other, with no shared key.

Any matching based on timing is a guess, and guesses break under real traffic. Ours did: at real volume, bookings were credited to whoever had landed most recently, usually the biggest Meta campaign. We wrote it up in detail in our attribution bug case study.

There are two ways to get a deterministic match:

  • Identity: capture the visitor's email or phone on your page before they book, store the click IDs against a hash of it, and look that hash up when the webhook arrives with the client's email and phone.
  • Own the booking flow: build the booking UI yourself on Boulevard's client-side SDK. Checkout returns the appointment ID in your page, so you can store it together with the click IDs. In our proof of concept, the ID returned at checkout matched the ID in the webhook exactly.

Common errors

  • AddToCart or InitiateCheckout firing on every page view: you're detecting the preloaded hidden iframe. Fire on the Book Now click instead.
  • window.blvd.on is not a function: there's no event API on the widget object.
  • UTMs missing from appointments: URL params passed to the widget are not saved on the booking.
  • Bookings credited to the wrong campaign: recency or IP-style matching under concurrent traffic. Use identity or an owned booking flow.

Testing checklist

  • Click IDs reach your server on landing (check your server logs).
  • Book Now fires one funnel event per click, not per page view.
  • A test booking triggers the APPOINTMENT_CREATED webhook and your server returns 2xx fast.
  • The conversion appears in Meta Events Manager and TikTok Events Manager test tools.
  • Two people book from two devices at the same time, and each booking gets its own click ID.
  • Cancel your test appointments in Boulevard afterward.

If you'd rather read the non-technical version, see why your Facebook ads show 0 bookings when you use Boulevard.

For the full build, step by step, read How to track Boulevard bookings in Meta, Google Ads & TikTok: the complete server-side guide.

Hamza El Asri
Written by Hamza El Asri

Media buyer and tracking specialist, founder of RM Click. Top Rated on Fiverr with 413 reviews. We built the pipeline this article describes. LinkedIn · Fiverr

FAQ

Frequently asked
questions

No. In our build, window.blvd only exposed loader and config methods such as init, continueInit, loadDesktop and openBookingWidget. There was no .on() method and no booking callback.

Not useful ones. We logged every message from Boulevard's iframe and never saw a booking-completed event we could rely on.

You can pass URL parameters to the widget, but in our testing they were not saved onto the booked appointment, so you can't read them back from the API afterward.

Boulevard preloads a hidden iframe when blvd.init() runs. If you fire an event when an iframe appears, it fires on every page view, not when the visitor opens the widget.

Server-side tracking: capture click IDs on your page, receive Boulevard webhooks for APPOINTMENT_CREATED and APPOINTMENT_COMPLETED on a server, match the booking to a click, and send it to each platform's Conversions API.

Free ad audit

Need this set up
for your salon?