Concepts
How the booking engine thinks. Read this before writing an integration; it prevents most wrong assumptions about slots, capacity, money and time.
Contents
- The shape of the system
- Your account (tenant)
- Catalogue: services, staff, extras
- When is a time bookable?
- Appointments, bookings, orders
- Statuses
- Special booking kinds
- Money
- Notifications
- Time and locale
- Terminology: Dutch UI and English
The shape of the system
┌─────────────────────────── admin UI (/app, login)
account ─► catalogue ──┤
(key) services │
staff └──► availability engine ──► slots ──► order ──► payment
schedules │
holidays └──► notifications (mail)
▲
public API / widget ───────────┘ (no login, public key, server-side re-check)
- One account = one business. Your account is identified by its public key on the public side, and by the signed-in user's membership in the admin.
- The availability engine is deterministic: given service, staff, schedules, holidays, existing appointments and preferences, it returns the list of bookable slots. The server re-runs the same check when a booking comes in, so what was offered is what gets verified.
- The public API never trusts the client. Every booking is re-resolved and re-checked, then written inside one transaction that takes a per-staff lock. The widget is a convenience client, not a privileged one.
Your account (tenant)
An account is one business (the API calls it a tenant): name, slug, public key (the credential for the public API), time zone, locale, currency, VAT mode, deposit mode, gateways, booking rules (slot length, minimum notice, booking horizon, default status), calendar options and feature switches (cart, chain, recurrence, waiting list, combined-price method, occupancy display).
All data belongs to one account: a business can only ever see its own records, and every public call
is bound to exactly one account by its key. A person relates to an account with a role (owner,
admin, front_desk, staff); there is no account switcher — the first membership wins.
Catalogue: services, staff, extras
Category — grouping for the customer-facing list (Knippen, Verhuur, …). Optional.
Service — the thing being booked. Four types:
| Type | Meaning | Example |
|---|---|---|
simple |
one appointment, one staff member | haircut, intake, rental hour |
compound |
a fixed sequence of parts, one after another, with optional spare time between them | wash → cut → dry |
collaborative |
parts that must happen at the same time, each by a different staff member | skipper + boat; two instructors |
package |
a prepaid bundle of N appointments of one simple service |
5-ride card |
A simple service carries: duration, optional own slot length, price, deposit rule, VAT rate, minimum
and maximum persons (capacity), padding before/after, staff preference for "any", visibility
(public/private), one_booking_per_slot, which recurrence frequencies customers may choose, and an
optional waiting-list cap. Prices and capacity can be overridden per staff member
(staff_services), which is why a slot is always evaluated per (service, staff) pair.
Staff — anything with its own schedule that can be booked: a person, a boat, a room, a device.
Staff carries a weekly schedule (per weekday start/end plus breaks), holidays/vacation days, contact
details, visibility (public/private/archive), and the list of services performed. A staff record needs
no login; a login is linked via staff.user_id when the person should see their own agenda.
Extras — optional add-ons per simple service: price, extra duration (added after the appointment), minimum/maximum quantity. A minimum of 1 makes the extra mandatory. Extras belong to simple services; on compound/collaborative services every extra is attached to the part that owns it.
Schedule and holidays — a service's availability comes from the staff schedules. Company opening
hours are only a template for new staff rows; company holidays apply to everyone, staff holidays only
to that staff member. A "toch open" holiday row (kind exception) makes the company open on a yearly
closing day.
When is a time bookable?
A slot is a start time the engine may offer. For the chosen service and each candidate staff member:
- The start time lies on the slot grid: step =
service.slot_lengthor the account-wideslot_length(default 15 min). - The whole block fits in the schedule: duration + extras + padding before/after must be inside a working period and outside breaks, on that weekday.
- The date is not a holiday for the staff member or the company.
- The start is at least
min_prior_minutesfrom now and within the booking horizon (max_days, default 365). - It does not conflict, with padding, with an existing blocking appointment of that staff member (plus events they are linked to, plus holds while a customer is mid-checkout).
- Capacity allows it: the number of persons already booked (blocking statuses only) plus the requested persons must fit the staff member's capacity for that service.
A block that is partly occupied is offered as a partial slot when the remaining capacity suffices
(group slots; the UI shows the occupancy). A full slot is omitted, unless a waiting list is open —
then it is returned with state: "waitlist".
Two services can be booked into the same appointment (group join) only when the service, start,
duration and extras match and the capacity still allows it; one_booking_per_slot forbids any join.
When several staff members qualify and the customer sends staff: "any", the service's
staff preference decides: order (list order), least_expensive, most_expensive.
For compound/collaborative services the engine searches the whole sequence at once: parts run consecutively (compound, with spare time free for others) or in parallel on distinct staff members (collaborative). Only the first part's staff is customer-choosable.
Appointments, bookings, orders
order (one checkout) ← orders row, one token, one payment
├── appointment 09:30–10:15 (service A, staff X) ← a block of time in a calendar
│ ├── booking: Ada, 1 person, approved
│ └── booking: Bob, 2 persons, approved ← group join, same appointment
├── appointment 10:15–11:00 (service B, staff Y) ← chain part
└── series: appointments on 7 dates ← recurring occurrences
- Appointment = the occupied time block (staff + service + start/end + internal note).
- Booking = one customer on an appointment, with persons, status, notes and a magic token.
The public cancel/reschedule page is
/b/<booking token>; the token also appears in mails. - Order = everything a customer checks out in one go (cart items, chain parts, all occurrences of
a series, compound/collaborative parts). One order = one payment. The order token is returned as
orderTokenand mailed as the cancel-order link ({cancel_order_url}→/o/<token>). - Hold = an item a customer has chosen but not yet booked; the client sends it back with slot searches so the customer's own choices count as occupied. Holds are never real appointments.
- Series = recurring appointments created together, sharing a
seriesrow with the rule. The admin can edit one, this-and-following, or all occurrences. - Compound/collaborative parts share a
group_token; status changes apply to all parts.
Blocking statuses are pending and approved — those (and appointments with no bookings at all,
which block entirely) determine occupancy.
Statuses
| Status | Dutch label | Blocking | Meaning |
|---|---|---|---|
pending |
In afwachting | yes | booked, awaiting confirmation (when the business requires approval, or a payment is open) |
approved |
Bevestigd | yes | confirmed |
waitlisted |
Wachtlijst | no | wants a seat if one frees up; never paid, never blocking |
cancelled |
Geannuleerd | no | cancelled by customer or admin |
rejected |
Afgewezen | no | refused by admin, or payment failed/expired |
done |
Afgerond | no | the appointment happened (manual or via automatic status change) |
An account-level automatic status change can move appointments that have ended from one status to another (e.g. approved → done) automatically, checked every minute; bookings changed in the last two days get a notification.
Special booking kinds
- Group booking — a service with capacity > 1; several customers share one appointment. Occupancy
can be shown to customers (off /
3/8/nog 5). - Waiting list — when a slot is full and the service and account settings allow it, customers can take a place on the list. Optional mode "Wachtlijst telt mee als bezet": waitlisted persons count as occupying and once anyone is waitlisted, later bookers are waitlisted too. When seats free up, all waiting customers of that appointment get a first-come-first-served mail. No payment is involved.
- Recurring — a series of occurrences with a rule (daily/weekly/biweekly/monthly, interval,
weekdays, monthly day or nth-weekday, until/count, max 52). Occurrences whose preferred time is taken
are moved to a nearby available time (
anotherTime: true) or reported as "no time found". Payment mode: the whole series in one payment, or only the first occurrence (the rest is paid on site —onSitein the quote). - Package — prepaid N appointments. Bought online (
kind: "package") or created by the admin; the customer receives a personal/p/<token>link and schedules visits with the widget in package mode. Validity starts at the first planned visit; visits are never paid again. - Events — separate from appointments: dated happenings with ticket types, quantities, reserved seats and a sales deadline. Tickets get unique codes from a mask; codes are only shown/valid once the order is approved (paid). Every staff member linked to an event (organiser or support) shows the event in the calendar and has that time blocked for bookings.
- Extras — priced and timed add-ons, snapshot per booking (title, price, duration, quantity). Whether extras multiply by persons is an account setting.
- Chain — several services back-to-back for one customer, one after another, with each part possibly a different staff member.
- Cart — several independent appointments checked out together as one order.
Money
Price sources. The unit price of an item is the staff-specific price (staff_services.price) when
that staff member was assigned, otherwise the service price. Compound/collaborative services use their
own price, or the sum of the parts' staff prices when the business chose Optelsom van de onderdelen.
Order formula: per item unit price × persons, plus
extras (× persons when the account multiplies them), summed to a subtotal; deposit applied per item
('100%', or '30%', or a fixed amount per person); coupon discount and fixed deduction; optional
gateway surcharge (Mollie % + fixed); VAT. Prices include VAT by default (NL B2C): the shown total
already contains it and the quote returns tax as the part that is VAT. Rounding is half-away-from-zero
at 2 decimals.
What the customer pays now (payNow) is the deposit, or the full amount, depending on the account's
deposit mode (deposit_only, choose_deposit, choose_full) and the customer's choice. The rest is
due, payable on site. A zero order becomes a free order; a Mollie order whose payable amount is
zero falls back to a local one.
Payment types. local (pay at the desk — the admin completes it later), free (nothing to pay),
mollie (online; iDEAL/card on Mollie's page). One payment per order. Statuses: pending,
completed, rejected, refunded; late arrivals are flagged (late_paid) when the slot was already
given away.
Coupons — percentage and/or fixed amount, usage limit (account-wide), once-per-customer, validity dates (the booking date counts), minimum/maximum appointments per order, and optional restrictions to services/staff. A coupon use is given back when the payment is rejected.
Notifications
Mail is driven by templates per type: new_booking, status_changed, reminder, waitlisted,
waitlist_offer, package_new, event_tickets. Each template picks recipients (customer, the staff
member, admins, extra addresses), a status filter, a service filter and — for reminders — a timing
(relative: N hours before the start; or a fixed hour, N days before). Bodies use {codes} and
{#if …} blocks. Rendering happens when the mail is queued; sending retries with backoff. Order mails
are sent once per order (staff get one per own appointment); status changes and reminders stay per
booking. See 06-notifications.md.
Time and locale
- Instants are stored and transmitted as UTC. Everything customer-visible (slot times, day headings, "today", holidays, schedules, reminder timing) is computed in the business time zone.
- Recurrence and slot
from/untiluse business-local calendar dates (YYYY-MM-DD). - Formatting uses the account
locale(defaultnl) andcurrency(defaultEUR). - Bookings do not store a customer time zone; the whole product presents business-local time.
Terminology: Dutch UI and English
| Dutch (UI) | English (docs) |
|---|---|
| dienst | service |
| medewerker | staff (member) |
| categorie | category |
| afspraak | appointment |
| boeking | booking |
| bestelling | order |
| reeks | series |
| wachtlijst | waiting list |
| pakket | package |
| evenement / deelnemer | event / attendee |
| tickettype | ticket type |
| rooster | schedule |
| vrije dag | day off / holiday |
| openingstijden | opening hours |
| pauze | break |
| toeslag | surcharge |
| aanbetaling | deposit |
| kortingscode | coupon |
| opmerking | note (customer's) |
| interne notitie | internal note |
| extra('s) | extras |
| tijdstap | slot length / time step |
| bezetting | occupancy |