A booking type is a reservation_definition. A booking is a reservation made against it. Times are never stored: the engine works out the open slots each time someone asks, from the booking type's days, hours, services, breaks and the bookings already made.
Related jobs: chairs, rooms or tables are Bookable resources for a booking type; a booking form on your own page is AppMint JS engine on a page; an assistant that books for callers or chatters is AI assistant for chat or phone.
Ask first
- What is booked: each service's name, length in minutes, price, and any gap needed after it (cleanup, notes).
- When: the days, the opening and closing time, the time zone, and any daily break (lunch).
- How many at once: how many bookings one time can take.
- With whom: the staff who deliver it, and whether a booking goes to one named person, rotates, or stays unassigned for the team.
- Confirm automatically, or does staff confirm each new booking?
- Who else should hear of each booking: extra email addresses and phone numbers.
- Reminders: the defaults (below), or their own times.
- Where people book: the site's booking page, a page of their own, or only staff and the AI.
Steps
1. Reservations: the booking type
Look first: find_records on reservation_definition. Then create_record (datatype: "reservation_definition") or update_record_partial (keys data.<field>). In the Studio: Reservations › Reservation Definitions (/reservations?tab=reservation-definitions). Its form asks the main questions; services, blocked times and reminders are under All settings.
| Field | Value | Notes |
|---|---|---|
name | e.g. consultation | Unique key: letters, digits, -, _. |
title, description, image | shown to the person booking | |
type | service (appointments) | service_point for chairs/rooms/tables, see the resources skill. |
status | active or inactive | inactive offers no times and refuses bookings. |
services | [{ name, duration, price, breakAfter, description, images }] | duration and breakAfter in minutes. Leave the field out (not []) for a single unnamed service; then the booking type's own duration is the length (default 30). |
workDays | ["Monday","Tuesday",…] | Full English names, capitalised. "monday" closes that day. |
officeHours | { timezone: "America/Chicago", startTime: "09:00", endTime: "17:00" } | One set of hours for every open day. The last slot must end by endTime. Without it: UTC, 00:00 to 23:59. |
blockedTime | [{ startTime: "12:00", endTime: "13:00" }] | A daily break, in the booking type's time zone, on every open day. Not a date. |
spots | number, default 1 | Bookings one time can take. |
hosts | staff emails | Who delivers it. |
bookingType | unset, round-robin, unassigned | Unset with one host: every booking is that host's, and the host can't be double-booked. round-robin: the next host after the last booking's who is free and not on leave. unassigned: no host. |
autoConfirm | true / false | A visitor's booking starts new; with autoConfirm it starts confirmed. A booking staff make is always confirmed. |
maxPartySize | number or empty | A bigger party is refused (party_too_large). |
checkInBy | minutes | How long a late booking is held, for check-in. |
emails, phones | extra addresses / numbers | Copied on every booking notice and reminder. |
notifications | see step 3 | The reminders. |
meetingLink, venue, meetingInfo | Put in the confirmation. | |
businessLocationId | a business location's sk | Bookings belong to that location. |
paymentRequired | leave off | See the warning in step 4. |
form | a form's name | Extra questions on the booking page. |
Everything on a booking type is public: GET /crm/reservations/definitions and the slots call return the whole record (hosts, emails, phones, reminder texts) to anyone. Put nothing private in it.
Hands on: the booking type's sk. Check: step 2, for the next open day.
2. Reservations: the times it offers
call_appengine POST /crm/reservations/slots (no sign-in needed) with:
{ "reservationDefinitionId": "<sk>", "serviceName": "Consult", "serviceDate": "2026-10-26" }reservationDefinitionIdmust be the sk; the name answers400 invalid id.serviceNamemust match a service's name exactly, same case ("consult"answers400 Service type "consult" not found.). Left out, it is the first service.serviceDateis a calendar day in the booking type's time zone.date,appointmentDate,startDate,daywork too. A past day answers400 "Invalid Service Date, date must be in the future.". Today is allowed; slots already started are left out.host: only that host's free times.
It answers { reservationDefinition, service, slots: [{ startTime, endTime, spotsAvailable, businessTimezone }] }. Times are UTC; show them in businessTimezone. A closed day answers { slots: [], closed: true, error: "We are closed on this day. Please pick Monday, …" }. Full slots are left out.
How the slots are made (reservations.service.ts 661–736, 781–809, 1055–1086, 1028–1052):
- The first slot starts at opening time.
- The next starts
duration+breakAfter+gracePeriodminutes later. (A booking type withslotGenerationset to anything butdurationsteps by the request'sslotIncrementMinutesinstead, default 5.) - A slot must end by closing time.
- A slot that overlaps a
blockedTimeis dropped; the next slot still starts on the step. - A slot is full when
spotslive bookings overlap it, counting each booking'sbreakAfter. Cancelled bookings don't count.
Tested on 2026-10-20: Monday–Friday, 09:00–12:00 Chicago, break 10:30–11:00, spots: 2.
| Service | Slots offered |
|---|---|
Consult, 30 min + 15 breakAfter | 9:00, 9:45, 11:15 (10:30 falls in the break) |
| Quick, 15 min | every 15 minutes from 9:00 to 10:15, then from 11:00 (10:30 and 10:45 fall in the break) |
| Any, on Saturday | none, closed: true |
For several days at once: POST /crm/reservations/availability with { reservationDefinitionId, serviceName, from: "YYYY-MM-DD", days: 7 } (up to 31). It answers nextAvailable and each day with closed, openSlots and its slots, full ones included (spotsAvailable: 0).
Hands on: the open times for one day, in the business's time zone.
Check: they match what the person expects. If a day is empty, check workDays spelling, officeHours, blockedTime, status, and whether it is already past.
3. Reservations: what gets sent
Every booking sends real email and SMS (reservations.service.ts 1563–1632, 238–400, 1635–1840, 1919–2088):
| When | Template (email + SMS) | To |
|---|---|---|
| Booking made | notificationTemplate, else reservation-confirmation, with a calendar invite | the customer, emails, the booking's host (or every host if none), and a copy to the business (setting notificationCopyTo.reservation.sendCopyTo, default the business email). SMS: the customer's phone and phones. |
| Reminders | notifications, else the defaults below | the same people |
| Changed by staff | notificationTemplate, else reservation-update | |
| Rescheduled | reservation-rescheduled (skipped with notify: false) | |
| Cancelled | cancellationTemplate, else reservation-cancelled | the same people; queued reminders are removed |
| Deleted | reservation-delete | the customer |
The default reminders, when notifications is empty: email and SMS 1 day before, the morning of (unit: "morning_of", 08:00), 1 hour before, and at the start. Each entry of your own is { timeBefore, unit: "minutes" | "hours" | "days", type: "email" | "sms" | "whatsapp" | "email,sms", subject, html, text }. A reminder whose time has already passed when the booking is made is skipped.
A reminder with timeBefore: 0 is sent the moment the booking is made, not at its time (reservations.service.ts 309–311). The defaults have two of those (the morning-of and the at-start ones), so a new booking with the defaults gets its confirmation plus two more emails and two more texts at once. Set your own notifications with timeBefore of 1 or more for every entry. The morning-of time is 08:00 on the engine's clock, not the business's.
To word these messages, see Email and SMS templates with merge fields.
Stop. Save notifications only after the person has seen the list of what each booking will send, to whom, and when.
Hands on: the reminder plan, written out.
Check: read the booking type back; notifications is what was agreed.
4. Site Config: the booking page
The site's booking page is the Reservations feature: /book while it is on, 404 while off. To switch it on or move it to a page of their own, see Site page routing. Confirmation emails link to it.
What the page does (base-app components/booking/BookingRoute.tsx, modules/reservation-system):
- Lists every active booking type, except one named
meeting(the internal one)./book?definition=<sk>opens one straight on its calendar; use that link for a "Book now" button. - Steps: booking type (if several), service (if several), with whom (if several), date and time, the visitor's details. It books through
POST /crm/reservations/book: no account needed, a name and an email or phone required. The visitor becomes a customer in CRM. One email or phone can make 3 bookings an hour from the site; the next is refused with 429 "Too many bookings for this contact. Try again later, or contact us directly." (reason: too_many_bookings). Staff bookings aren't counted (reservations.service.tsthrottleSiteBooking). /book?managelets a visitor find a booking by email and confirmation code, then move or cancel it. The code is required:GET /crm/reservations/by-email/<email>/<code>. Without it, only the signed-in customer whose email or phone it is, or staff, get a list; anyone else gets 400 "Enter your reservation number to find your booking, or sign in." (tested through a site's/api).
Leave paymentRequired off. With it on and a service priced above 0, the page goes to a payment step whose payment is never started: it shows "Preparing payment…" for ever, and no booking is made (reservation-payment.tsx 55–80). Prices are shown, never charged. There are no deposits.
Stop. Switching the feature on publishes the page, and every booking from it sends the messages in step 3. Wait for a yes.
Hands on: the booking page's address, and a ?definition=<sk> link.
Check: open the page. The booking type, its services and the times from step 2 show. Don't finish a booking (step 5).
5. Check without booking
Don't make a test booking with POST /crm/reservations/create, POST /crm/reservations/book, create_record on reservation, or by finishing the booking page: each one emails and texts the customer, the hosts and the business. The checks in steps 2 and 4 are enough. If a booking must be proven, ask first, and use a @test.local email and no phone.
create_record on reservation goes through the same booking service as create (slot check, customer, notices). A plain repository write of a reservation is refused (reservation should be created using CRMService api).
Hands on: nothing new.
6. Reservations: change, move or cancel a booking (when asked)
| Job | Call | Notes |
|---|---|---|
| Find bookings | find_records on reservation, or Reservations (/reservations, /reservations?openId=<sk>) | data.name is the 8-letter confirmation code. |
| Move | POST /crm/reservations/reschedule/<sk> with { startTime, service?, reason?, notify? } | Refused with 409 slot_unavailable and problems[] (past, closed, outside_hours, blocked, full, host_busy, host_on_leave). Staff may add override: true to place it anyway. Keeps a rescheduleHistory. |
| Cancel | POST /crm/reservations/cancel/<sk> with { reason } | Status cancelled, stamped with who and why, reminders removed, notice sent. The booking is kept. |
| Change status or details | POST /crm/reservations/update with the record | Sends reservation-update. Status can't be set to used (that is check-in) or rescheduled (use Move). |
Stop. Each of these tells the customer by email and text (a move can skip it with notify: false). Say so, and wait for a yes.
Don't cancel or move a booking with update_record_partial. It writes the record directly: no slot check, no notice, and the reminders for the old time are still sent.
Hands on: the booking's code and its new state.
Check: read it back; for a cancel, status is cancelled and cancelledAt is set.
When it's done
Tell the person:
- the booking type's name, services (length, price, gap), days, hours and time zone, daily breaks, and how many per slot;
- the open times you checked, for a real day, in their time zone;
- who is told of each booking, and the reminder plan (and if they kept the defaults, that two reminders go out at once);
- the booking page's address and the
?definition=<sk>link, or that it is still off; - that prices are shown but not charged.
Open the booking type: /reservations?openDefinition=<sk>.
What it can't do yet
- No payment or deposit on the booking page (step 4).
- No minimum notice or booking window. Any open slot from the next minute onwards can be booked, months ahead.
- One set of hours for all days. Different hours on Saturday need a second booking type.
- No gap before a service, only after (
breakAfter). No one-off closed dates:blockedTimerepeats every day. Staff leave is honoured for hosts. - A booking made through the API is not checked against open days or hours.
bookandcreatecheck the service, breaks and capacity, but notworkDays,officeHoursor a past time. The booking page only offers real slots; a direct call can book outside them. - Reminders with
timeBefore: 0go out at booking time, and morning-of uses the engine's clock (step 3). - Slot service names are exact-case; booking accepts any case. Use the names exactly as saved.
Don'ts
- Don't write
workDaysin lower case or short form. - Don't put a date in
blockedTime; it is a daily time. - Don't turn on
paymentRequired. - Don't make a test booking, from any API or the page, without a yes. It sends real email and SMS.
- Don't cancel or move bookings with
update_record_partial. - Don't put private notes, phone numbers or staff details on the booking type you don't want public.
- Don't switch the booking page on without a yes.