An AI receptionist for healthcare is not a chatbot with appointment links. It's an agent that listens to what a patient needs, determines which provider is eligible and available, presents the options, and books the appointment — without any human doing the routing step in the middle.
Building it requires more than a language model. It requires scheduling infrastructure that understands provider eligibility, not just calendar availability.
What an AI receptionist for healthcare actually does
General-purpose chatbots can answer FAQ questions. They can tell a patient what your clinic's hours are. They can send them a link to your Calendly page.
An AI receptionist does something harder: it resolves the routing problem. Given what the patient says — their need, their insurance, their language, their preference for in-person vs virtual — it determines which of your providers should handle this request and when.
The difference between these two things is enormous:
| General chatbot | AI receptionist | |---|---| | Answers questions | Resolves routing | | Sends a booking link | Picks the right provider | | "Here is our calendar" | "María García is available Tuesday at 9am, she speaks Spanish, and accepts Sanitas" | | Stops at information | Ends with a confirmed booking |
The routing capability is what makes the AI receptionist worth building. And routing requires eligibility logic that no calendar API provides.
The core challenge: routing, not booking
A patient says: "I need a Spanish-speaking therapist who can see me virtually this week. I have Sanitas."
Now what?
A booking API gives you slot lookup for a known provider. It does not help you answer: which of your 40 therapists speaks Spanish, accepts Sanitas, offers virtual sessions, and is accepting new patients right now?
That filtering step — what we call constraint resolution — is where AI receptionists break without proper infrastructure. You either:
- Build a custom eligibility database, write filtering logic, maintain it as providers update their profiles, and bolt it onto your scheduling backend
- Or use an API that handles this as a first-class operation
The second path is faster, more maintainable, and gives you explainability out of the box.
Why calendar APIs aren't enough
Calendar APIs (Google Calendar, Microsoft Graph, Cal.com, Acuity) expose one thing: time slots for a known calendar or user. They have no concept of:
- Which providers speak which languages
- Which insurance plans a provider accepts
- Whether a provider is accepting new clients
- What modalities they offer
All of that has to live somewhere else. If you're building on a plain calendar API, you're maintaining that "somewhere else" yourself — usually a relational database that has to stay synchronized with the scheduling system, with no guarantee of consistency.
For a small team with 5 providers, this is manageable. For a behavioral health platform scaling to 200+ therapists, it becomes a maintenance burden that absorbs engineering time indefinitely.
The three-step flow AI receptionists need
A production AI receptionist booking flow has three steps:
1. Resolve — Given the patient's needs, find the eligible provider(s) with availability.
2. Hold — Reserve the slot for the patient while they confirm. Without this, a concurrent booking can take the slot between when you surface it and when the patient confirms.
3. Confirm — Commit the booking atomically. Send confirmations to both provider and patient. Fire webhooks for downstream systems.
Each step has different safety properties. Resolve is read-only — safe to call multiple times with different constraints as the agent clarifies the patient's needs. Hold is write-only (creates a temporary reservation) — it should only happen once the agent has a specific option the patient is considering. Confirm is destructive and irreversible — it should only happen after the patient has explicitly approved.
Agent safety: what the AI can do autonomously vs what requires approval
Not all scheduling decisions should be made autonomously. A well-designed AI receptionist follows a clear delegation model:
Agent can do autonomously:
- Call resolve to find eligible providers
- Present options to the patient
- Answer questions about providers (specialty, language, modality)
- Collect patient contact information
- Hold a slot while the patient decides
Requires patient approval before proceeding:
- Confirming a booking (the patient must explicitly say yes)
- Selecting a specific provider from multiple options (if the patient hasn't indicated a preference)
- Rescheduling an existing booking
Never autonomously:
- Cancelling a booking
- Changing insurance or billing information
- Making medical recommendations
The resolve → hold → confirm pattern enforces this naturally: resolve and hold are exploratory, confirm is commitment, and confirm only happens when the patient (or a human in the loop) approves.
Code example: AI receptionist that routes and books
Here is a minimal AI receptionist using OpenAI function calling and the Orita SDK:
import OpenAI from 'openai';
import Orita from 'orita-sdk';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const orita = new Orita({ apiKey: process.env.ORITA_API_KEY });
const tools = [
{
type: 'function',
function: {
name: 'resolveScheduling',
description:
'Find eligible providers and available slots for a patient request. ' +
'Use when you know the service needed and any patient constraints (language, insurance, modality).',
parameters: {
type: 'object',
properties: {
serviceId: { type: 'string', description: 'The service the patient needs' },
languageCode: { type: 'string', description: 'Language code if the patient has a language preference' },
insuranceCode: { type: 'string', description: 'Insurance code if the patient mentioned their plan' },
modality: {
type: 'string',
enum: ['virtual', 'in-person', 'hybrid'],
description: 'Preferred modality if mentioned',
},
fromDate: { type: 'string', description: 'Earliest date (YYYY-MM-DD)' },
toDate: { type: 'string', description: 'Latest date (YYYY-MM-DD)' },
},
required: ['serviceId', 'fromDate', 'toDate'],
},
},
},
{
type: 'function',
function: {
name: 'confirmBooking',
description:
'Confirm a booking after the patient has approved a specific option. ' +
'Do NOT call this without explicit patient approval.',
parameters: {
type: 'object',
properties: {
resolutionId: { type: 'string' },
optionId: { type: 'string' },
patientName: { type: 'string' },
patientEmail: { type: 'string' },
patientPhone: { type: 'string' },
},
required: ['resolutionId', 'optionId', 'patientName', 'patientEmail'],
},
},
},
];
async function executeToolCall(name, args) {
if (name === 'resolveScheduling') {
const constraints = {};
if (args.languageCode) constraints.languageCodes = { anyOf: [args.languageCode] };
if (args.insuranceCode) constraints.insuranceCodes = { anyOf: [args.insuranceCode] };
if (args.modality) constraints.modalityCodes = { anyOf: [args.modality] };
constraints.acceptsNewClients = true;
const resolution = await orita.resolveScheduling({
serviceId: args.serviceId,
constraints,
dateRange: { from: args.fromDate, to: args.toDate },
});
return JSON.stringify({
resolutionId: resolution.resolutionId,
options: resolution.options.map(opt => ({
optionId: opt.optionId,
providerName: opt.provider.name,
slot: opt.slot,
reason: opt.reason,
matchedConstraints: opt.matchedConstraints,
})),
});
}
if (name === 'confirmBooking') {
const booking = await orita.confirmResolution(args.resolutionId, {
optionId: args.optionId,
customer: {
name: args.patientName,
email: args.patientEmail,
phone: args.patientPhone,
},
});
return JSON.stringify({
bookingId: booking.id,
status: booking.status,
provider: booking.provider?.name,
startUtc: booking.startUtc,
});
}
}
async function runReceptionist(patientMessage) {
const messages = [
{
role: 'system',
content: `You are a healthcare scheduling assistant. Help patients find and book appointments.
Today: ${new Date().toISOString().split('T')[0]}
Rules:
- Always call resolveScheduling before presenting any provider options
- Present options clearly: provider name, date, time, and why they match
- NEVER call confirmBooking without the patient saying "yes" or "confirm" to a specific option
- Collect patient name and email before confirming
- If no eligible providers found, explain why and ask if they can be flexible on any constraint
- For urgent symptoms, always direct to emergency services first`,
},
{ role: 'user', content: patientMessage },
];
while (true) {
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages,
tools,
tool_choice: 'auto',
});
const message = response.choices[0].message;
messages.push(message);
if (!message.tool_calls) {
return message.content; // Final response to patient
}
const toolResults = await Promise.all(
message.tool_calls.map(async call => ({
role: 'tool',
tool_call_id: call.id,
content: await executeToolCall(call.function.name, JSON.parse(call.function.arguments)),
}))
);
messages.push(...toolResults);
}
}
A session with this receptionist looks like:
Patient: I need a Spanish-speaking therapist for virtual sessions. I have Sanitas.
→ resolveScheduling({ serviceId: 'svc_therapy', languageCode: 'es',
insuranceCode: 'sanitas', modality: 'virtual',
fromDate: '2026-08-04', toDate: '2026-08-11' })
Receptionist: I found a great match for you. Dr. María García is available
Tuesday, August 5th at 9:00 AM. She speaks Spanish, offers virtual sessions,
and accepts Sanitas. She's also accepting new patients.
Would you like to book this appointment? If so, I'll need your name and email.
Patient: Yes, that works. I'm Ana López, ana@example.com.
→ confirmBooking({ resolutionId: 'res_abc123', optionId: 'opt_001',
patientName: 'Ana López', patientEmail: 'ana@example.com' })
Receptionist: All set! Your appointment with Dr. María García is confirmed
for Tuesday, August 5th at 9:00 AM (virtual). Booking ID: BK-29183.
You'll receive a confirmation email at ana@example.com with the video link.
Webhook integration: staying updated after booking
When confirmBooking succeeds, Orita fires a booking.created webhook to your configured endpoint. This enables downstream automation without polling:
// Your webhook handler
app.post('/webhooks/orita', express.json(), async (req, res) => {
const { event, data } = req.body;
if (event === 'booking.created') {
// Send SMS reminder to patient
await sms.send(data.customer.phone, `Appointment confirmed: ${data.startUtc}`);
// Log to your EHR or CRM
await crm.createContact({ email: data.customer.email, source: 'ai-receptionist' });
// Notify the care coordinator
await slack.notify(`New booking: ${data.provider.name} with ${data.customer.name}`);
}
if (event === 'booking.cancelled') {
await sms.send(data.customer.phone, 'Your appointment has been cancelled.');
}
res.json({ received: true });
});
The receptionist doesn't need to poll for status — webhooks handle all downstream state transitions.
Use cases
This pattern works across a range of healthcare and professional service verticals:
Behavioral health — route patients to therapists by language, insurance, specialty (CBT, DBT, trauma), and modality. The most common use case for constraint-based routing.
Telehealth — route patients to physicians by symptom type, license state, insurance, and availability. Combine with the AI's symptom intake to auto-populate the provider selection criteria.
Coaching platforms — match clients to coaches by methodology, industry, timezone, and engagement format. Less regulated than clinical care, but the routing logic is equally complex.
Legal intake — route clients to attorneys by jurisdiction, practice area, case type, and fee structure. Many legal platforms have geographically distributed provider networks that require state-level eligibility filtering.
Start building
Orita's free tier includes 250 bookings per month — enough to build and test a full AI receptionist flow. The Node SDK, Python SDK, and MCP server are all available.
Check the developer docs for the full resolveScheduling API reference, constraint schema, and webhook event catalog.
Questions? hola@orita.online
