How to Build a Voice Agent That Books Appointments via Cal.com
Connect an ElevenLabs voice agent to Cal.com using Claude Code to automatically check availability and book discovery calls from your website.
Why Voice Agents Are Replacing Booking Forms
Nobody enjoys filling out a scheduling form. You pick a time, get a confirmation email, then wait. The whole process feels cold — especially when someone just wants a quick discovery call.
Voice agents solve this differently. A visitor lands on your site, speaks to an AI, and walks away with a confirmed appointment — no clicks, no forms, no friction. The technology to build this is mature enough now that you can wire it together in an afternoon.
This guide walks through how to build a voice agent that books appointments via Cal.com. You’ll connect an ElevenLabs conversational agent to the Cal.com API, handle availability checks in real time, and deploy it to your website. By the end, you’ll have a working system that can schedule discovery calls automatically — even while you sleep.
What You’ll Need Before You Start
Before touching any code, make sure you have accounts and access to the right tools.
Required accounts:
- Cal.com — free tier works fine for this
- ElevenLabs — for the voice agent and speech synthesis
- Claude Code (Anthropic’s agentic coding tool) — this is what we’ll use to wire everything together
- A website or web app where you’ll embed the agent
API keys you’ll need:
- Cal.com API key (found in Settings → Developer → API Keys)
- ElevenLabs API key
- Anthropic API key (for Claude)
Assumptions:
- You’re comfortable with basic terminal/command line usage
- You have Node.js installed locally
- You understand what a webhook is, at least in general terms
If you want a no-code path instead, skip to the MindStudio section later in this article.
How the System Works
Before building anything, it helps to understand the flow.
When a visitor speaks to your voice agent, here’s what happens under the hood:
- The visitor speaks — ElevenLabs captures audio via the browser
- Speech-to-text converts their words into a transcript
- The AI agent reasons about the intent — are they asking about availability? Trying to book a specific time?
- The agent calls Cal.com’s API to check open slots or create a booking
- The agent responds in natural speech, confirming the appointment or offering alternatives
- Cal.com fires a confirmation email to both parties automatically
The key insight is that this isn’t just a voice interface bolted onto a booking form. The agent understands context, handles back-and-forth conversation, and resolves ambiguity before making the API call.
Set Up Cal.com for API Access
Cal.com’s API is well-documented and straightforward. Here’s how to get it ready.
Create a bookable event type
Log into Cal.com and create an event type for your discovery call. Set the duration, add any questions you want answered before the booking, and configure your availability hours.
Note your event type ID — you’ll need it for API calls. You can find it in the URL when editing the event: cal.com/event-types/[ID]/edit.
Generate your API key
Go to Settings → Developer → API Keys and create a new key. Copy it somewhere safe — Cal.com won’t show it again.
Test the availability endpoint
Run a quick test to confirm your setup works:
curl -X GET \
"https://api.cal.com/v1/slots?apiKey=YOUR_KEY&eventTypeId=YOUR_EVENT_ID&startTime=2025-01-20T00:00:00Z&endTime=2025-01-27T00:00:00Z" \
-H "Content-Type: application/json"
If you get back a JSON object with available time slots, you’re good. If not, double-check your event type ID and API key.
Key endpoints you’ll use
| Endpoint | Method | What it does |
|---|---|---|
/v1/slots | GET | Returns available time slots |
/v1/bookings | POST | Creates a new booking |
/v1/bookings/{id} | GET | Retrieves booking details |
/v1/bookings/{id}/cancel | DELETE | Cancels a booking |
Build the Voice Agent with ElevenLabs
ElevenLabs Conversational AI lets you create an agent with a custom voice, a system prompt, and tool-calling capabilities. The tool-calling part is what connects it to Cal.com.
Configure your agent
In the ElevenLabs dashboard, create a new Conversational AI agent. Set:
- Name — something like “Scheduling Assistant”
- Voice — pick one that matches your brand tone
- First message — what the agent says when someone opens the widget (e.g., “Hi! I can help you book a discovery call. Want to check availability?”)
Write a tight system prompt
Your system prompt defines how the agent behaves. Keep it focused:
You are a scheduling assistant for [Company Name].
Your only job is to help visitors book a 30-minute discovery call.
When helping someone book:
1. Ask for their preferred date and time range
2. Check availability using the check_availability tool
3. Confirm a specific slot before booking
4. Collect their name and email address
5. Create the booking using the create_booking tool
6. Confirm the appointment details clearly
If asked about anything unrelated to scheduling, politely redirect.
Do not make up availability — always use the tools.
Not a coding agent. A product manager.
Remy doesn't type the next file. Remy runs the project — manages the agents, coordinates the layers, ships the app.
Short, directive prompts work better than long ones for task-focused agents.
Define the tools
ElevenLabs supports tool definitions in OpenAI function-calling format. You’ll define two tools: one for checking availability and one for creating bookings.
check_availability tool:
{
"name": "check_availability",
"description": "Check available time slots for booking a discovery call",
"parameters": {
"type": "object",
"properties": {
"start_time": {
"type": "string",
"description": "Start of the search window in ISO 8601 format"
},
"end_time": {
"type": "string",
"description": "End of the search window in ISO 8601 format"
}
},
"required": ["start_time", "end_time"]
}
}
create_booking tool:
{
"name": "create_booking",
"description": "Book a discovery call at a specific time",
"parameters": {
"type": "object",
"properties": {
"start_time": {
"type": "string",
"description": "Booking start time in ISO 8601 format"
},
"name": {
"type": "string",
"description": "Guest's full name"
},
"email": {
"type": "string",
"description": "Guest's email address"
}
},
"required": ["start_time", "name", "email"]
}
}
Build the Backend with Claude Code
The voice agent needs somewhere to send tool calls. You’ll build a small API server that receives those calls and proxies them to Cal.com. This is where Claude Code does the heavy lifting.
Scaffold the project
Open Claude Code and give it a clear prompt:
Build a Node.js Express server that:
1. Exposes a POST /tools endpoint
2. Receives tool calls from ElevenLabs in the format { tool_name, parameters }
3. Routes check_availability calls to Cal.com's /v1/slots endpoint
4. Routes create_booking calls to Cal.com's /v1/bookings endpoint
5. Returns responses in the format ElevenLabs expects
6. Uses environment variables for the Cal.com API key and event type ID
Claude Code will generate the scaffolding, install dependencies, and write the handler logic. Review what it produces before running it — particularly the error handling around the Cal.com API calls.
The core handler logic
Once Claude Code generates the base, your tool handler will look roughly like this:
app.post('/tools', async (req, res) => {
const { tool_name, parameters } = req.body;
if (tool_name === 'check_availability') {
const slots = await calClient.getSlots({
eventTypeId: process.env.CAL_EVENT_TYPE_ID,
startTime: parameters.start_time,
endTime: parameters.end_time,
});
return res.json({ slots });
}
if (tool_name === 'create_booking') {
const booking = await calClient.createBooking({
eventTypeId: process.env.CAL_EVENT_TYPE_ID,
start: parameters.start_time,
responses: {
name: parameters.name,
email: parameters.email,
},
});
return res.json({ bookingId: booking.uid, confirmed: true });
}
res.status(400).json({ error: 'Unknown tool' });
});
Deploy the server
For production, deploy to Railway, Render, or Fly.io. Each takes under 10 minutes and gives you a public HTTPS URL — which ElevenLabs requires for webhook tool calls.
Set your environment variables on the platform:
CAL_API_KEYCAL_EVENT_TYPE_ID
Connect the server to ElevenLabs
Back in the ElevenLabs dashboard, go to your agent’s tool settings and add the server URL as the webhook endpoint for both tools. ElevenLabs will POST to your server whenever the agent decides to call a tool.
Embed the Voice Widget on Your Website
ElevenLabs provides a JavaScript widget you can drop into any HTML page.
<elevenlabs-convai agent-id="YOUR_AGENT_ID"></elevenlabs-convai>
<script src="https://elevenlabs.io/convai-widget/index.js" async></script>
Place this snippet where you want the widget to appear — a floating button in the corner works well for scheduling assistants.
Coding agents automate the 5%. Remy runs the 95%.
The bottleneck was never typing the code. It was knowing what to build.
Style and positioning
You can customize the widget’s appearance with CSS or use ElevenLabs’ theme options in the dashboard. For a scheduling-specific agent, consider:
- Triggering it after a user spends 30+ seconds on your pricing or contact page
- Placing it on a dedicated “Book a Call” landing page
- Pre-loading it after a user clicks a CTA button
The widget handles microphone permissions, audio playback, and the WebSocket connection automatically.
Use MindStudio If You Want a No-Code Path
Not everyone wants to manage a backend server. If you’d rather skip the infrastructure and still get a working voice booking agent, MindStudio handles this without any code.
MindStudio’s Agent Skills Plugin is an npm SDK that lets AI agents — including Claude Code — call over 120 typed capabilities as simple method calls. But for teams who don’t want to write code at all, MindStudio’s visual builder lets you create the same kind of scheduling workflow directly in a browser.
You can build a workflow that:
- Accepts incoming voice or text input
- Calls the Cal.com API via MindStudio’s built-in integration layer
- Returns available slots and confirms bookings
- Sends confirmation emails without touching a mail server
MindStudio connects to Cal.com alongside 1,000+ other tools — no API wrangling, no server deployments. The average workflow takes 15 minutes to an hour to build, and you can expose it as a webhook endpoint or embed it directly on your site.
If you’re a developer who wants to use Claude Code but offload the plumbing, MindStudio’s Agent Skills Plugin handles rate limiting, retries, and authentication automatically — so your agent focuses on logic, not infrastructure.
You can try MindStudio free at mindstudio.ai.
Test the Full Flow
Before going live, run through the full booking journey manually.
Test cases to cover
Happy path:
- User asks for availability next week → agent returns slots → user picks one → agent collects name and email → booking confirmed
Edge cases:
- User asks for a time outside your availability hours
- User gives an invalid email format
- Cal.com returns no available slots for the requested window
- Network timeout on the Cal.com API call
Conversation handling:
- User changes their mind mid-booking
- User asks an off-topic question
- User hangs up mid-conversation (no booking created)
For each case, listen to how the agent responds. Adjust your system prompt if the agent is handling any scenario awkwardly.
Check Cal.com confirms bookings correctly
After a successful test booking, verify:
- The booking appears in your Cal.com dashboard
- Both you and the test email receive confirmation emails
- The calendar invite is correctly formatted with the right timezone
Common Mistakes and How to Fix Them
Timezone confusion
Cal.com returns slots in UTC. If you don’t convert these to the user’s local timezone before the agent reads them aloud, you’ll get confused callers.
Fix: detect the user’s timezone via the browser (Intl.DateTimeFormat().resolvedOptions().timeZone) and pass it as a parameter to your availability check. Format the returned slots in local time before the agent speaks them.
The agent books without confirming
If your system prompt isn’t explicit about confirming before booking, the agent may create bookings the user didn’t finalize. This leads to phantom appointments.
Fix: add a line to your system prompt: “Always repeat the proposed time slot back to the user and wait for explicit confirmation before calling create_booking.”
Slot formatting sounds unnatural
Saying “2025-01-20T14:00:00Z” aloud is terrible UX. Your server should format slots into natural language before returning them to the agent.
Fix: in your tool handler, format the response like: "Monday January 20th at 2:00 PM Eastern" rather than returning raw ISO strings.
Missing email validation
If the user gives a malformed email, Cal.com will reject the booking with a 400 error. Your server should validate the email format before calling the API.
Fix: add a simple regex check in your tool handler and return a friendly error message if the format is wrong.
Frequently Asked Questions
Does this work with Google Calendar and Outlook, or only Cal.com?
Cal.com connects to both Google Calendar and Outlook Calendar natively. When you set up your Cal.com account, you can sync it with your existing calendar so availability checks reflect your real schedule. The voice agent interacts with Cal.com’s API, which handles the calendar syncing behind the scenes.
How do I handle multiple hosts or team scheduling?
Cal.com supports round-robin and collective event types for teams. Create a team event type in Cal.com and use its event type ID in your configuration. The availability logic remains the same — Cal.com handles which team member gets assigned the booking.
Can the voice agent reschedule or cancel existing bookings?
Yes, but you’ll need to add two more tools: reschedule_booking and cancel_booking. Both are supported by the Cal.com API. You’ll also need a way for the agent to look up existing bookings by email so it can find the booking ID before modifying it.
What happens if someone books outside my working hours?
Your Cal.com availability settings define what time slots are returned by the /v1/slots endpoint. If a user requests a time outside those hours, the API simply won’t return that slot. The agent should handle an empty slot response gracefully — something like “I don’t see any availability on that day, can you try a different date?”
Is there a cost to run this at scale?
The main costs are ElevenLabs (priced per minute of conversation), your hosting server (usually $5–10/month for a small Node app), and Cal.com (free tier covers most use cases). ElevenLabs’ pricing depends on usage volume — check their current plans for specifics. At moderate traffic (a few hundred calls per month), you’re looking at $20–50/month total.
How do I pass custom data like company name or use case before the call?
You can pass context to the ElevenLabs widget using dynamic variables at initialization. This lets you pre-populate the conversation with data you already have — like the visitor’s name from a CRM or which product page they’re on. Use the dynamic-variables attribute on the widget or pass them via the JavaScript initialization API.
Key Takeaways
- Voice booking agents reduce friction compared to forms — visitors speak naturally and walk away with confirmed appointments
- The core stack is ElevenLabs (voice + conversation) + a Node.js middleware server + Cal.com’s REST API
- Claude Code accelerates the backend build significantly — use it to scaffold, then review the output carefully
- Timezone handling, slot formatting, and confirmation logic are the three areas most likely to cause problems in production
- MindStudio offers a no-code alternative that connects voice agents to Cal.com without backend infrastructure
How Remy works. You talk. Remy ships.
If you want to build this kind of automation without managing a server, MindStudio’s visual builder lets you wire together voice input, API calls, and scheduling logic in a single workflow. It’s free to start and doesn’t require any setup beyond an account.