If you track bookings from an embedded booking widget, every attribution method you have is a match after the fact. The only fully reliable fix is to make the booking happen in your own page.
Short answer: build the booking flow yourself on Boulevard's client-side book SDK (@boulevard/blvd-book-sdk). When checkout succeeds, you get the appointment ID in the browser, right next to the ad click IDs you captured on landing. Send both to your server together. When Boulevard's webhook arrives with that appointment ID, attribution is an exact lookup, with no timing and no guessing.
This is a planning guide, not a finished build. We validated the key idea with a proof of concept for a US salon using Boulevard, after our timing-based matching failed under real traffic (the case study).
Based on our proof of concept in 2026. Check the SDK's current documentation and your Boulevard plan before committing.
Why the widget can't be fixed
The widget runs in an iframe on Boulevard's domain. Your page can't see the booking, and the webhook doesn't know which visitor made it. The details are in the iframe problem.
So every widget-based method reconstructs the link afterwards:
| Method | How it matches | Reliability |
|---|---|---|
| Most recent landing or intent | Timing | Breaks with concurrent visitors |
| Identity (email/phone hash) | Data typed on both sides | Deterministic when captured, but needs an extra step |
| Own booking flow | Appointment ID from checkout | Deterministic for every booking made through it |
What the proof of concept proved
We completed a real booking programmatically with the book SDK against production:
checkout()returned an appointment ID.- Boulevard then sent
APPOINTMENT_CREATEDfor that appointment. - The two IDs were byte-identical.
That shared ID is the key the widget never gave us. Two more details from the test:
- The client API was authenticated with the application ID alone, as a publishable key, with no secret. The Admin API secret stays on the server.
- The intent heartbeat we used for timing matching wrote to storage every 30 seconds per visitor. The new design writes once per booking.
The target design
Landing page Your booking flow (SDK) Your server
──────────── ─────────────────────── ───────────
capture click IDs ─▶ services → staff → date/time →
client details → checkout()
│ returns appointmentId
└──── POST /track/booking ────────▶ store booking:{appointmentId} → click IDs
▲
Boulevard webhook APPOINTMENT_CREATED (appointmentId) ───────────────┘ exact lookup → fan out to ad platforms
Keep the old matching as a labeled fallback for bookings that don't come through your flow, such as phone or front-desk bookings.
The booking steps you have to rebuild
The SDK sequence we exercised:
// Shape of the flow (names from our proof of concept: check the SDK docs for exact signatures)
const cart = await client.createCart({ locationId });
const categories = await cart.getAvailableCategories(); // service menu
await cart.addBookableItem(item /*, staffVariant */); // service (+ optional staff)
const dates = await cart.getBookableDates();
const times = await cart.getBookableTimes(dates[0]);
await cart.reserveBookableItems(times[0]); // hold the slot
await cart.updateClientInformation({ firstName, lastName, email, phone });
const result = await cart.checkout();
const appointmentId = result.appointments[0].id; // the deterministic key
await fetch('https://YOUR_WORKER_DOMAIN/track/booking', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ appointmentId, ...storedClickIds }),
keepalive: true,
});
Each step needs real UI: a service picker, optional staff choice, a calendar and time slots, a client details form, error states and a confirmation screen. This is the bulk of the work. The widget gave you all of it for free.
Questions to answer before you commit
- Do real services require payment or a card on file? If yes, the SDK flow must handle card tokenization, which adds scope and compliance work. Check this on a real service, not a free test service.
- Can attribution be stamped onto the booking itself? If a field you set at checkout is readable later through the Admin API, the server could read attribution directly. Test it: it could remove the extra request.
- Does a sandbox exist for your account? Build and test everything there first.
Rollout plan that protects revenue
The booking page is how the business makes money. A broken flow means lost appointments, so:
- Server first. Add the
/track/bookingendpoint and the exact-match lookup. It does nothing until the site calls it, so it's zero risk. - Build against the sandbox. Full flow, end to end, including the webhook match.
- Ship behind a flag on the live site (for example
?flow=sdk), with the widget still the default. Make real test bookings in production through the new flow. - Compare conversion rates. If the new flow books less than the widget, the attribution gain isn't worth it yet.
- Cut over, keeping the widget one deploy away as an instant rollback.
- Keep the fallback matching for bookings made outside your flow.
Honest assessment
- The server part is small and safe: a couple of hours.
- The booking flow is a real build: a polished multi-step UI on a revenue page, possibly with payment. Plan weeks, not a weekend.
- The payoff: attribution that stays correct at any traffic level, and full control over the booking experience.
- The lighter alternative: identity matching (capture email or phone before the widget opens). It gets most of the accuracy for far less work, and it's what we'd do first.
Testing checklist
- Sandbox booking: the checkout ID equals the webhook ID.
-
/track/bookingstores click IDs under the appointment ID. - The webhook finds an exact match and logs it as deterministic.
- Two simultaneous bookings from two devices each keep their own click IDs.
- Bookings made by phone still go through the fallback, labeled as such.
- The widget can be restored with one deploy.
Related reading: how to track Boulevard bookings in Meta, Google Ads & TikTok.

