Slotly Manual, as shipped in the download
Slotly — User Manual
Appointment booking you own outright. Version 3.1.4 [src: app/index.php:7].
About this manual
Every statement here was written by reading Slotly's own source, and each one carries the file it came from in a bracketed src marker. Paths are relative to the folder holding this docs directory. Where the application and this manual disagree, the application is right — the markers exist so you can check.
No feature is described that the code does not provide. Where something is off until you enable it, or is deliberately capped, that is stated rather than glossed.
What Slotly is
Slotly is a booking system with two faces: a public page where your customers book themselves in, and an admin area where you run the diary [src: app/controllers/public.php:140].
It is a single-business installation — one diary, one team, one database [src: app/src/Auth.php:15]. It runs on PHP with either SQLite or MySQL and needs no Composer, no Node and no build step [src: app/install/index.php].
Installing
Point a web server at the app directory and follow the installer [src: app/install/index.php]. Schema creation and later upgrades are handled by the application itself [src: app/src/Database.php:147], so an upgrade is a matter of replacing files.
Signing in
The admin area is at /admin/login [src: app/controllers/admin.php]. Repeated failures are rate-limited [src: app/src/LoginRate.php].
Two-step verification is available per user and uses standard time-based codes, so any authenticator application works [src: app/src/Totp.php]. Enrolling shows recovery codes once; only their hashes are kept, so they cannot be shown again [src: app/src/Totp.php].
Single sign-on through OpenID Connect is supported, starting at /admin/login/sso and returning to /admin/login/sso/callback [src: app/controllers/v3.php].
Roles and permissions
Slotly has three roles — viewer, staff and admin [src: app/src/Auth.php:15] — and, unlike a simple two-tier system, it gates individual actions through a permission map rather than by role name alone [src: app/controllers/api.php:27].
The roles are described in the source in the terms a salon or clinic actually uses: a viewer is read-only, a member of staff runs the desk, and an admin is the owner [src: app/controllers/api.php:20].
What each role may do
This table was produced by running Slotly's own permission function against its own map, not by reading role names:
| Action | viewer | staff | admin |
|---|---|---|---|
| View appointments, calendar, reports | yes | yes | yes |
| View the audit log | yes | yes | yes |
| Create an appointment | — | yes | yes |
| Edit an appointment | — | yes | yes |
| Record a payment | — | yes | yes |
| Edit a customer | — | yes | yes |
| Delete a customer | — | — | yes |
| Change settings | — | — | yes |
| Manage the team | — | — | yes |
[src: app/controllers/api.php:27]
Two consequences are worth knowing before you hand out a role.
A viewer can read the audit log. The viewer grant is *.view, which matches any permission ending in .view — and that includes audit.view [src: app/controllers/api.php:30]. If you intend a viewer to be an accountant or a partner who sees the diary but not the history of who changed what, that is not what this role does.
Settings are the largest admin-only surface. Of the permissions the code actually enforces, settings.edit is required in seventeen places, team.manage in four, and customer.delete and audit.view in two each [src: app/controllers/api.php:39].
A refusal renders inside the admin chrome rather than as a bare page, deliberately, because a blank 403 reads as a broken link [src: app/controllers/api.php:44].
Services and categories
Services are the things customers book, and each carries at least a name and a duration [src: app/src/Schedule.php:52]. They can be grouped into categories [src: app/controllers/admin.php]. Creating, editing and deleting services and categories are all admin-only [src: app/controllers/admin.php].
The team and who can be booked
Each team member is a user with a role, an optional personal time zone, a colour for the calendar, and a flag for whether they may be booked at all [src: app/src/Database.php:147]. A member who is not bookable still has an account but does not appear as an option to customers.
Which staff can perform a given service is resolved per service [src: app/src/Schedule.php:35].
How an available slot is worked out
This is the heart of the product and it is worth understanding, because four settings shape every slot a customer sees [src: app/src/Schedule.php:52]:
| Setting | Default | Effect |
|---|---|---|
| Slot step | 15 minutes | the granularity of offered start times [src: app/src/Database.php:147] |
| Lead time | 120 minutes | how far ahead of now the earliest bookable slot sits [src: app/src/Database.php:147] |
| Maximum advance | 60 days | how far into the future booking is allowed [src: app/src/Database.php:147] |
| Business time zone | UTC | the zone the diary is reckoned in [src: app/src/Database.php:147] |
Appointments are stored in UTC and presented in the business's zone [src: app/src/Schedule.php:29], which is what keeps a diary correct across a daylight-saving change.
Before a booking is accepted the chosen slot is validated again on the server, against the service, the member of staff, the party size and the current time [src: app/src/Schedule.php:166]. That second check is what prevents two customers who loaded the page at the same moment from taking the same slot.
Taking a booking
The public page
Your customers start at / [src: app/controllers/public.php]. The page is driven by three small endpoints it calls as the customer chooses: /api/staff for who can do the job, /api/slots for when, and /api/quote for what it will cost [src: app/controllers/public.php]. The booking itself is posted to /api/book [src: app/controllers/public.php:138].
A successful booking returns the customer's own booking link [src: app/controllers/public.php:138].
Approval mode
By default a booking is taken as final. Turn on require approval and bookings arrive needing your confirmation instead [src: app/src/Database.php:147].
Statuses
An appointment is pending, confirmed, cancelled, completed or no_show [src: app/controllers/public.php:160].
The customer's own booking page
Every appointment has a short code and its own cancellation token, and the two do different jobs [src: app/controllers/public.php:138].
- The code alone shows the booking.
/b/{code}renders the appointment read-only [src: app/controllers/public.php:142]. - The code plus the token allows managing it. Whether the cancel and reschedule controls appear is decided by comparing the supplied token in constant time [src: app/controllers/public.php:145].
Acting on the booking requires the token without exception: cancelling and rescheduling both refuse with HTTP 403 if it is absent or wrong [src: app/controllers/public.php:155]. So a customer who forwards their confirmation e-mail shares the ability to cancel; a link with the code alone does not.
Cancelling and rescheduling
Both can be switched off entirely, independently of each other [src: app/src/Database.php:148].
When cancellation is allowed, three things are checked before it goes through [src: app/controllers/public.php:159]:
- Online cancellation must be enabled.
- The appointment must not already be cancelled, completed, or marked as a no-show.
- It must be further away than the cancellation cutoff, which defaults to 24 hours [src: app/src/Database.php:149].
A customer who is inside the cutoff is told to contact you rather than being silently refused [src: app/controllers/public.php:163].
A cancellation releases the slot back into availability and notifies [src: app/controllers/public.php:165]. Rescheduling moves the appointment, re-validating the new time exactly as a fresh booking would be [src: app/src/Schedule.php:293].
Prices, tax and coupons
A quote is calculated from the service, the party size and any coupon code [src: app/src/Pricing.php:10]. Coupons are validated against the amount, so a minimum-spend coupon cannot be applied to a smaller booking [src: app/src/Pricing.php:52]. Coupons are managed by an admin [src: app/controllers/admin.php].
Tax is off by default. When enabled you set its name and its rate, and the rate is held in basis points — so 20% is stored as 2000, which avoids rounding drift [src: app/src/Database.php:147].
Currency code and symbol are yours to set and default to USD and $ [src: app/src/Database.php:147].
Notifications and reminders
Slotly sends on four occasions: when a booking is confirmed, cancelled or rescheduled, and as a reminder before the appointment [src: app/src/Notify.php:23].
E-mail is off until you enable it and supply your own SMTP details — host, port, user and encryption, defaulting to port 587 and tls [src: app/src/Database.php:147]. The sender name and address are separate settings [src: app/src/Database.php:147].
Reminders need a scheduled task. They are sent by cron/reminders.php, intended to run hourly [src: app/cron/reminders.php:3]. The reminder lead time defaults to 24 hours [src: app/src/Database.php:147]. Without that scheduled task the other three notifications still work and reminders simply never go out.
Customers
Customers are records with a name and e-mail, created as bookings come in [src: app/controllers/public.php]. Staff may edit them; only an admin may delete one [src: app/controllers/api.php:30].
Existing customers can be brought in from a CSV file at /admin/customers/import [src: app/index.php:155]. Columns are matched by header name rather than position, ignoring case, spaces and underscores and accepting common aliases [src: app/src/CsvImport.php].
Reports
The reports page is admin-only [src: app/controllers/admin.php]. A day-level summary is also available to an AI assistant as a tool — see below [src: app/controllers/mcp.php].
Settings reference
Settings are admin-only and are saved from one form [src: app/controllers/admin.php:580]. The defaults below come from the schema that creates the table [src: app/src/Database.php:147].
| Setting | Default |
|---|---|
| Business name | My Business |
| Business e-mail, phone, address | empty |
| Time zone | UTC |
| Currency, symbol | USD, $ |
| Time format | 12 hour |
| Week starts | Sunday (0) |
| Slot step | 15 minutes |
| Lead time | 120 minutes |
| Maximum advance | 60 days |
| Require approval | off |
| Allow cancellation, allow rescheduling | both on |
| Cancellation cutoff | 24 hours |
| Tax | off; name Tax, rate 0 basis points |
| Accent colour | #0f766e |
| Reminder lead time | 24 hours |
| SMTP | off; port 587, encryption tls |
Free-text fields for the booking page — an introduction, a confirmation note and your terms — are set here as well [src: app/controllers/admin.php:580].
Backup and restore
An admin can export the data, and restoring is a two-step upload-then-confirm so you see what you are about to overwrite [src: app/src/BackupRestore.php]. Every booking also downloads as a spreadsheet at /admin/appointments.csv (administrators only, because it carries customer contact details): reference, start and end in your time zone and in UTC, service, staff, customer, party size, status and the money.
Unattended backups are fetched by your scheduler from /backup/scheduled?t=… [src: app/controllers/v3.php]. Two details matter: the token is compared against a stored hash, and a bad or missing token returns 404 rather than 401 — deliberate, since it makes the endpoint indistinguishable from a wrong URL to anyone probing [src: app/controllers/v3.php].
Backups are written to data/backups, created if absent [src: app/controllers/v3.php].
The calendar feed
Appointments can be subscribed to at /feed/appointments.ics?t=… [src: app/controllers/v3.php]. When you create the feed URL under Calendar feeds you choose whose appointments it carries: the whole team's, or one staff member's own diary — so a stylist who subscribes sees their own bookings and nobody else's.
Slotly's feed tokens are better than a single shared secret: they live in their own table, several can exist, and any one of them can be revoked individually without disturbing the others [src: app/controllers/v3.php]. An unknown or revoked token returns 404 [src: app/controllers/v3.php].
The REST API
Six read-and-write endpoints sit under /api/v1/: appointments as a list and singly, customers, services and slots [src: app/controllers/api.php]. A machine-readable description is published at /api/openapi.json [src: app/controllers/api.php].
Authentication is a bearer key of the form apk_ followed by forty hexadecimal characters [src: app/src/Api.php:32]. Keys are checked against a stored hash, must belong to an active user, and must not be revoked; each use updates a last-used timestamp so you can spot a key nobody is using [src: app/src/Api.php:35].
A read-only key is refused at the door. The scope is enforced in the shared entry function rather than in each route, on the stated reasoning that the route which forgets is exactly the one that would leak [src: app/src/Api.php:44].
Webhooks
Slotly sends four events, the four offered in Settings [src: app/src/Webhook.php]: appointment.created (the public booking page, the admin's own form, the REST API and the AI assistant), appointment.rescheduled (the customer's own link, the admin calendar and the assistant), appointment.cancelled (the customer's own link, or staff setting the status) and payment.recorded (a payment recorded against a booking).
Connecting an AI assistant
Slotly speaks the Model Context Protocol at /mcp, with a probe on GET and the endpoint on POST [src: app/controllers/mcp.php].
Eight tools are exposed: list_services, list_staff, availability, list_appointments, appointment_detail, book_appointment, reschedule_appointment and day_report [src: app/controllers/mcp.php].
The assistant's rights come from the API key it is given, and the refusal vocabulary is shared with the admin interface, so an assistant is told the same thing a person would be [src: app/controllers/api.php:37].
The audit log
The log is readable in the admin area and exportable as CSV at /admin/audit.csv [src: app/controllers/v3.php]. As noted under Roles and permissions, every role can read it.
Data-protection tools
A customer's held data can be exported as JSON from /admin/customers/{id}/gdpr.json [src: app/controllers/v3.php]. Slotly records the request; whether your response satisfies the law where you trade is a matter for you and your adviser.
Health check
/healthz returns the application name, the version and an ok flag, and needs no sign-in, so an uptime monitor can poll it [src: app/controllers/api.php:80].
Where things live
| Path | Contents |
|---|---|
app/index.php | the front controller, the version constant and most routes [src: app/index.php:7] |
app/controllers/public.php | the customer-facing booking flow [src: app/controllers/public.php:140] |
app/controllers/admin.php | the admin area [src: app/controllers/admin.php:580] |
app/controllers/api.php | the REST API, and the permission map [src: app/controllers/api.php:27] |
app/controllers/mcp.php | the AI-assistant endpoint [src: app/controllers/mcp.php] |
app/src/Schedule.php | availability, validation, booking and moving [src: app/src/Schedule.php:52] |
app/src/Pricing.php | quotes and coupons [src: app/src/Pricing.php:10] |
app/src/Notify.php | the four notification occasions [src: app/src/Notify.php:23] |
app/cron/reminders.php | the hourly reminder task [src: app/cron/reminders.php:3] |
app/tests/run.php | the test suite [src: app/tests/run.php] |