Usage
Embedding the Booking Form
{{ craft.stub.bookingForm() }}
Setting Up Services
Navigate to Stub → Services to create bookable services with duration, pricing, buffer times, and capacity per slot. Each service can use its own currency — see Currencies.
Displaying Prices
Format a price with its currency’s symbol, decimal count and placement rather than assuming two decimals:
{{ craft.stub.formatPrice(service.price, service.currency) }}
Managing Providers
Add providers in Stub → Providers with weekly schedules, breaks, blocked dates, and service assignments. A provider can be linked to a Craft user, which is what makes filtering by user possible.
Taking a Booking Yourself
Not every booking arrives through the form. Stub → Bookings → New Booking enters one by hand — a phone call, a walk-in, a regular whose slot never changes.
The time picker is fed by the same availability engine the front-end form uses, so what you are offered is genuinely free, and the slot is checked again when you save — the list may be a minute old and a customer may have taken it in the meantime.
Overriding availability
Turn on Override availability and the picker is replaced by a plain time field with every check switched off: the slot list, the provider’s schedule, their breaks and blocked dates, capacity and buffer times, and the requirement that the provider is even assigned to the service.
That is the point of it. A walk-in at a time nobody is technically free, two people in the same slot, or an appointment last Tuesday you are only now writing down — all fine. Nothing warns you, because you asked.
What the form does and doesn’t send
Booking status and payment status are yours to set. No payment is taken here, so a booking you have already been paid for is one you mark Paid yourself.
Send confirmation email controls whether the customer gets the usual confirmation. The admin notification is never sent for these — you filled the form in, so you already know.
An existing customer is matched on email address and reused; anyone new is created. Creating
a booking needs the manageBookings permission, which is also what shows the
button.
Reminder Emails
Stub can email a customer a reminder before their appointment — 24 hours ahead by default. The same reminder, worded for the people running the appointment, goes to the provider at the address on their own record and to the admin address.
Both are ordinary Craft system messages, so their wording lives in Utilities → System Messages alongside the confirmation and cancellation copy, and is translatable the same way.
Reminders need a cron entry
Craft has no scheduler, so nothing sends reminders on its own. Stub ships a console command and your server decides when to run it. Hourly is plenty for a 24-hour lead time:
0 * * * * /path/to/craft stub/reminders/send
This is why both reminder switches start off. Turning them on without the cron entry would promise your customers an email that never arrives.
craft stub/reminders/send # send everything due
craft stub/reminders/send --dry-run # list what would go out, send nothing
craft stub/reminders/send --limit=50 # cap a single run
What gets reminded
- Only confirmed bookings. A pending booking hasn’t been paid for, and reminding someone about an appointment they don’t have yet is worse than saying nothing.
- Each booking once. Stub records when a reminder went out and never sends a second.
- Never after the fact. If the cron stops for three days, the catch-up run covers what is still ahead and stays quiet about appointments people have already been to.
The command is safe to run at any interval and safe to run twice at once, so a tight schedule or an overlapping run can’t produce a duplicate.
Sending one by hand
Every booking’s detail page shows when its reminder went out and offers a Send Reminder Now button. It is the quickest way to check your wording reads right before trusting it to a schedule — and the fallback on hosting where you can’t add a cron entry at all.
Filtering Services on the Front End
By default a template shows every enabled service. Both craft.stub.services() and
craft.stub.bookingForm() take an optional filter, so a page can show one
provider’s services instead — a per-author booking page, a category listing, or a
page built around a single service.
Listing a subset
{# Everything the logged-in user offers, via the provider they're linked to #}
{% for service in craft.stub.services({ user: currentUser }) %}
{{ service.name }} — {{ craft.stub.formatPrice(service.price, service.currency) }}
{% endfor %}
{# …or by provider handle, ID, or the Provider model itself #}
{% set services = craft.stub.services({ provider: 'jane' }) %}
{# …or a named handful of services #}
{% set services = craft.stub.services({ handles: ['consultation', 'follow-up'] }) %}
Scoping the booking form
The same keys work on the booking form. A form narrowed to a single service skips the “select a service” step; one narrowed to a single provider skips that step too and opens straight on the calendar. The progress bar and Back buttons follow along.
{# A page for one service #}
{{ craft.stub.bookingForm({ handle: 'consultation' }) }}
{# A provider's own booking page #}
{{ craft.stub.bookingForm({ user: currentUser }) }}
{# …but keep the provider step, even with one provider #}
{{ craft.stub.bookingForm({ user: currentUser, pinProvider: false }) }}
Filter keys
| Key | Takes |
|---|---|
id / ids | Service IDs |
handle / handles | Service handles |
provider / providers | A provider ID, handle, or Provider model |
user / users | A Craft User element or its ID, matched against the provider’s linked user |
includeDisabled | true to include disabled services and providers |
Every key takes one value or a list, and the keys combine — all conditions must hold. Two behaviours are deliberate:
- An unknown key throws rather than being quietly ignored. A typo’d
{ providor: … }that returned every service would be the worst way for this to fail. - A filter that resolves to nothing matches nothing.
{ user: currentUser }on a logged-out request returns an empty list, not the full one.
Single service and provider lookups
{% set service = craft.stub.service('consultation') %} {# by handle or ID #}
{% set provider = craft.stub.provider(currentUser) %} {# by user, handle, or ID #}
{{ provider.name }} offers {{ provider.serviceIds|length }} services.
Filtering is a display filter, not access control. The booking endpoints are anonymous and take a service ID from the request, so narrowing what a visitor is shown does not stop a crafted request from booking a service that was filtered out. Use it to shape pages, not to restrict who may book what.
Custom fields
Services are configuration rather than content, so they have no Craft field layout and cannot be filtered by a custom field. Provider — and the Craft user behind a provider — is how Stub models “whose service is this”. If you need an arbitrary taxonomy on top, keep a matching entry or category per service and filter Stub’s list by the handles it gives you.
Stripe Webhook
If payments are enabled, set your Stripe webhook endpoint to:
https://yoursite.com/actions/stub/webhook/handle
Permissions
Six permissions: viewBookings, manageBookings, deleteBookings, manageServices, manageProviders, manageCustomers.