Notifications API
Customer emails are built from templates and sent by automation rules. A template holds the subject and body for one email type in one language. A rule says when to send which template, for example "24 hours before the booking starts".
All endpoints need a staff token with the admin:write permission and the x-tenant-id header. For the admin UI, see Emails and automations.
Templates
| Method | Path | Description |
|---|---|---|
GET | /api/v1/admin/messaging/templates | List templates. |
POST | /api/v1/admin/messaging/templates | Create a template. |
GET | /api/v1/admin/messaging/templates/{id} | Get a template. |
PUT | /api/v1/admin/messaging/templates/{id} | Update a template. |
DELETE | /api/v1/admin/messaging/templates/{id} | Delete a template. Returns 204. |
POST | /api/v1/admin/messaging/templates/{id}/clone | Copy a template, for example to another language. |
GET | /api/v1/admin/messaging/templates/key/{templateKey} | The tenant-wide or default template for a key and language. |
GET | /api/v1/admin/messaging/templates/types | The email types you can write templates for. |
GET | /api/v1/admin/messaging/templates/variables/{templateKey} | The variables available for an email type. |
GET | /api/v1/admin/messaging/templates/variables/{templateKey}/categorized | The same variables, grouped by category. |
POST | /api/v1/admin/messaging/templates/preview | Render a subject and body with sample data. |
List templates
Query parameters, all optional: templateKey, languageCode, templateType (email or sms), siteId, and includeGlobal (default true, includes the built-in defaults).
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"tenantId": "t_demo",
"siteId": null,
"templateKey": "booking_confirmed",
"languageCode": "en",
"displayName": "Booking confirmation",
"subjectTemplate": "Your booking {{bookingReference}} is confirmed",
"bodyTemplate": "<p>Hi {{customerName}}, ...</p>",
"templateType": "email",
"isGlobal": false,
"createdAt": "2026-09-01T10:30:00Z",
"updatedAt": "2026-09-01T10:30:00Z"
}
]
Built-in defaults have isGlobal: true and tenantId: null. You cannot change them; create your own template with the same key and language instead.
Which template is used
GET /api/v1/admin/messaging/templates/key/{templateKey} shows the tenant-wide template for a key and languageCode (default en). It falls back to the built-in default, and then to English. It does not consider site-specific templates.
Create a template
{
"templateKey": "booking_confirmed",
"languageCode": "fi",
"subjectTemplate": "Varausvahvistus {{bookingReference}}",
"bodyTemplate": "<p>Hei {{customerName}}, varauksesi on vahvistettu.</p>",
"templateType": "email",
"siteId": "s_demo"
}
| Field | Required | Description |
|---|---|---|
templateKey | Yes | The email type. See GET .../templates/types. |
subjectTemplate | Yes | Subject line. |
bodyTemplate | Yes | HTML body. |
plainTextTemplate | No | Plain-text body. |
languageCode | No | Default en. |
templateType | No | email (default) or sms. |
siteId | No | Use only for this site. Leave it out for all your sites. |
displayName | No | Name shown in the admin UI. |
description | No | Note shown in the admin UI. |
A second template with the same key and language returns 409. A template with unbalanced blocks returns 400.
PUT accepts subjectTemplate, bodyTemplate, plainTextTemplate, displayName and description. clone takes an optional languageCode and displayName.
Email types
GET .../templates/types returns key, name, description and triggerEvent for each type. The keys are:
| Key | Sent when |
|---|---|
booking_confirmed | A booking is confirmed. |
booking_reminder | A set time before the booking starts. |
booking_canceled | A booking is cancelled. |
booking_rescheduled | A booking's date or time changes. |
status_changed | A booking's status changes. |
booking_no_show_followup | A customer is marked as a no-show. |
course_start | Around a course's first session. |
course_session | Around each course session. |
course_end | Around a course's last session. |
Template syntax
Templates use a Handlebars-style syntax. Use the variables endpoint to see exactly which variables an email type has.
| Syntax | Result |
|---|---|
{{customerName}} | The value, HTML-escaped. |
{{{bodyHtml}}} | The value, not escaped. |
{{#if siteAddress}}...{{else}}...{{/if}} | Show a block when the value is set. |
{{#unless isPaid}}...{{/unless}} | Show a block when the value is not set. |
{{#each items}}{{this}}{{/each}} | Repeat a block for each item. {{@index}} is the position. |
{{formatDate bookingDateTime}} | The date, in the site's time zone and the email's language. |
{{formatTime bookingDateTime}} | The time, 24-hour clock. |
{{formatCurrency totalAmount}} | The amount with the currency symbol. |
{{uppercase name}}, {{lowercase name}} | Change the case. |
Preview
POST /api/v1/admin/messaging/templates/preview
{
"subjectTemplate": "Your booking {{bookingReference}}",
"bodyTemplate": "<p>Hi {{customerName}}</p>",
"sampleData": {
"customerName": "Alex Example",
"bookingReference": "BK-2026-001"
}
}
Returns { "subject": "...", "body": "...", "plainText": "..." }.
Automation rules
| Method | Path | Description |
|---|---|---|
GET | /api/v1/admin/messaging/automation-rules | List rules. |
POST | /api/v1/admin/messaging/automation-rules | Create a rule. |
GET | /api/v1/admin/messaging/automation-rules/{id} | Get a rule. |
PUT | /api/v1/admin/messaging/automation-rules/{id} | Update a rule. Send only the fields you change. |
DELETE | /api/v1/admin/messaging/automation-rules/{id} | Delete a rule. Returns 204. |
The list takes optional triggerEvent, isActive and templateKey filters.
Create a rule
{
"name": "24-hour reminder",
"triggerEvent": "booking_date",
"triggerOffset": -24,
"triggerOffsetUnit": "hours",
"templateKey": "booking_reminder",
"productIds": [],
"productTags": [],
"siteIds": [],
"isActive": true
}
| Field | Required | Description |
|---|---|---|
name | Yes | Rule name. |
triggerEvent | Yes | What the rule reacts to. See below. |
triggerOffset | Yes | Time from the event. Negative is before, positive is after, 0 is at once. |
templateKey | Yes | The email type to send. |
triggerOffsetUnit | No | hours or days. |
triggerSessionNumber | No | For course_session only: the session to target, starting at 1. Leave it out for every session. |
productIds | No | Only these products. Empty means all. |
productTags | No | Only products with any of these tags. Empty means all. |
siteIds | No | Only these sites. Empty means all. |
destinationType | No | email (default) or webhook. |
integrationId | No | The webhook integration, when destinationType is webhook. |
description | No | Note for staff. |
isActive | No | Whether the rule runs. |
triggerEvent | Offset is measured from |
|---|---|
booking_created | When the booking is created. |
booking_confirmed | When the booking is confirmed. |
booking_completed | When the booking is marked complete. |
booking_date | The booking's start time. |
course_start | The course's first session. |
course_session | Each course session. |
course_end | The course's last session. |
Responses include isSystem (a default rule created for every account) and isProtected. A protected rule sends a required confirmation, cancellation or reschedule email; you can edit it but not deactivate it (409).
Errors
| Status | When |
|---|---|
400 | Invalid body or template syntax. |
404 | Template, rule or email type not found. |
409 | Duplicate template for a key and language, or deactivating a protected rule. |
See Authentication for tokens.