List bookable appointment slots
The public availability grid. Reads through the service-role client inside this route rather than through an anon RLS policy, so there is one security model to reason about rather than two.
Slot allocation is first-come: for a given start time the first provider found free wins it. There is no fairness or least-booked balancing. Slots whose start time has already passed are filtered out, because the booking POST rejects them anyway.
Ranged mode (days). Answers up to 14 consecutive days from date in one request, for the wizard's landing walk, which used to fire one request per day (up to 14) hunting for the first open one. The single-date shape ({slots}) is unchanged and stays the default; sending days greater than 1 switches the response to {days: {"<date>": {slots}, ...}}, one entry per requested date.
Path Parameters
The organization's public booking slug, i.e. the {slug} in /{slug}. Only orgs with status active resolve; anything else is a 404.
Query Parameters
One active service id of this org, or up to six comma-separated for a multi-service visit (0054). The slot maths runs on the SUM of the durations, and a specific provider must offer EVERY id; with any, eligibility is the intersection; one person carries the whole block. Any id unknown, inactive, or not this org's makes the whole request a 404; more than six is a 400.
YYYY-MM-DD. Interpreted as a UTC wall clock, the same convention computeAvailableSlots uses. The first day answered in ranged mode.
dateOmit, or send any, to search every provider offering the service. A specific id that is not an active provider of this org offering this service returns an EMPTY slot list, not a 404; returning 404 would let a caller probe which provider ids exist.
How many consecutive days from date to answer in one call. Omitted or 1 keeps the original single-date {slots} shape; 2 to 14 switches the response to {days: {"<date>": {slots}, ...}}.
1 <= value <= 14Present only when the guest manage page calls this route to pick a new time for an existing booking. Governs which notice policy gates the slots returned, since a reschedule cannot reuse the ordinary minimum-notice rule unchanged.
uuidResponse Body
application/json
application/json
application/json
application/json
curl -X GET "https://example.com/api/book/string/availability?service_id=string&date=2019-08-24"{ "slots": [ { "startsAt": "2019-08-24T14:15:22Z", "providerId": "4834bcdc-4a64-444d-966b-1a6fe381da24" } ]}Hospitality's "Your booking funnel" (self-hosted, cookieless) GET
The hospitality twin of getWebsiteAnalytics above, scoped to the one panel that's actually about the booking widget rather than a marketing site: no popular pages, referrers, devices or domain/publish status, since hospitality has no site pages for any of that to describe yet (see organization/website/page.tsx's own redirect). Same Umami plumbing (ensureUmamiWebsiteId, getUmamiEventCounts) and the same buildBookingFunnelSteps ranking/drop-off maths as the appointments funnel, over hospitality's own step vocabulary (RESERVATION_FUNNEL_STEP_ORDER, funnel-events.ts). The terminal "Booked" row is the reservation_completed Umami event count, not a DB ground-truth count the way appointments' via_website column gives it, since hospitality has no site to distinguish "arrived via the site" from "direct" (there is nothing that column would be truthful about). Surfaced on /reports, the one screen both verticals already share, not a hospitality-only "Website" nav item that does not exist.
Book an appointment (guest checkout) POST
Public guest checkout. The requested slot is re-derived server-side, not in the past, inside the provider's working hours, not inside a blocked period; because the wizard only offers valid slots but nothing stops a direct POST inventing one. Overlap is enforced by the database, not here. The client record is upserted on `(company_id, email)`, so a returning customer accumulates history on one row. `notes` goes on the appointment, never on the client record. **Engine-boundary note.** This route checks only that the org is `active`; it does NOT check `business_type`. Its hospitality counterparts go through `hospitalityCompany()`, which refuses an appointments org outright. So the public surface enforces the engine boundary in one direction only: a hospitality org that has any active `book_services` row is publicly bookable here. See the "Two engines" section.