AI WhatsApp Receptionist for Clinics: Booking, Reminders and Staff Handoff

A WhatsApp assistant that books, cancels and reschedules clinic appointments around the clock. It understands voice notes, answers common questions, sends reminders and passes urgent or medical questions to a real staff member. Built with n8n, OpenAI and Postgres.

Clinics lose patients when messages go unanswered after hours or when the front desk is too busy to reply. ClinicOS answers patients on WhatsApp the way a receptionist would, any time of day.

What it does

  • Books, cancels and reschedules appointments based on each doctor’s real availability
  • Understands voice notes and casual Arabic or English
  • Answers questions about prices, location, payment and parking
  • Remembers returning patients, so they don’t repeat their details
  • Sends a reminder 24 hours before each appointment
  • Never gives medical advice. Urgent cases are sent to emergency services and the chat is handed to a staff member

Why it’s reliable

  • The AI only works out what the patient wants. The booking itself is handled by the database, so the same slot can’t be booked twice and nothing is confirmed unless the patient clearly agrees
  • If WhatsApp goes down for a moment, messages are queued and sent later. Nothing gets lost and nothing is sent twice
  • Tested against 270 realistic conversations before the demo, including messy messages, changes of mind and attempts to trick the bot

Tools: n8n, OpenAI, WhatsApp Cloud API, Supabase (Postgres)
Status: working prototype, shown in a live demo

video: https://youtu.be/YoO-eJfvpjM
Code: GitHub - ziadmohamed-ai/ClinicOS-WhatsApp-AI-Engine: Arabic-first WhatsApp AI receptionist for clinics: dialect-aware chat, voice notes, live slot booking, human handoff. n8n + Supabase + OpenAI. · GitHub

Booking owned by the database and “nothing is confirmed unless the patient clearly agrees” are the two decisions I’d have made too. On a hotel booking agent of mine, the AI never confirms a payment either; the database or a person does.

One thing to check before going live with the reminders. A reminder 24 hours before the appointment usually lands outside WhatsApp’s 24-hour customer service window, because the patient’s last message was when they booked, days earlier. Outside that window the Cloud API only accepts an approved template, so the reminder has to be a utility template with the date and the doctor as variables, approved before launch. I’d also log the send result per reminder: if the template gets paused or rejected, the reminder fails quietly unless something checks it.

How do you handle the patient’s reply to the reminder? It reopens the window, so “I can’t make it” could go straight into your reschedule path.

Good catch on the 24-hour window, that is super easy to miss before going live. We actually have an outbox table in Postgres that tracks the last inbound message time and logs the send result and error for every single reminder, so if Meta ever pauses or rejects a template it gets flagged right away instead of failing quietly. Before launch we just make sure the reminder uses the approved utility template with the doctor and date variables.

For the reply, that is pretty much how it works. Once the patient replies that they can’t make it, their message reopens the 24-hour window and goes straight into the reschedule or cancel flow. The bot pulls the next available slots from the database, and just like the initial booking, it waits for the patient to clearly agree before actually moving or canceling the appointment.

Nice build, and letting the database own the booking is the part I would copy.

I built a much smaller WhatsApp bot on the Cloud API (orders for a demo restaurant) and three things caught me that might be worth a check on yours:

  1. Status updates fire the same trigger. Sent, delivered and read each arrive as their own event, so one customer message became four runs until I added an If node that only lets real messages through.
  2. The access token from Meta’s setup page stopped working after 24 hours. A System User token set to never expire fixed that.
  3. Memory. Mine took an order and forgot it one message later until the memory session key was the customer’s phone number.

On voice notes: mine just replies “please type your order”, so I’m curious how you handle them. Do you transcribe the audio first and pass the text to the same flow?

Those three gotchas get almost everyone on their first Cloud API build, especially the status events triggering four runs per message. The way I handled that here is right in the Postgres ingest function. When the webhook fires, the database logs the delivered and read statuses against the outbox table, and only returns actual new customer messages to the rest of the workflow, so status pings stop right after the webhook acknowledgment.

For memory, I skipped the built-in memory node completely and kept the conversation history and the pending slot state inside Postgres tied to the patient’s phone number. That way even if the workflow restarts between messages, it still knows exactly which appointment slot they were looking at, and of course using a permanent System User token so authentication doesn’t die after a day.

On voice notes, right now I do the same thing and either ask for text or route it to the clinic reception if it looks urgent, since I didn’t want a transcription error to mess up a medical booking. Adding a step to fetch the media URL and run it through Whisper before feeding it into the same intent flow is definitely the next upgrade though, especially since patients love sending voice notes.