Notifications and e-mail
Every automatic mail is driven by a template that a business can edit in the admin (Meldingen). The service renders the mail when the event happens, puts it in an outbox, and sends it from there in the background with retries. Nothing is sent directly from the request that created the booking — mail failures can never fail a booking.
Contents
- Lifecycle
- Types
- Recipients
- Timing
- Template language
- Codes
- Attachments and reply-to
- One mail per order, one per booking
- Deferred mail for online payments
- Delivery, retries and the log
- Background processing
- Admin reference
- Sender and links
- Not implemented
Lifecycle
event (booking, status change, reminder window, waitlist offer, …)
│
├─ match active templates of that type (status filter, service filter)
├─ resolve recipients (customer / staff / admins / extra addresses)
├─ render subject + body per recipient ← template codes, business time zone, locale
├─ dedupe (a unique dedupe key per recipient + event) and add to the outbox
▼
outbox (status pending → sending → sent | failed)
▲
└─ every minute: send what is due → retry with backoff
Rendering happens at enqueue time, so the mail shows the data as it was at that moment.
Types
| Type | UI label | Fires when |
|---|---|---|
new_booking |
Nieuwe afspraak | a booking/order is created (immediately for local/free orders; after payment for Mollie orders; also on waitlist joins and package purchases the corresponding type is used instead) |
status_changed |
Statuswijziging | a booking's status changes (admin action, cancellation, refund-rejection, automatic status change) |
reminder |
Herinnering | the reminder window for an upcoming appointment opens |
waitlisted |
Wachtlijst | a customer joins a waiting list |
waitlist_offer |
Plek vrij (wachtlijst) | a seat frees up and the customer gets a claim link |
package_new |
Nieuw pakket | a package is bought or created (mail contains the personal planning link) |
event_tickets |
Tickets | an event order is placed (a "codes follow after payment" variant) and again once the order is approved with the actual codes |
New accounts get a default template set for every type on signup (Dutch), so mail works out of the box.
Recipients
Each template has four recipient switches:
| Switch | Who | Reply-To |
|---|---|---|
| Klant (customer) | the booking customer's e-mail | your Antwoordadres (reply_to), or the platform sender when empty |
| Medewerker (staff) | the staff member of the appointment (skipped when archived); for packages the package staff | the customer's e-mail |
| Beheerders (admins) | every member of the account with role owner or admin | the customer's e-mail |
| Andere adressen (custom) | the newline-separated addresses in the template | the customer's e-mail |
Sender: display name = your Naam afzender (or company name), address = the platform's
MAIL_FROM. At least one recipient must be enabled; custom recipients require at least one address.
Timing
Status filter (Bij status): send only when the booking has that status; Elke status matches
everything. Note that Elke status in a reminder never includes cancelled or rejected bookings.
Service filter: all services, or a ticked subset.
Reminder timing (only for reminder):
- Een aantal uur van tevoren — send N hours before the appointment start (default 24; 0 means at the appointment time). Evaluated with a tolerance window: your Verzendmarge herinneringen (uur) (default 2) widens the match, so a reminder is not skipped after a short processing delay.
- Op een vast uur — send at a fixed local hour (0–23, default 18) a number of days before the appointment date (0–30, default 1). "0 = op de dag van de afspraak zelf".
Reminders dedupe on a key that includes the appointment start time, so rescheduling re-arms the reminder while nothing is sent twice for the same booking.
Template language
Placeholders and conditionals in both subject and body:
{client_name},
Je afspraak voor {service_name} is bevestigd op {appointment_date} om {appointment_time}.
{#if extras}
Extra's: {extras}
{/if}
{#if number_of_persons > 1}
We reserveren {number_of_persons} plaatsen.
{/if}
Rules:
{code}— unknown or empty codes render as empty text.{#if code}…{/if}— block, may be nested. A newline directly after a block tag is swallowed.- Comparisons:
{#if code == value},!=,>,>=,<,<=,empty(empty/absent),!empty. Values are compared as strings/numbers; missing codes are honest about being empty. - No loops: there is no
{#each}, so multi-item orders use the pre-joined{appointment_schedule}/{extras}/{ticket_codes}codes instead. - HTML mode (the body of HTML mails): inserted values are HTML-escaped, newlines become
<br>, and blank lines become paragraphs. A bad placeholder can therefore never inject HTML into a mail. Subjects always use plain text mode.
Codes
{code} list as offered in the editor (Dutch labels in the admin):
Bedrijf — company_name, company_phone, company_address, company_website
Klant — client_name, client_email, client_phone
Afspraak — appointment_date ("dinsdag 14 oktober 2026"), appointment_time,
appointment_end_time, appointment_notes (the customer's remark), booking_number,
number_of_persons, extras ("Haarmasker (2×), Föhnen"), extras_total_price, order_count,
appointment_schedule (one line per appointment of the order), online_meeting_url and
online_meeting_join_url (empty unless the appointment has an online meeting), series_count,
status (Dutch label),
internal_note (only for staff/admin recipients), cancel_appointment_url (/b/<token>),
cancel_order_url (/o/<order token>),
cancellation_reason (currently always empty)
Dienst — service_name, service_info, service_price, service_duration ("1 uur 30 min"),
category_name, fields (the booking's answers to the service's Velden, one "Vraag: antwoord" line
per answer; a per-person answer gets one line per person. Empty without answers. A field that was
deleted later keeps showing its old answer, labelled with the field's id. A Bestand answer shows
the file name only: its download link is signed and short-lived, and mails are kept.)
Medewerker — staff_name, staff_email, staff_phone
Betaling — total_price (empty when free), amount_paid, amount_due,
payment_type (Ter plaatse / Online / Gratis), payment_status (Dutch), coupon, total_tax
Wachtlijst — waitlist_url (only waitlist_offer)
Pakket — package_url, package_name, package_size, package_remaining, package_expires
(only package_new)
Evenement — event_title, event_start_date, event_start_time, event_end_time, event_info,
attendees_count, ticket_codes (before payment: "Je codes krijg je na betaling."), order_total
(only event_tickets)
Which codes a template may use depends on its type: package and ticket templates have no appointment, so they only offer company/client codes plus their own group.
Attachments and reply-to
Agenda-bestand (.ics) meesturen attaches a standards-compliant calendar file (UTC times, escaped, folded at 75 octets) for booking-based mails. It follows the appointment's current time; rescheduling does not rewrite already-sent mails.
One mail per order, one per booking
A single checkout can contain a cart, chain parts, compound/collaborative parts and a whole recurring series. To keep this readable:
- Customer, admins and custom recipients get one
new_bookingmail per order, using the first appointment's codes plus{appointment_schedule}and{order_count}. - Staff get one mail per appointment of their own.
status_changedandreminderstay per booking — every cancelled occurrence gets its own mail.
Deferred mail for online payments
For Mollie orders nothing is mailed at order time: the customer should not receive "your appointment is confirmed" before paying. The confirmation mail is queued when the payment completes (webhook, return poll, or the expiry check noticing the payment went through). Local and free orders are mailed immediately. Ticket orders are the exception: they get a "codes follow after payment" mail right away, and the codes mail once approved.
Delivery, retries and the log
- Queued mail is sent within about a minute.
- Retry backoff after a failed send: 1 min, 5 min, 30 min, 2 h, 12 h. After 5 attempts the row becomes
failed(it stays visible in the log with the error text). - Mail stuck in Bezig (sending) for more than 10 minutes is retried automatically.
- Meldingen → Verzendlog shows every mail: recipient, subject, status (Verstuurd / In wachtrij / Bezig / Mislukt) and attempt count, filterable by status.
- Test versturen renders the saved template with sample data and sends it to the signed-in user — use it after every template change.
Background processing
Everything timed runs as one scheduled pass, once a minute: queue the reminders that are due, expire unpaid payments, apply the automatic status change, send the waiting-list offers, and deliver queued mail. The service runs this itself — nothing to configure — but it is worth knowing because it explains the timing you and your customers see:
- A reminder goes out at its chosen moment, with the tolerance window above absorbing a short delay.
- An online payment that is never completed expires after your Betaaltermijn; the slot then frees up. Until that moment it stays reserved.
- A waiting-list offer is mailed within a minute of a seat freeing up.
Admin reference
Meldingen (/app/notifications) — grouped by type with an Aan/Uit switch per template. The editor
fields: Naam, Soort, Actief, Ontvangers (Klant / Medewerker / Beheerders / Andere adressen), Bij status,
Diensten (alle or a selection), reminder timing, Onderwerp, Bericht, Agenda-bestand (.ics) meesturen,
and the code table. Existing templates also have Test versturen and Verwijderen.
Verzendlog (/app/notifications/log) — the outbox, newest first, 50 per page.
Relevant settings: Instellingen → Bedrijf en e-mail (Naam afzender, Antwoordadres, Verzendmarge herinneringen, Annuleren tot) and Instellingen → Algemeen (Tijdzone — all mail times use it).
Sender and links
The display name comes from Instellingen → Bedrijf en e-mail (Naam afzender), replies go to the
Antwoordadres, and the links in mails point at the pages this service serves (/b/…, /p/…,
/w/…, /o/…). Mail is sent from the platform's own sending domain; you do not configure it.
Not implemented
- SMS, WhatsApp and voice notifications (only e-mail exists).
cancellation_reasonis always empty (no reason is captured on cancellation).- No
{#each}loops, no attachments other than .ics, no birthday/last-appointment campaigns. - No per-recipient language: templates are sent in the language they are written in (Dutch by default).