doc-site
Architecture

MediSpa booking

How a treatment booking travels from the calendar through Sesami, Klaviyo and Zapier into the Shopify cart.

MediSpa booking

The booking calendar is the only React in this theme. It mounts into a Liquid placeholder, reads its whole configuration from a global, and coordinates three external services.

A customer opens the booking calendar. A Liquid config snippet supplies clinics, treatments and labels through window.theme. The React calendar reads availability from the Sesami SDK, adds a deposit or pay-in-clinic line to the Shopify cart, and collects contact details through a Klaviyo form whose submission posts a booking intent to a Zapier webhook.

Ownership

ConcernOwner
Clinic detailsShopify medispa_locations metaobject
Treatment prices, deposits, pay-in-clinic amountsShopify product and product metafields
Which treatments a page offersThe Services Overview section's blocks
Labels, deposit defaults, timezoneTheme settings
Real appointment availability and reservationsSesami
Contact capture formKlaviyo
What happens after submissionZapier
PaymentShopify checkout

Configuration handover

Liquid gathers everything the calendar needs and writes it to window.theme.medispaCalendarConfig. snippets/medispa-calendar-config.liquid does that work, pulling from metaobjects, product metafields, page metafields and theme settings in one pass.

React never queries Shopify for this. The calendar reads the global once at mount, through a Jotai atom seeded from window.theme.medispaCalendarConfig.

A page can hold more than one treatment list, so the config can change after mount. The calendar listens for a theme:medispaCalendarConfig event on document and re-reads the config when a Book Now button in a different Services Overview section dispatches it.

All prices in the config snippet arrive in dollars, from the render parameters, the landing page's offer_price metafield and the theme settings. Shopify's money filters expect cents, so the conversion happens exactly once at the formatting boundary. Keep that property if you touch the snippet, because a second conversion produces a price a hundred times too large and it is not obvious in review.

Mount sequence

  1. A MediSpa layout renders snippets/medispa-calendar-config.liquid and snippets/medispa-calendar-placeholder.liquid.
  2. src/entrypoints/medispa-calendar-react.tsx waits for window.SesamiSDK using the deferJS helper, which polls every 50 milliseconds.
  3. Once the SDK exists, React mounts CalendarRoot into .medispa-calendar--placeholder.
  4. src/entrypoints/medispa-booking-actions.ts binds every .js-action[data-action="open-calendar"] button, finds the nearest treatment list, and opens the calendar with that list.

The polling wait exists because Sesami's SDK arrives as its own script tag and the theme cannot control when. If it never arrives, the calendar never mounts and the placeholder stays empty.

Booking sequence

When a customer confirms a slot:

  1. The calendar revalidates the slot, because someone else may have taken it while the customer waited on the confirmation step.
  2. It calls sesami.reserve(slot) and receives a reservation token with an expiry.
  3. It looks up the treatment's Downpay selling plan by fetching the product's selling plan groups.
  4. It posts to /cart/add.js with the treatment variant and a set of line item properties.
  5. The drawer opens with the booking line.

The line item properties carry the whole booking:

PropertyContents
Date, Time, TimezoneThe chosen appointment
_reservation_tokenThe Sesami reservation, which is what actually holds the slot
_remaining-slotsRemaining availability at the time of booking
_sourcemedispa-calendar:handleReserveSlot
The Remaining Amount label from theme settingsBalance owing after the deposit

Shopify hides properties beginning with an underscore from the storefront and checkout, so customers never see the reservation token or the source marker. Both stay visible in /cart.js, the order in Shopify Admin and webhooks.

A cart line without _reservation_token is a treatment purchase with no appointment attached.

Lead capture

Separately from the cart flow, submitting the Klaviyo booking form posts a payload to the Zapier webhook URL in theme settings.

src/@types/calendar.ts lists every payload key explicitly instead of passing form fields through, so the Zapier receive node sees a stable shape. Adding a field to the Klaviyo form does not add it to the webhook. Both sides need changing together, along with the type.

Deposits

A customer can book a treatment two ways, and product metafields control which options appear.

Paying a deposit now uses a Downpay selling plan. At add-to-cart time the calendar reads the product's selling plan groups and looks for deposit_selling_plan. When that lookup fails, the calendar adds the line without a selling plan, and checkout charges the full price rather than the deposit.

Paying in clinic adds the line with no selling plan and shows the full amount as owing at the clinic. Its availability comes from custom.enable_pay_in_store on the product, which a page-level custom.pay_in_store metafield can override for everything on that page.

custom.enable_checkout controls whether the deposit option appears at all. It is absent on older products, and the calendar treats absent as enabled, so adding the metafield set to false is how you turn it off.

Trust boundaries

Availability is Sesami's. The calendar renders what the SDK returns and cannot verify it. An empty calendar is indistinguishable from a Sesami outage without checking Sesami directly.

The reservation is Sesami's. The theme holds a token and passes it to Shopify as a line item property. Nothing in this repository can extend, release or validate it.

Prices are Shopify's. The calendar's labels come from metafields and settings and are display only. Checkout recomputes from the product and selling plan.

Failure modes

SymptomLikely cause
Calendar never appears, placeholder stays emptywindow.SesamiSDK never loaded. Check for a blocked script
Calendar opens with the wrong treatmentsThe Book Now button sits in a different Services Overview section
Every clinic shows no slotsSesami outage, or the clinic has no availability configured there
Deposit charges the full priceThe Downpay selling plan lookup failed for that product
Bookings stop reaching downstream systemsZapier webhook URL changed on one side only
Prices a hundred times too largeA dollars value converted to cents twice

Diagnosis steps are in Booking calendar will not open.

Source map

ConcernFile
Configuration assemblysnippets/medispa-calendar-config.liquid
Inline configuration variantsnippets/medispa-calendar-config-inline.liquid
Mount pointsnippets/medispa-calendar-placeholder.liquid
React entry and SDK waitsrc/entrypoints/medispa-calendar-react.tsx
Booking sequence and cart addsrc/components/medispa-calendar/CalendarRoot.tsx
Confirmation step and CTA labelssrc/components/medispa-calendar/CalendarConfirm.tsx
Shared statesrc/components/medispa-calendar/calendar-atoms.ts
Sesami, Klaviyo and Zapier typessrc/@types/calendar.ts
Book Now bindingsrc/entrypoints/medispa-booking-actions.ts
Treatment list sectionsections/overview-services-grid.liquid
Balance owing recalculationsrc/entrypoints/mini-cart/internals/line-item-rules/remaining-amount-recalculation-rule.ts

Operator instructions are in MediSpa booking.

On this page