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
- Type selector: choose a booking type, unless a specific
bookingTypeSlugis provided. - 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.
- Calendar and times: choose an available time and reserve it while completing contact details.
- Guest info: name, email, phone, and notes. Every booking requires a phone number.
- 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.