Bookings for developers
This reference describes the booking model and the APIs, jobs and templates available to developers.
Before you begin
- An API key or a signed-in admin session, and the permission each route wants. See REST API.
api.accesson the account, which is Grav's own gate in front of everything on this store's API.- Shell access for the CLI, and the worker running for anything on the queue.
The model
| What it is | |
|---|---|
| Bookable | A person, room or item of equipment with reservable time. Stores its name, capacity, timezone, hours, time off, location and colour. People can also link to a Grav account for front desk access. |
| Service | Booking rules for a product of type booking: duration, intervals, buffers, capacity, notice, booking window, cutoff, approval and fallback hours. Product details, pricing, tax and purchase restrictions remain on the product. |
| Need | What a service occupies: this many units, from one of these bookables, with a flag for whether the customer picks which |
| Claim | One row per bookable a booking or a hold occupies, with the buffers frozen onto it |
| Hold | Somebody standing on a slot while they decide or pay. It is a claim like any other |
All resource types use the same model. Interchangeable resources can share a record; ten lanes can be one bookable with ten units.
A service rule without a variant applies to the product and its variants. A variant-specific rule replaces it for that variant, allowing options with different durations and prices.
Availability reads claims, so temporary holds and confirmed bookings reserve capacity in the same way.
A service without requirements uses a hidden bookable created on first use. It follows the same reservation process as other resources.
Availability calculation
- Load the service's rule and its needs, in order.
- Build candidate start times from the first requirement. If customers choose the resource, results are keyed by bookable for separate picker columns. Otherwise, merge the times for automatic assignment.
- Check every requirement for each candidate. An eligible resource must be open for the whole service and have enough units available for the duration plus setup and clean-up buffers.
- Automatically assign the first available bookable by position for any requirement the customer did not select.
- Attach the resource selections to the slot so the reservation uses the resources shown to the customer.
Start times are calculated from the beginning of each opening interval. A shop opening at nine offers the same nine, half-past-nine and ten o’clock slots regardless of the query range or the customer’s timezone.
Double-booking protection
Each reservation runs through one method in a transaction. It locks bookables in ascending ID order, removes expired holds, rechecks availability including buffers, then writes a claim for each selection. If any resource lacks capacity, the entire reservation is refused.
The transaction performs no external side effects: no email, job or event. Core may retry the outer transaction when the database is busy, so side effects must run after it succeeds.
A concurrency test starts six real processes at the same time. With one unit, exactly one reservation succeeds; with two units, exactly two succeed.
API endpoints
The API has 75 endpoints: 54 admin, 12 public and 9 front desk. Of these, 52 are also MCP tools. See Run the store from an AI assistant.
Every route has an entry in the REST API reference, generated from the plugin's own manifest, with its permission and its parameters:
- Bookings: the diary, one booking, the seven actions on one, the live holds and the CSV export.
- Bookings overview and settings: the overview calendar, Troubleshoot, the settings, and the scripts behind screens other plugins add.
- Bookable people and places: bookables, working hours, time off, locations and the accounts a person can be tied to.
- Booking services: the booking rule on a product or an option, and what it needs.
- Booking calendar sync: calendar and channel links, their
.icsfiles, and testing an outside calendar. - Classes and waiting lists: classes, their dates and registers, and the queue behind each date.
- Storefront booking routes: what a booking page and a customer's own booking link call.
- Front desk routes: what the front desk app calls.
Admin routes
Admin reads require kahunacart.bookings.view; writes require kahunacart.bookings.manage. Both also require api.access. The CSV export and add-on screen script return files and are not MCP tools. The booking list tool provides the records used by the CSV export.
Service rules use PUT, not POST, because each product or variant has a single rule. Repeating a write updates that rule.
Outside calendar URLs are never returned in full. Bookable responses include only the label, host and final six characters.
Public routes
The twelve routes under /kahunacart/bookings/public do not require a session. A browser-generated visitor ID identifies its holds, while an emailed token grants access to a specific booking. Two routes serve calendar feeds, authenticated by a path token.
availability checks the requested seat count. A date with two places left is full for a party of three. If payment is required, book returns 409 with add_to_cart: true, directing the client to the cart flow.
While the store is coming soon or paused (KahunaCart 1.5.0's store status, Bookings 1.1.8 or later), book takes no new booking and no place on a waiting list, and holds holds no time. Both answer what the storefront JSON API answers a purchase: 503 with Retry-After: 3600, code: store_closed, store_status saying which, and the store's message as detail, which the picker shows. Reading a booking, cancelling it and moving it through its manage link still work, and so does everything in the admin and on the front desk. A signed-in admin with KahunaCart permissions can still book from the storefront, the same way they can still buy.
The public routes are not MCP tools. Assistants should use the corresponding admin tools, which enforce account permissions and record actions in the audit log.
Booking tokens contain 32 hexadecimal characters, enforced by the route pattern. A booking ID in place of a token returns 404. Writes accept the store’s origin and any origins configured under Other sites that may book.
Front desk routes
Nine, under /kahunacart/bookings/desk, opened by kahunacart.bookings.desk or kahunacart.bookings.desk.own plus api.access.
Front desk routes return a limited set of fields. Tests reject responses containing order IDs, order item IDs, totals, providers, refunds or coupons.
Every desk.own route restricts reads to the account’s resource on the server. Diary requests for another column return the account’s own column. Requests for another resource’s appointment return 404, matching a nonexistent booking.
desk/me returns all permitted columns and locations for filtering. Its day_units data (codes, unit, services, bookables) controls rental views and eligible resource rows. desk/slots returns the booking mode’s offer, including dates, a readable quote and the amount to collect for a nightly stay. It returns no order.
Session-authenticated writes require a desk nonce from desk/me, sent in X-Desk-Nonce. A missing nonce produces a reload message. API-key requests do not require this nonce.
Front desk routes are not MCP tools. Use the admin tools for automated booking management.
Cart line data
Core’s line_data stores six booking keys in the cart line snapshot. These are copied to the order line at order creation.
| Key | Holds | Example |
|---|---|---|
booking_hold |
The hold standing on the slot | 41 |
booking_start |
When it starts, with the offset on it | 2026-09-24T14:00:00-06:00 |
booking_tz |
The timezone the time is being read in | America/Denver |
booking_with |
The bookable the customer picked, empty for "anyone" | ann |
booking_label |
What the customer sees on the line | Thu 25 Sep, 2:00pm with Ann |
booking_party |
How many people are coming, where the service asks | 3 |
For classes, quantity is the number of places purchased. Appointments retain quantity one, with the number of people in booking_party.
The booking label is saved when the slot is chosen and reused in the cart, receipt and email. Later resource-name changes do not alter it.
Cart line identity combines the variant and line data, keeping appointments at different times on separate lines.
The jobs
Seven job types run through the core worker:
| Type | What it does |
|---|---|
bookings.tick |
Looks for what is due and queues the three below |
bookings.send_message |
One message about one booking |
bookings.send_reminder |
One reminder, at one lead time |
bookings.send_followup |
One thank-you |
bookings.prune |
Throws away bookings older than the retention setting |
bookings.sweep_holds |
Lets go of the holds whose time has run out |
bookings.promote_waitlist |
Takes back the offers that ran out, then offers or gives every place that has come free |
Jobs can be retried safely. Messages are claimed before sending, deleting an expired hold is idempotent, and assigned places are not offered again.
The last two jobs run during the sweep so they detect places released through any booking change.
Shortcodes and Twig
Three shortcodes, and three Twig functions beside them.
[kahunacart-booking product="full-groom" /]
[kahunacart-booking product="full-groom" with="ann" variant="24" /]
[kahunacart-booking-manage /]
[kahunacart-sessions days="60" category="classes" /]
{{ kahunacart_booking('full-groom') }}
{{ kahunacart_booking('full-groom', { with: 'ann' }) }}
{{ kahunacart_booking_manage() }}
{{ kahunacart_sessions(60) }}
{{ kahunacart_sessions_data(60) }}
product accepts the product slug. with selects a bookable; variant selects an option and hides the option selector. Use kahunacart_sessions_data() to retrieve session data for custom markup.
KahunaCart routes already load the booking assets. On other pages, the shortcode loads its script and stylesheet once per request. Pages without a booking shortcode load neither.
See Shortcodes and Twig functions for the rest of the store's.
Template overrides
Override any of the four partials in templates/partials/kahunacart-bookings/ by placing a file of the same name in user/themes/<your-theme>/templates/partials/kahunacart-bookings/:
| File | Where it is drawn |
|---|---|
order-panel.html.twig |
The appointment on the receipt page |
email-panel.html.twig |
The same block in the order email |
overview-card.html.twig |
The next appointment on the account overview |
account-tab.html.twig |
The whole Bookings tab |
The guest's page is a whole template rather than a partial: templates/kahunacart-booking-manage.html.twig, copied into user/themes/<your-theme>/templates/.
Every customer message is a pair of templates under templates/emails/, replaced the same way from user/themes/<your-theme>/templates/emails/:
| Message | Templates |
|---|---|
| Booking request received | booking-received.html.twig, booking-received.txt.twig |
| Booking confirmed | booking-confirmed.* |
| Add this to your calendar | booking-calendar.* |
| Booking cancelled | booking-cancelled.* |
| Booking moved | booking-rescheduled.* |
| Booking declined | booking-rejected.* |
| Reminder | booking-reminder.* |
| Thank-you | booking-followup.* |
| Added to a waiting list | booking-waitlisted.* |
| A place is being held for you | booking-seat-offered.* |
| You have a place | booking-promoted.* |
Shared appointment details live in templates/emails/partials/. Override this partial to change them across all messages. HTML emails inherit the core layout’s colours, logo and footer.
The picker uses one JavaScript file with no build step. Themes can replace it through Grav’s asset override. All strings come from the language file via the mount point, so translations require no JavaScript changes.
Plugin-served pages
The plugin creates /desk and /booking dynamically in Grav’s page tree. Neither needs a file in user/pages.
/desk |
/booking |
|
|---|---|---|
| Who opens it | Staff, signed in with the Login plugin | A customer, from a link in an email |
| Setting | desk.route, and desk.enabled |
manage_route; empty switches it off |
| Extends | Nothing | Your theme's base template |
| A real page at that route | Falls through to ordinary routing | Wins, and the shortcode on it still works |
The desk uses a standalone layout independent of the storefront theme. The booking management page uses the theme so it matches the shop.
Neither is indexed and neither is cached.
The CLI
bin/plugin kahunacart-bookings slots <product> [--variant N] [--with <bookable>] [--date YYYY-MM-DD] [--days N] [--explain]
bin/plugin kahunacart-bookings seed <recipe> [--timezone <zone>]
slots lists times for a product and date. Add --explain to include blocked times and their reasons. It uses the storefront availability engine and prints times in UTC.
seed creates example products, resources, schedules, service rules and requirements.
| Recipe | What it writes |
|---|---|
salon |
Three staff schedules, cut and colour services, two colour options, approval requests and an evening workshop. |
groomer |
Two groomers, one tub, and a groom that needs both, with a longer rule on the large dog |
trainer |
One trainer, two lengths of one-to-one, and an eight-week bootcamp sold as a course |
massage |
Two therapists, two rooms, and two treatments — the long one asking for a deposit |
range |
Ten lanes as one bookable with ten units, hire on the hour paid at the counter, and a Saturday safety course |
diary |
A fortnight of bookings across whatever of the five are there, in every status |
all |
The five businesses, then the diary |
Recipes do not share records. They find existing records by slug and leave them unchanged, so repeated runs are safe. They use the store’s timezone unless --timezone is provided.
Each recipe matches its getting-started guide, allowing you to create the example and then inspect its setup.
The tables
The first migration creates twelve tables. Five migrations are applied through bin/plugin kahunacart migrate.
reset --orders deletes bookings, holds, claims, history and email-send records. It retains resources, schedules and services.
Tables include a store ID and use UTC epoch timestamps. Migrations are idempotent. The plugin uses the core database connection and does not store payment amounts.
Package contents
The package includes its runtime Composer dependency for recurrence rules and the built front desk bundle. Production installation requires neither composer install nor Node.
If the recurrence library is missing, recurring services report an error and the dashboard shows an alert. Reinstall the add-on to restore missing package files.
Related
- REST API - how every route here is authenticated, and the conventions they all follow
- Provider developer guide - add-on migrations, the reset registry, product types and the hook registry this plugin hangs off
- Admin sections - the rules an add-on's own admin screen follows
- Run the store from an AI assistant - use the admin routes exposed as MCP tools
- Shortcodes and Twig functions