Hosted booking page
A self-service web page customers can use without ever opening WhatsApp.
URL shape
https://[agency-subdomain]/book/[agency-id]/[client-id]/[service-id]ID-based — slugs aren’t supported (they break on rename + can collide). Operator shares the URL by clicking “Share link” on the Service Catalog row; the link uses the document’s current origin so it inherits the white-label domain.
What the customer sees
- Service card — name, duration, price, optional description, white-label primary colour
- Day strip — 14-day scroller. Tap a day, slot grid filters to that day.
- Slot grid — open slots at the service’s step interval
- Contact form — name (required), phone with E.164 normalisation (required), email (optional)
- Confirm button — POSTs to
/api/public/bookings - Confirmation screen — green check, booking summary, “Add to my calendar” (ICS download), “Cancel this booking” link (signed JWT)
Anti-abuse
- IP rate limit — 10 requests/min across every endpoint
- (Agency + phone) rate limit — 3 bookings/hour
- Server-side slot re-validation — POST checks the slot is still open right before creating (handles the “two people clicked at once” race with a clean 409)
- Phone normalisation — digits-only canonical form server-side
Confirmation extras
- ICS download — VCALENDAR file the customer can drop into Google Calendar /
Outlook / Apple Calendar. UID is
booking-[id]@[host]so calendar apps dedupe repeat clicks. - Signed cancel link —
/book/cancel?t=[jwt]. JWT expires at slot start + 60s. Opening hits a public endpoint that callsBookingService.cancel(idempotent — cancelling an already-cancelled booking returns success).
White-label theming
Primary colour from the agency’s brand settings stamps onto a CSS variable
(--booking-primary) that day pills, slot pills, and the submit button all consume.
The agency’s logo + name appear in the header. Footer shows support phone + email.
Last updated on