Technical

Building a custom booking flow with Boulevard's book SDK for deterministic attribution (planning guide)

The only way to tie every booking to its ad click with no guessing: own the booking flow. What we proved, what it costs, and how to roll it out safely.

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

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:

  1. checkout() returned an appointment ID.
  2. Boulevard then sent APPOINTMENT_CREATED for that appointment.
  3. 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

  1. 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.
  2. 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.
  3. 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:

  1. Server first. Add the /track/booking endpoint and the exact-match lookup. It does nothing until the site calls it, so it's zero risk.
  2. Build against the sandbox. Full flow, end to end, including the webhook match.
  3. 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.
  4. Compare conversion rates. If the new flow books less than the widget, the attribution gain isn't worth it yet.
  5. Cut over, keeping the widget one deploy away as an instant rollback.
  6. 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/booking stores 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.

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

Because the widget runs in an iframe your page can't see into, so a booking can't be tied to the visitor's ad click with certainty. With your own flow, checkout returns the appointment ID in the same page that holds the click IDs.

A full booking through the SDK against production returned an appointment ID at checkout that was byte-identical to the ID delivered by Boulevard's APPOINTMENT_CREATED webhook.

In our proof of concept, the client API used the application ID alone as a publishable key, with no secret. Keep the Admin API secret on the server only.

Rebuilding the booking experience on a page that makes the business money. A bug means lost bookings. Use the sandbox, a feature flag and an instant rollback to the widget.

Yes: identity matching. Capture email or phone before the widget opens and match on a hash of it when the webhook arrives. It's mostly deterministic with far less work.

Free ad audit

Need this set up
for your salon?