A physiotherapy clinic front desk where a smartphone quietly holds the counter while a physiotherapist treats a patient behind
TL;DR

Cliniko has an official API, and a WhatsApp AI agent can book real appointments through it, if you put a small purpose-built API layer between the agent and the calendar. The layer exposes six narrow endpoints (list clinics, check availability, book, list a patient's bookings, cancel, reschedule), enforces identity and safety rules in code, keeps Cliniko as the only source of truth, and turns every failure into a sentence the agent can relay. We built exactly this to replace a make.com version that kept wobbling. This post is the architecture, with the production lessons left in.

The gap between talking and doing

In our guide to HTTP Request steps, we covered how a WhatsApp AI gets its second brain: the ability to look things up in your records mid-conversation. Reads are transformative on their own. But clinics don't just want an AI that can see the calendar. They want one that can change it: book the slot, move the slot, cancel the slot, correctly, at 10pm, without waking anyone up.

Writes are a different sport. A wrong read is an awkward reply; a wrong write is a phantom appointment in a real practitioner's diary, or worse, someone else's appointment cancelled. This post is about how we build the write side safely, using a build for a physiotherapy group running on Cliniko as the worked example.

Elmhurst Physio is a fictional business: a four-outlet physiotherapy group used here so we can show real payloads and real lessons without describing any specific client. The architecture, the endpoint design, and the production incidents are drawn from live Zelix deployments of this exact pattern.

The architecture: four pieces, one source of truth

The whole system is a relay. Each piece has one job, and the calendar has exactly one home.

The agent talks. The API is the only thing with hands. Cliniko is the only thing with memory.

The piece that surprises people is the third one. Why not point the agent's HTTP actions straight at Cliniko's API? Because almost nothing a booking agent does is a single call. "Book me Thursday 3pm" is genuinely five operations against Cliniko: find the patient record, verify it really is this patient, create the record if they're new, re-check the slot is still open, then write the appointment. A language model asked to run that raw chain perfectly, every time, in the right order, will eventually improvise. The API layer runs the chain in tested code instead, identically, on every request.

Just as important is what the layer does not do: it stores nothing. No mirror of the calendar, no cached patient list, no database of its own. Every question is answered by asking Cliniko fresh, which means the answer can never be stale and there is no second copy of anything to drift out of sync. If a build can be stateless, make it stateless: every piece of stored state is a future support ticket.

A clinic coordinator at a workstation with the practice calendar on screen while treatment continues in the room behind
One calendar, one home. The AI reads it live and writes to it carefully; it never keeps a copy.

The six endpoints

The agent's entire vocabulary for touching the calendar is six endpoints. Not a general-purpose gateway: six fixed questions, each with a strict format in and a strict format out. Anything without an endpoint simply cannot happen, no matter how a conversation is phrased.

EndpointOne job
GET /api/clinicsThe outlets, each outlet's practitioners, and the bookable appointment types with durations, live from Cliniko
GET /api/availabilityOpen slots for one outlet across a short date range, optionally filtered by practitioner or type
POST /api/bookFind or create the patient, re-verify the slot, create the appointment with a note saying the agent made it
GET /api/appointmentsThe requesting patient's own upcoming bookings, identity-verified, nobody else's
POST /api/cancelCancel one booking the patient owns, recorded as a proper Cliniko cancellation
POST /api/rescheduleMove a booking to a new time, practitioner, or outlet, preserving its duration

Here is a real exchange, exactly as it travels. Note that the response carries both a machine-precise timestamp (which the agent copies character-for-character into a booking, so a time can never be mistyped) and friendly wording (which the agent speaks):

# The agent asks: what is free at the Northgate studio this Thursday?
GET /api/availability?clinic=Northgate&date_from=2026-07-23&date_to=2026-07-23

# The API answers
{
  "ok": true,
  "clinic": "Northgate",
  "type": "Initial Assessment",
  "slots": [
    { "start": "2026-07-23T10:00", "day": "Thursday 23 Jul", "time": "10:00am", "practitioner": "Hannah" },
    { "start": "2026-07-23T15:30", "day": "Thursday 23 Jul", "time": "3:30pm",  "practitioner": "Marcus" }
  ]
}

Every response uses the same envelope: ok plus the data, or ok: false plus a sentence written for the patient.

And here is that flow as the patient experiences it:

Hi, any slot this Thursday afternoon at Northgate?9:12 pm
⚡ AI checks live availability in Cliniko · under a second
Hi Nadia! This Thursday at Northgate I have 3:30pm with Marcus free. Shall I book you in?9:12 pm
yes please9:13 pm
⚡ AI re-verifies the slot, then books it in Cliniko
Booked! Thursday 23 Jul, 3:30pm with Marcus at Northgate. See you then, we'll send a reminder the day before.9:13 pm

9pm on a weeknight, no human involved, and the appointment is genuinely in the practice calendar with a note saying the agent made it.

The production rules, with the incidents that wrote them

The design above looks tidy because launch week wasn't. These are the rules that earned their place, and every one of them transfers to any agent-plus-calendar build, whatever the platform.

1 · Business errors travel as HTTP 200

Textbook API design says a taken slot deserves an error status. But respond.io's HTTP client discards the response body on any non-2xx status, so a helpful error body is invisible to the agent, and an agent that sees a void starts inventing. So the API returns business failures as HTTP 200 with ok: false and a message the agent can relay word for word: "That time was just taken. Nearest available: 3:15pm, 3:30pm." Only a bad access key gets a hard 401. Know the textbook rule, know exactly why you're breaking it, write the reason down.

2 · Expect the word "any", because the platform forces it

respond.io treats every defined input as required, so when a patient has no practitioner preference, the agent fills the literal word any. Before we normalised no-preference words in the API, availability checks filtered for a practitioner named "any", found nobody, and reported a half-empty clinic as fully booked. The platform's quirks are design inputs, not annoyances to wish away.

3 · Identity is phone plus name, never name alone

Cliniko's patient search cannot filter by phone number, so the API narrows by name and then verifies the phone against the record's stored numbers before showing or changing anything. The phone itself is never typed by anyone: it is attached by respond.io from the WhatsApp conversation, which means a patient cannot ask the agent to act on someone else's number. Asking the agent to "check appointments for another number" fails by construction, not by politeness.

4 · Design for the agent's real weaknesses

Cliniko appointment IDs are 19 digits long. Language models garble long numbers, reuse cancelled IDs from earlier in a conversation, and often hold no ID at all because the platform drops earlier turns' HTTP responses. Our first version assumed the agent would quote IDs reliably; a live reschedule then stalled twice while the agent asked the patient for "the appointment reference". No patient has ever known their Cliniko ID. Now cancel and reschedule accept a missing or broken ID and recover from the patient's own verified bookings: exactly one upcoming means act on it, several means ask which, none means say so.

5 · Re-verify before every write

Between the agent offering a slot and the patient saying yes, someone else can take it. The booking endpoint never trusts the conversation: it re-checks availability at the moment of writing and books only if the exact slot is still open. If it's gone, the patient gets the nearest alternatives in the same breath instead of a failed booking.

An engineer working at a dusk-lit desk with laptop and monitor, careful systems work in progress
Most of this system's intelligence is not in the AI. It's in the boring, tested code around it.

Why this replaced make.com

The first version of this integration ran on make.com scenarios wired to contact fields. We use make.com happily for what it's built for: scheduled jobs, notifications, event-driven syncs. But a live booking flow exposed four structural limits at once:

  • No version history. Logic lived in a visual canvas: no diff, no review, no way to know what changed last Tuesday. The API lives in git, where every change is reviewed and any version can be restored in one click.
  • No tests. Every scenario change was verified by clicking through it by hand. Every endpoint and helper in the API has automated tests that run before anything deploys.
  • State smeared everywhere. Slot choices and booking details sat in contact fields that scenarios and agents both wrote, drifting out of step with the calendar. The API holds no state at all.
  • Silent failure. A failed scenario step usually meant silence, and the agent talked on regardless. The API turns every failure into a message the agent relays.

The honest summary: if a mistake in the automation writes something wrong into a real system, it deserves code, tests, and version control. If it just sends a digest or a notification, lighter tools remain the right call.

Hands holding a phone over breakfast, an appointment settled in a quiet minute

The safety model, in one paragraph

The API layer stores no patient data: Cliniko remains the only home. Every request is locked to the WhatsApp number it arrived from, identity requires name and phone to match the record, and every cancel or reschedule additionally verifies the booking belongs to that verified patient. Both access keys (the agent's key to the API, and the API's key to Cliniko) live server-side only, and the practice can revoke the Cliniko key at any moment from inside Cliniko. Logs carry record IDs, never names or phone numbers. And each agent-made booking is stamped with a note in Cliniko itself, so the front desk can always tell agent activity from their own.

Does this only work for Cliniko?

No, and that's the point of the pattern. Cliniko happens to have a clean, well-documented API, which makes it an excellent system of record for this architecture. But the shape transfers to any bookable system with an API: other practice-management platforms, gym and studio software, POS systems, CRMs. The endpoint list changes with the business; the pattern (narrow endpoints, one source of truth, identity in code, errors the agent can speak) does not.

The prerequisite is the same one from the HTTP Request steps guide: your bookings have to live in a system, not a notebook. The AI can only be as good as the records behind it.

The build in one line
The agent talks. The API is the only thing with hands. The calendar is the only thing with memory.

Frequently asked questions

Can a WhatsApp AI book appointments directly into Cliniko?

Yes. Cliniko has an official API, and an AI agent on respond.io can book, cancel, and reschedule real appointments through a small custom API layer that translates agent requests into safe Cliniko operations, with identity and safety rules enforced in code on every call.

Why not connect respond.io straight to Cliniko's API?

A direct connection can serve a simple read-only lookup. Booking is a multi-step chain (find patient, verify phone, re-check slot, write), and the middle layer runs that chain in tested code, keeps the Cliniko key in one server-side place, and converts errors into messages the agent can relay. Directly wired, all of that would depend on the language model behaving perfectly, every turn.

Why replace make.com for this?

make.com remains excellent for scheduled and event-driven automations. A live booking flow needs version history, automated tests, instant rollback, and readable errors, which a visual scenario canvas doesn't provide. The dividing line: automations that write into a real system of record deserve code.

Is patient data safe in a setup like this?

Built properly, yes. The layer stores no patient data, every request is phone-locked to the sender, identity needs name plus phone to match, all traffic is HTTPS, keys live server-side with the practice able to revoke Cliniko access at any time, and logs contain record IDs only.

Does this work with practice systems other than Cliniko?

Yes. Any system of record with an API fits the pattern: the endpoints change, the architecture doesn't. Cliniko's API is particularly well documented, which makes it one of the smoother systems to build against.

What does a build like this involve?

Three parts: the API layer (a small set of endpoints deployed on a platform like Vercel), the respond.io side (an AI agent whose HTTP actions are wired to those endpoints, on a plan tier that includes HTTP requests), and the agent's instructions. The endpoints are the engineering; the rest is careful configuration and honest testing, ugly paths included.


Final words

The difference between a WhatsApp AI that impresses in a demo and one that runs a clinic's front desk is not the model. It's the plumbing: six narrow endpoints, one source of truth, identity rules that don't depend on the AI's mood, and errors that arrive as sentences instead of silence.

None of that is glamorous. All of it is why the booking actually lands in the calendar at 9pm, correctly, with a note saying who made it. If your bookings live in Cliniko or any system with an API, this pattern is buildable for your business today.