Bookings
A booking workspace over bookingsV2 — the six sections, where availability comes from, and the time-zone rule the schema does not carry.
bookingsV2(startTime, endTime) takes a window and nothing else — no cursor, no filter, no
sort, no search, no count. Every section of this module is built on that: it asks for exactly
the days it draws, filters and sorts in the browser, and says how much it is holding rather
than pretending to page.
Six sections over one booking list: a calendar you drag appointments around, an appointments table, staff with weekly hours and Google Calendar sync, the services catalog, the AI booking settings and the bot time zone, and insights over the loaded window.
Installed as
| Wizard id | bookings |
| Flag | --modules bookings |
| Selection | Default — --yes installs it |
| Requires | — |
| Recommends | knowledge-base, contacts |
| Skill | chatfuel-bookings |
Routes and views
The section is a path segment; everything else is a query parameter. calendar is
DEFAULT_VIEW, so it has no segment of its own.
| Path | What it shows |
|---|---|
/bookings | Calendar — day, week or month, columns by day or by specialist, drag to move, drag an edge to resize, drag empty grid to create. |
/bookings/appointments | Upcoming, past or a custom range as a table, with bulk status changes and CSV export. |
/bookings/staff | Specialists: weekly working hours, the services they offer, and their Google Calendar connection. |
/bookings/services | The catalog — duration, price, availability, images. |
/bookings/settings | Notification channel, confirmation, the 24 h / 2 h notices, language, the bot time zone. AI autonomy is read here, not set. |
/bookings/insights | Status mix, no-show and cancel rates, attended revenue per currency, utilisation per specialist, busiest weekdays and hours. |
The parameters the module owns:
| Parameter | Value |
|---|---|
b | The open booking's id. A drawer below 1280px, an inline second column above it. |
specialist, service, status | Comma lists. Shared by the calendar, the appointments list and insights. |
mode | day, week (the default) or month. |
date | Anchor day, YYYY-MM-DD. Absent means today. |
by | time (the default) or specialist — what a column is. |
color | specialist (the default) or status. |
range | upcoming (the default), past or custom. |
from, to | The custom range, and the custom insights period. |
q | Search over the loaded rows. |
sort | <column>:asc or :desc. |
period | week, month, 30d (the default), 90d or custom. |
s | A specialist id, or new, on the Staff section. |
density | comfortable (the default) or compact. |
new | 1 opens the booking wizard, prefilled by start, end, contact, ns (specialist) and nsvc (service). |
?new=1&contact=<id> is the deep link a "Book" button in Live Chat uses: the wizard opens on
the customer step with that contact chosen. A ?week= from an older link is still read, as
mode=week&date=, and never written back. Defaults are omitted from what is written, and an
unrecognised value anywhere renders the default rather than throwing.
The model
An appointment is a BookingBase in bot.bookingsV2, and queries and mutations answer with
that interface while subscriptions answer with the concrete type — so a selection always goes
through ... on Booking / ... on BookingWithGoogleCalendarRef. Deleted services and
specialists still arrive, as DeletedGoodsService and DeletedSpecialist, keeping enough of
themselves to render a past appointment.
Six statuses, in the order the whole module uses: Pending, Confirmed, Attended, No-show,
Reschedule, Canceled. Everything but Canceled occupies its slot. Only five of them are
targets. A booking is born Pending and can never return to it: → Pending answers
InternalServerError from every state, including Pending → Pending, so the app never offers
it.
Availability is a separate call, not a field on the calendar.
GoodsService.bookingAvailableStartTime(botID, date) answers one service × one day, per
specialist, with start-time ranges whose end is inclusive — a 30-minute service on a
09:00–18:00 day answers 09:00–17:30. A start s is bookable when start ≤ s ≤ end; slicing
it with the usual s + duration ≤ end silently drops the last slot of every period. Existing
bookings are already subtracted, Canceled and deleted ones free the slot, and the module
offers a start every 15 minutes inside what comes back.
Which leaves the fact that decides every timestamp you send:
A Time with a zero offset is read as the bot's wall clock, not as UTC.
2026-08-21T13:00:00Z was stored and echoed as 13:00:00-06:00 on a bot in
America/Mexico_City. A non-zero offset is honoured as an instant. So toISOString() puts
a booking at the wrong hour and nothing errors — the record round-trips, and only a
comparison against the specialist's schedule shows it. Format every instant with the bot
zone's real offset (lib/zone.ts, toZoneIso), range bounds included. Schedules and
availability are HH:mm in that same zone, which is why slots stay in the bot's zone even
when you view in your own.
Specialists are bot.specialists, unpaginated. A schedule is seven named days with one
optional break each, HH:mm in the bot zone, and availability is computed from that schedule
— not from the knowledge base's business hours, which are a different, unrelated shape.
specialistCreate and specialistUpdate replace profile, schedule and services together.
Configuration
| Variable | Notes |
|---|---|
CHATFUEL_TOKEN | Secret. Server-side only. |
VITE_CHATFUEL_WORKSPACE_ID | The wizard's workspace step writes it. |
CHATFUEL_API_BASE | Defaults to https://panel.chatfuel.com. |
Nothing here is specific to this module. Environment variables has what each one is and which side reads it.
Permissions
| Object | Action | Required for |
|---|---|---|
| People | View | Reading the calendar and appointments |
| People | Edit | Creating, moving, resolving and deleting bookings |
| Ai | Edit | Managing staff and services, the AI booking settings and the bot time zone |
Limits
There is no server-side filter, search, sort, count or pagination over bookings. Filters,
search and sort all run over the loaded window, which is why the header says 12 of 40 and
the search box says "Search loaded rows…". To see more, the list asks for a bigger window:
Upcoming grows 90 days forward, Past 30 days backward, and every window is capped at 366 days.
There is no aggregation in the booking API. Every insights figure is a fold over the
loaded rows and every card prints its coverage. Four things are absent from the data rather
than unbuilt: lead time (a booking carries no createdAt), trends across more than one
window, anything server-aggregated, and revenue as one number across currencies.
bookingUpdateV2 is a full replace, and there is no patch. There is no status on the
inputs and no dedicated cancel or reschedule mutation either — Canceled and Reschedule are
status values, moved through bookingStatusResolveV2, and a reschedule is an update of the
times. Delete has no counterpart: there is no restore mutation, so delete asks first and undo
does not cover it.
The API validates nothing on the calendar. It accepted overlapping bookings for one specialist, bookings outside working hours, bookings in the past, a 24-hour booking, and a booking with neither service nor specialist. Only the wizard, going through availability, is opinionated — it warns, and the panel does not stop you.
Availability needs a service, and answers one day at a time. There is no availability
without a service and none for a range of days. A specialist with no working hours answers
hasSchedule: false, always empty — their existing bookings still show on the calendar and an
operator can still book them by hand. A schedule allows exactly one break per day, because the
input has one.
Only WhatsApp contacts can be booked — anything else is
BookingContactPlatformNotAllowed. On a WhatsApp-connected bot the API turns inline-contact
input into a real WhatsappContact, so the same phone booked twice yields one real contact
rather than an inline one.
Two things this module cannot finish. AI autonomy is read on the Settings page and
changed in the automations module — the legacy setter is deprecated and answers
BotMigratedToNewFuelySettings on a migrated bot. And Google's sign-in happens on Chatfuel's
own page: this app creates the connection link, shows who created it, starts the sync and
follows the task, but the OAuth round trip needs credentials it does not have.
The only reminders are bot-wide. The 2-hour and 24-hour appointment notices in Settings
are the whole set — there is no per-booking reminder and no recurring booking in the API.
calendarLandingURL, the hosted booking page, is read-only and an experiment: null outside
it.
The module's own ceilings:
| Offered start times, every | 15 minutes |
| Calendar snap, and a keyboard nudge | 15 minutes |
| Shortest booking a drag can make | 15 minutes |
| Booking duration in the panel | 5 minutes to 24 hours |
| Upcoming window grows by | 90 days |
| Past window grows by | 30 days |
| Any window | 366 days |
| Undo window | 60 seconds, one entry, never into Pending |
| Default service duration | 30 minutes |
| Service title / description | 120 / 1000 characters |
| Images on a service | 10 |
| Specialist first and last name | 60 characters each |
| About a specialist | 1000 characters |
Bookings, as a product
The calendar, the staff hours that decide what can be booked, and how far the AI books on its own.
Knowledge base
Recommended alongside this one — it shows the same services and specialists from the AI's side.
Contacts
Recommended alongside this one — the customer on a booking is a contact.
Your agent's notes
The wizard installs .claude/skills/chatfuel-bookings/ (Codex: .agents/skills/chatfuel-bookings/) — the time-zone rule, availability semantics, the statuses and the traps verified live.