Slotly API, as shipped in the download
Slotly — API & agent guide (Own It 3.0)
Two machine doors, one set of rules. Everything below authenticates with a bearer key created in Settings → API & Webhooks (apk_…, shown once, revocable any time). A key carries its owner's role — viewer keys read, staff keys can book — and a deactivated user's keys stop working with them. A staff member's key sees and moves only that person's own appointments (/api/v1/appointments, and the MCP appointment tools and day report), exactly as their login does; an admin key sees every appointment.
The public booking endpoints (/api/slots, /api/quote, /api/book, /b/{code}) are your storefront, unauthenticated by design. 3.2 adds to them, without changing what was there: /api/book takes policy_ok (required when you set a cancellation policy), sms_opt_in and w (a waitlist offer token); /api/slots answers open (does anyone work that day); POST /api/waitlist joins the waitlist (service_id, staff_id, date, name, email, phone, sms_opt_in), rate-limited per address. Bookings made through /api/v1 or MCP are confirmed as before; deposit holds apply to the public page only.
4.1.2 guards POST /api/book and still asks for no key and no session token, so a server-side caller works as before: at most 30 posts per address in 10 minutes (then 429); a browser post whose Origin names another website is refused with 403 (a request with no Origin header, such as one from a server, passes the Origin check; the 30 per address limit still applies to it, so integrations should use /api/v1/appointments); and the field website must be empty or absent (the booking page's hidden honeypot; filled = 422, nothing booked). Behind Cloudflare or another proxy, list the proxy in trusted_proxies in config.php, or every caller counts as the proxy's one address (the manual's "Behind Cloudflare or a load balancer" has the steps).
The REST API (/api/v1/*)
Machine-readable spec: GET /api/openapi.json — imports into n8n, Power Automate, Zapier and most HTTP tooling.
GET /api/v1/services active services (duration, price in cents, capacity)
GET /api/v1/slots?service_id&date free slots for a local date (optional staff_id)
GET /api/v1/appointments list; filters: status, from, to (UTC)
POST /api/v1/appointments create — through the conflict-free engine; 409 on a lost slot
GET /api/v1/appointments/{id} one appointment, with payments
GET /api/v1/customers list/search customers
GET /healthz liveness + version (no auth)
POST /api/v1/appointments body:
{
"service_id": 1, "staff_id": 2, "start_utc": "2026-09-01 14:00:00",
"party_size": 1,
"customer_name": "Cara Doe", "customer_email": "cara@example.com",
"notes": "prefers window seat"
}
Pass customer_id instead of name/email to book for an existing customer. The engine validates the slot server-side (lead time, advance window, working hours, buffers, capacity) and inserts inside a serialized transaction with a UNIQUE slot key — the same code path the public form uses, so the API cannot double-book what the form protects.
MCP — the agent door (POST /mcp)
Point Claude Desktop/Code, an n8n AI node, or any MCP client at POST /mcp with the same bearer key. GET /mcp answers with a transport hint.
Tools:
| Tool | Writes? | What it does |
|---|---|---|
list_services | no | services with price + capacity |
list_staff | no | bookable staff for a service |
availability | no | free slots for a service on a date — from the product's own generator |
list_appointments | no | appointments in a UTC range |
appointment_detail | no | one appointment by id or booking code, with payments |
book_appointment | yes | books through Schedule::book — the same engine as every other door |
reschedule_appointment | yes | moves a booking via Schedule::move; the reference code survives |
day_report | no | a local day at a glance: bookings, expected takings, per-staff load |
The rules do not relax for agents. A write tool checks the key's role through the same permission map the browser uses; a viewer key is refused by name. Booking runs the identical serialized transaction — two agents racing for the last seat lose exactly like two humans do.
What an agent deliberately cannot do: cancel, delete, or touch money. There is no such tool. An AI receptionist that can fill your book overnight is useful; one that can empty it is a liability — so the second kind cannot be built on this endpoint.
Connect it to an assistant
Mint the key in the app first: Settings → API keys. Choose Read only when the assistant should answer questions but never change anything — the endpoint then lists only the read tools and refuses the rest by name, so a careless prompt cannot write. Full access behaves as before.
Every client needs the same three facts, and nothing in the handshake is vendor-specific:
| The address | https://booking.example.com/mcp |
| The key | header Authorization: Bearer apk_xxxx |
| The transport | MCP over streamable HTTP, stateless |
Claude — one command, or the same URL and header as a custom connector in the desktop and web apps:
claude mcp add --transport http slotly https://booking.example.com/mcp \
--header "Authorization: Bearer apk_xxxx"
ChatGPT and the OpenAI API — one entry in the Responses API's tools array (in ChatGPT itself, the same URL and key go in as a connector):
{
"type": "mcp",
"server_label": "slotly",
"server_url": "https://booking.example.com/mcp",
"authorization": "apk_xxxx",
"require_approval": "never"
}
**Every other client spells the same three facts differently — copy the shape from its own documentation, not from here.** VS Code is the clearest example of why: its configuration reference (read 6 September 2026) puts servers in .vscode/mcp.json under a "servers" object — *"an object that maps server names to their configurations"*. Pasted as-is, a block written for another client will not load there. The id, the URL and the token are what travel; the JSON around them belongs to whichever client you are configuring.
A local model, n8n, or your own code — n8n's MCP Client node takes the URL and the same Authorization: Bearer header; a model running on your own machine reaches it through any MCP client, so nothing leaves your network at all. Writing it yourself is one POST of JSON-RPC 2.0:
curl -X POST https://booking.example.com/mcp \
-H "Authorization: Bearer apk_xxxx" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Any client that speaks MCP's streamable-HTTP transport works, including ones that do not exist yet: Slotly implements the protocol, not an integration with a particular vendor.
Webhooks
Settings → API & Webhooks. Events: appointment.created, appointment.rescheduled, appointment.cancelled, payment.recorded, appointment.confirmed (3.2: a deposit hold was paid, so the booking now counts; a hold that lapses sends appointment.cancelled with "by": "hold_lapsed"). Each delivery is signed X-Slotly-Signature: sha256=<hmac_sha256(body, secret)> so your receiver can verify origin and integrity. Recent deliveries with status codes are listed on the same page.
Backups, scheduled
Mint a token-guarded URL under Backups & restore, then let cron fetch it:
*/60 * * * * curl -fsS "https://booking.example.com/backup/scheduled?t=<token>" > /dev/null
Each hit writes a secrets-redacted JSON backup into data/backups/ and prunes to the configured keep-count. Restoring always dry-runs first — the diff shows add/update/delete counts per table before you commit.
Connect it to Zapier, Make or n8n
Slotly talks to other tools without a plugin: it sends a webhook (an HTTPS message) the moment something happens, and it accepts calls to the API above. Zapier, Make and n8n can use both.
Before you start
- In Slotly, open Settings → API & Webhooks. Mint an API key (choose Read only if the other tool only reads), and add a webhook when you have its URL (below).
- Use your install's full address, for example
https://slotly.yourcompany.com.
When something happens in Slotly: send it to another tool
- In the other tool, create the trigger that gives you a URL to receive webhooks: - Zapier: the "Webhooks by Zapier" app, trigger Catch Hook (Webhooks by Zapier needs a paid Zapier plan). - Make: module Webhooks → Custom webhook. - n8n: the Webhook node.
- Copy that URL into Slotly under Settings → API & Webhooks, and choose the event or events below.
- Make the event happen once in Slotly, so the other tool receives a sample and you can map its fields.
| Event | Fires when |
|---|---|
appointment.created | A booking is made, online or by staff |
appointment.rescheduled | A booking moves to another time |
appointment.cancelled | A booking is canceled; a deposit hold that lapses also sends this, with "by": "hold_lapsed" |
payment.recorded | A payment is recorded against a booking |
appointment.confirmed | A held booking's deposit was paid, so the booking now counts |
Every delivery carries X-Slotly-Signature, an HMAC-SHA256 signature over the raw body. Zapier and Make accept the delivery without checking it, so keep the webhook URL private. In n8n you can check it in a Code node before using the data.
When something happens elsewhere: act in Slotly
- Zapier: "Webhooks by Zapier", action POST (or Custom Request).
- Make: module HTTP → Make a request.
- n8n: the HTTP Request node.
Send the request to your install's address plus the path from the Endpoints table above, with the header Authorization: Bearer <your API key> and a JSON body. A read-only key cannot write.
The endpoints that change something:
POST /api/waitlistPOST /api/v1/appointments
Example recipes
- appointment.created → a row in Google Sheets: webhook trigger on
appointment.created, then the Google Sheets "Create row" action with the fields you want. - appointment.rescheduled → a message in Slack or Teams: webhook trigger on
appointment.rescheduled, then the Slack or Microsoft Teams "Send message" action. - A new form entry or row elsewhere → Slotly: the other tool's trigger (for example a new Google Sheets row or a form submission), then an HTTP action calling
POST /api/waitlist.
These recipes use only the events and endpoints documented above. We have not run them inside Zapier, Make or n8n ourselves; Slotly's own automated tests check its API.