docs
    site-kit: Booking
    v7.2.0.md

    Sync (Booking) — @sonordev/site-kit/sync

    Embeddable booking/scheduling widget — like Calendly, built into Sonor. Appointments, consultations, classes.

    Usage

    'use client'
    import { BookingWidget } from '@sonordev/site-kit/sync'
    
    export default function BookPage() {
      return (
        <BookingWidget
          onBookingComplete={(result) => {
            console.log('Booked:', result.booking.confirmationCode)
          }}
        />
      )
    }

    Props

    interface BookingWidgetProps {
      orgSlug?: string                    // Organization slug (for public mode)
      apiKey?: string                     // API key (auto-resolved from env)
      apiUrl?: string                     // Default: https://api.sonor.io
      bookingTypeSlug?: string            // Show specific type only
      timezone?: string                   // Guest timezone (auto-detected)
      className?: string
      daysToShow?: number                 // Days of availability to display
      onBookingComplete?: (result: BookingResult) => void
      onError?: (error: Error) => void
      hideTypeSelector?: boolean          // Hide type selector for single-type embed
    
      styles?: {                          // Custom theme
        primaryColor?: string
        borderRadius?: string
        fontFamily?: string
        backgroundColor?: string
        textPrimary?: string
        borderColor?: string
      }
    }

    UI Flow

    1. Type selector: choose a booking type, unless a specific bookingTypeSlug is provided.
    2. Meeting format: when the booking type enables meeting options, choose virtual, office, or a visit to the guest's location. Travel meetings validate the address before showing availability.
    3. Calendar and times: choose an available time and reserve it while completing contact details.
    4. Guest info: name, email, phone, and notes. Every booking requires a phone number.
    5. Result: a confirmed booking shows calendar links when Sonor sends the guest's invitation. When the host's Google Calendar sends it, the widget says who the invite is coming from instead (see Calendar invitations). Meetings that require review show a pending request without calendar links.

    Changing a meeting format restores the selected format, address, and access notes. Guests can return to the service selector when it's available. Contact details survive changes to the format or reserved time.

    Expired meeting details and time reservations show a recovery action. Guests can recheck their meeting details or choose another time without re-entering their contact information. An active hold remains usable if its prepared meeting token expires, until the hold's own deadline.

    Navigation is disabled while a reservation or booking request is running. Stale responses cannot reopen a previous step, and holds created after the widget is removed are released.

    Accessibility

    Screen readers and AI agent browsers (which act on elements by their accessible names) can book without guessing:

    • Days are buttons named with the full date, year included: "Wednesday, October 7, 2026". The picked day is pressed (aria-pressed="true"), and days that can't be booked are disabled.
    • Times are buttons named with the time and the date, "10:30 AM, Wednesday, October 7, 2026", in the booking's time zone. They still show just the time. The picked time is pressed.
    • Confirm, the button that reserves the picked time, is named "Confirm 10:30 AM, Wednesday, October 7, 2026".

    Every name contains what its button shows (a time's and Confirm's start with it), so voice control can still target a button by its visible text (WCAG 2.5.3). None of this changes what the widget sends.

    API Functions

    import {
      fetchBookingTypes, fetchBookingTypeDetails,
      fetchAvailability, fetchAvailableDates,
      createSlotHold, releaseSlotHold, createBooking,
      prepareMeeting,
      detectTimezone, formatTime, formatDate, formatDuration,
    } from '@sonordev/site-kit/sync'

    Types

    interface BookingType {
      slug: string; name: string; description?: string;
      duration_minutes: number; color?: string;
      location_type: 'virtual' | 'phone' | 'in_person' | 'custom';
      price_cents?: number; currency?: string; is_active: boolean;
    }
    
    interface BookingResult {
      success: boolean
      booking: {
        id: string; confirmationCode: string; scheduledAt: string;
        durationMinutes: number; hostName?: string; timezone: string;
      }
      cancelUrl: string; rescheduleUrl: string;
      invitation?: 'google' | 'sonor'   // who sends the guest's calendar invite
      calendarLinks: { google: string; outlook: string; ics: string }
    }

    Calendar invitations

    Each booking gives the guest one calendar invitation, and invitation says who sends it:

    • 'google': the host has Google Calendar connected, so Google invites the guest to the host's event. Sonor's emails don't attach a second invite, and the success screen says who the invite is coming from instead of showing calendar links. The Google and Outlook links build a separate event with no tie to that invitation, so a guest who clicked one would have two entries for the same meeting.
    • 'sonor': there's no Google event for the booking, usually because the host hasn't connected a calendar. Sonor's confirmation email carries the invite, and the success screen shows the calendar links.

    APIs older than the field don't send it; treat a missing invitation like 'sonor'. calendarLinks is still sent for every booking so older widgets keep working. If you build your own success screen with createBooking, check invitation before you render calendarLinks.

    Features

    • Automatic timezone detection
    • Slot hold with expiry (prevents double-booking)
    • Business hours support
    • Multiple hosts per booking type
    • Custom meeting URL / location

    Booking regression checks

    Run pnpm exec vitest run src/sync/booking-flow.test.ts for expiry and server-error recovery checks. Start pnpm exec vite --host 127.0.0.1 --port 5189, then run node test/meeting-flow/run.mjs and node test/meeting-flow/regression.mjs for the real widget flows. The browser harness intercepts requests to a reserved test domain, so it creates no live bookings.