Every missed call at a clinic or consulting firm is a missed appointment. Receptionists work 8 hours a day. Patients call at 9 PM. By morning, they've booked somewhere else.
An AI receptionist doesn't sleep. It doesn't put callers on hold. It understands natural language, finds the right specialist, checks live availability, and confirms the booking — all in under 60 seconds.
This tutorial shows you how to build one using OpenAI's function calling and Orita's scheduling API. You'll end up with a fully working agent that can be embedded in a chat widget, a phone system, or a WhatsApp bot.
What you'll build
A conversational AI agent called Vera that:
- Greets the caller and asks what kind of appointment they need
- Searches for available professionals by specialty
- Checks real-time availability from the Orita calendar
- Collects the caller's name and email
- Books the appointment and returns a confirmation ID
Architecture
User message
│
▼
OpenAI GPT-4o ──── tool call ──── list_professionals ──── Orita /professionals
│ │
│ tool call ──────── check_availability ──── Orita /slots
│ │
│ tool call ──────── book_appointment ─────── Orita /bookings
│
▼
Confirmed booking + booking ID
OpenAI handles the language: understanding what the caller wants, asking for missing information, and communicating naturally.
Orita handles the scheduling infrastructure: who's available, when, and the atomic act of creating a confirmed booking.
Neither can do the other's job. Together, they're a complete receptionist.
Prerequisites
- Python 3.9+
- An Orita API key → Get one free
- An OpenAI API key → platform.openai.com
- At least one professional configured in your Orita account with availability set
pip install openai orita-sdk
export ORITA_API_KEY=orita_your_key_here
export OPENAI_API_KEY=sk-your_key_here
Step 1: Initialize the clients
import json
import os
from datetime import date, timedelta
from openai import OpenAI
from orita import OritaClient, OritaError
orita = OritaClient(api_key=os.environ.get("ORITA_API_KEY"))
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
TODAY = date.today().isoformat()
TOMORROW = (date.today() + timedelta(days=1)).isoformat()
Both clients are stateless. OritaClient handles authentication headers and error parsing. OpenAI manages the conversation thread.
Step 2: Define the tools
OpenAI function calling lets the model decide when to call an external function and with what arguments. You define the tools as JSON schemas — the model reads the descriptions and uses them to figure out the right call.
TOOLS = [
{
"type": "function",
"function": {
"name": "list_professionals",
"description": (
"List all professionals available for booking. "
"Optionally filter by specialty (e.g. 'psychology', 'nutrition'). "
"Returns each professional's ID, name, specialty, and event types."
),
"parameters": {
"type": "object",
"properties": {
"specialty": {
"type": "string",
"description": "Optional specialty to filter by",
}
},
"required": [],
},
},
},
{
"type": "function",
"function": {
"name": "check_availability",
"description": (
"Check available appointment slots for a given event type and date. "
"Returns available times."
),
"parameters": {
"type": "object",
"properties": {
"event_type_id": {"type": "string"},
"date": {"type": "string", "description": "YYYY-MM-DD"},
"provider_id": {"type": "string"},
},
"required": ["event_type_id", "date"],
},
},
},
{
"type": "function",
"function": {
"name": "book_appointment",
"description": "Book a confirmed appointment. Only call after user confirms.",
"parameters": {
"type": "object",
"properties": {
"event_type_id": {"type": "string"},
"date": {"type": "string"},
"time": {"type": "string", "description": "HH:MM from check_availability"},
"client_name": {"type": "string"},
"client_lastname": {"type": "string"},
"client_email": {"type": "string"},
"notes": {"type": "string"},
},
"required": ["event_type_id", "date", "time",
"client_name", "client_lastname", "client_email"],
},
},
},
]
Three tools. That's all you need. The descriptions are the most important part — the model reads them to decide which to call.
Step 3: Implement the tool execution
When the model calls a tool, you run the Orita API and return the result as a JSON string:
def execute_tool(name: str, args: dict) -> str:
try:
if name == "list_professionals":
specialty = args.get("specialty") or None
professionals = orita.professionals(specialty=specialty)
if not professionals:
event_types = orita.event_types()
return json.dumps({
"message": "Showing all available appointment types.",
"event_types": [
{"id": et["id"], "title": et["title"]}
for et in event_types
],
})
result = []
for pro in professionals:
pro_events = orita.event_types(provider_id=pro["id"])
result.append({
"provider_id": pro["id"],
"name": f"{pro['name']} {pro['lastname']}",
"specialty": pro.get("specialty", "General"),
"event_types": [
{"id": et["id"], "title": et["title"]}
for et in pro_events
],
})
return json.dumps({"professionals": result})
elif name == "check_availability":
slots = orita.slots(
event_type_id=args["event_type_id"],
date=args["date"],
provider_id=args.get("provider_id"),
)
return json.dumps({
"date": args["date"],
"available": bool(slots),
"slots": [{"time": s["value"], "display": s["label"]} for s in slots],
})
elif name == "book_appointment":
booking = orita.book(
event_type_id=args["event_type_id"],
date=args["date"],
time=args["time"],
client_name=args["client_name"],
client_lastname=args["client_lastname"],
client_email=args["client_email"],
notes=args.get("notes"),
)
return json.dumps({
"success": True,
"booking_id": booking["id"],
"status": booking["status"],
"message": "Appointment confirmed! Confirmation email sent.",
})
except OritaError as e:
return json.dumps({"error": str(e)})
Key design decision: always return valid JSON. The model reads the tool output as context. Structured responses — including error messages — make the agent more reliable.
Step 4: The system prompt
The system prompt is the agent's personality and operating procedure. Keep it precise:
SYSTEM_PROMPT = f"""You are Vera, an AI receptionist for a professional services clinic.
Today is {TODAY}.
Your job:
1. Greet the caller and understand what type of appointment they need
2. Use list_professionals to find the right specialist
3. Ask for their preferred date (suggest today or tomorrow if unsure)
4. Use check_availability to find open slots
5. Collect their full name and email
6. Use book_appointment to confirm
7. Give them the booking ID and confirm a confirmation email is on its way
Rules:
- Be conversational — translate tool results into friendly language
- Never dump raw JSON on the user
- Always confirm details before booking
- If no slots available, check the next day proactively
- Present times as "9:00 AM", not "09:00"
"""
Step 5: The conversation loop
The agent loop is the heart of the receptionist. It maintains conversation state and processes tool calls until the booking is confirmed:
def run_receptionist():
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
booking_confirmed = False
while not booking_confirmed:
# Get user input
user_input = input("You: ").strip()
if user_input.lower() in ("quit", "exit"):
break
messages.append({"role": "user", "content": user_input})
# Get AI response
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=TOOLS,
tool_choice="auto",
)
msg = response.choices[0].message
# If the model wants to call a tool
if msg.tool_calls:
messages.append({
"role": "assistant",
"content": msg.content or "",
"tool_calls": [tc.dict() for tc in msg.tool_calls],
})
for tc in msg.tool_calls:
result = execute_tool(tc.function.name,
json.loads(tc.function.arguments))
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": result,
})
# Check if booking just confirmed
if tc.function.name == "book_appointment":
data = json.loads(result)
if data.get("success"):
booking_confirmed = True
# Get assistant response after tool results
follow_up = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=TOOLS,
tool_choice="auto",
)
final_msg = follow_up.choices[0].message
messages.append({
"role": "assistant",
"content": final_msg.content or "",
})
if final_msg.content:
print(f"Vera: {final_msg.content}\n")
else:
# Pure text response
messages.append({"role": "assistant", "content": msg.content or ""})
if msg.content:
print(f"Vera: {msg.content}\n")
The loop continues until booking_confirmed = True or the user exits. Tool calls are processed inline — the model sees both the tool call and the result as part of the conversation history.
What a conversation looks like
You: Hi, I'd like to book an appointment with a nutritionist.
Vera: Of course! Let me check our nutritionists for you.
[Tool] list_professionals(specialty=nutrition)
Vera: We have Dr. Sofía Ramírez available for 60-minute nutrition consultations.
What date works best for you? I can check today or tomorrow.
You: Tomorrow afternoon would be great.
Vera: Let me check tomorrow's afternoon availability.
[Tool] check_availability(event_type_id=evt_xxx, date=2026-07-30)
Vera: Dr. Ramírez has openings tomorrow at 3:00 PM, 4:00 PM, and 5:00 PM.
Which works best?
You: 3 PM, perfect.
Vera: Excellent! Could I get your full name and email to complete the booking?
You: María García, maria.garcia@example.com
Vera: Let me confirm: María García, booking with Dr. Sofía Ramírez
tomorrow July 30th at 3:00 PM. Shall I go ahead?
You: Yes, please!
[Tool] book_appointment(event_type_id=evt_xxx, date=2026-07-30, time=15:00, ...)
Vera: You're all set! Your booking ID is BK-29471.
A confirmation email is on its way to maria.garcia@example.com.
See you tomorrow! 🎉
What the professional sees
When the booking is created through Orita, the professional receives:
Email notification:
New appointment: Nutrition consultation
Patient: María García (maria.garcia@example.com)
Date: July 30, 2026 at 3:00 PM
Booked via: AI Receptionist
Webhook payload (if configured):
{
"event": "booking.created",
"booking": {
"id": "BK-29471",
"eventTypeId": "evt_xxx",
"date": "2026-07-30",
"time": "15:00",
"clientName": "María García",
"clientEmail": "maria.garcia@example.com",
"status": "confirmed"
}
}
The professional sees a normal booking in their Orita dashboard — they never need to know whether a human or an AI made it.
Extend this
SMS reminders: After booking, call Twilio's API to send a reminder 24h before the appointment. The booking ID from Orita is your reference.
Google Calendar sync: Orita webhooks fire on booking.created. Your webhook handler can use the Google Calendar API to create a calendar event on the professional's calendar automatically.
Multilingual support: GPT-4o understands and responds in any language. Change the system prompt to "Respond in the same language the user uses" and the receptionist handles Spanish, French, Portuguese, and more without any code changes.
Voice interface: Swap input() for a Twilio <Gather> webhook and print() for a TTS response. The same agent logic runs on actual phone calls.
Embed in a chat widget: The run_receptionist() function is pure Python. Wrap it in a FastAPI endpoint and call it from any frontend — a widget, a WhatsApp bot, or a Telegram integration.
Get the full code
The complete example with the demo mode is available in the Orita Python examples repository.
Run it yourself in under 5 minutes:
git clone https://github.com/Alkilo-do/orita-python
cd orita-python
pip install -e .
export ORITA_API_KEY=orita_your_key
export OPENAI_API_KEY=sk-your_key
python examples/ai_receptionist.py --demo
Ready to build your own?
Get an Orita API key and start with 250 free bookings/month. No credit card required.
Have questions? Reach us at hola@orita.online or open an issue in the GitHub repo.
