cd /news/ai-agents/voice-ai-for-appointment-booking-bui… · home topics ai-agents article
[ARTICLE · art-128384] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

voice ai for appointment booking: Build a Vapi + ElevenLabs Agent that books slots and sends reminders 24/7

A developer has published a step-by-step guide for building a voice AI appointment-booking agent that chains Vapi's speech-to-intent engine with ElevenLabs' neural text-to-speech, orchestrated through an n8n workflow connected to Google Calendar and Twilio. The system handles inbound calls, extracts booking intents and slots, creates calendar events, confirms via SMS, and places automated reminder calls, with OpenAI as a fallback when Vapi's intent confidence drops below 0.8. The writeup estimates a 6-8 hour build using free tiers and pay-as-you-go services.

by read9 min views1 publishedSep 13, 2026

You can wire Vapi's speech-to-intent engine to ElevenLabs' neural text-to-speech, then glue the two together with an n8n workflow that talks to Google Calendar and Twilio. The result is a fully-automated "voice AI for appointment booking" that can take calls, create calendar events, confirm via SMS, and call the client back with a reminder generated on the fly.

Tool Plan / Price* Role
Vapi Free tier (up to 5,000 minutes/month) - check the pricing page Speech recognition, intent extraction, phone number provisioning
ElevenLabs Free tier (10,000 characters/month) - check the pricing page High-quality voice synthesis for confirmations & reminders
n8n Self-hosted Docker (free) or Cloud starter $20 / month Orchestrates API calls between Vapi, ElevenLabs, Google Calendar, Twilio, OpenAI
Google Cloud - Calendar API Free tier (up to 1 million calls/month) - check pricing Stores appointment slots, provides event IDs and reminders
Twilio Pay-as-you-go (≈ $0.0085 per outbound call, $0.0075 per SMS) PSTN/SMS gateway for confirmations and reminder calls
OpenAI (optional) Free trial $18, then $0.002 per 1 k tokens Fallback NLU when Vapi intent confidence is low
Calendly (optional) Free plan (basic scheduling) Alternative booking UI if you prefer a web link over Google Calendar

*Prices are accurate as of August 2026; always verify on the provider's pricing page.

Estimated build time: 6-8 hours (account setup ≈ 1 h, n8n workflow ≈ 3 h, testing ≈ 2 h).

Below is a concrete, copy-paste-ready path from "zero" to a production-ready voice AI for appointment booking.

Intent Sample utterances Slots (variables)
book_appointment "I'd like to book a meeting", "Schedule a 30-minute call next Thursday at 2 pm" date ,time ,duration ,name ,phone
reschedule_appointment "Can we move my appointment to Friday?", "Change my booking" event_id ,new_date ,new_time

https://YOUR_N8N_DOMAIN/webhook/vapi-book). Tip: Vapi returns a JSON payload with the extracted slots and a confidence score. Keep the confidence > 0.8 for direct processing; otherwise forward to OpenAI.

EXAMPLE_VOICE_ID). https://YOUR_N8N_DOMAIN/ as an authorized redirect URI. credentials.json and store it in the n8n data folder (/root/.n8n/). Why OAuth? The Calendar API requires a refresh token for long-running workflows; n8n can store it automatically.

docker run -d \
 --name n8n \
 -p 5678:5678 \
 -v ~/.n8n:/root/.n8n \
 n8nio/n8n:latest

http://localhost:5678 and set a strong | Credential | Service | Fields | |---|---|---| | Vapi API | Vapi | apiKey | | ElevenLabs API | ElevenLabs | apiKey | | Google Calendar OAuth2 | Google Calendar | upload credentials.json , authorize the account that owns the target calendar | | Twilio | Twilio | Account SID ,Auth Token ,From number (a purchased Twilio number) |

The workflow consists of three logical parts:

What this does: Accepts the JSON Vapi sends after intent extraction.

{
 "nodes": [
 {
 "parameters": {
 "httpMethod": "POST",
 "path": "vapi-book",
 "responseMode": "onReceived"
 },
 "name": "Vapi Webhook",
 "type": "n8n-nodes-base.webhook",
 "typeVersion": 1,
 "position": [
 250,
 300
 ]
 }
 ]
}

The incoming payload looks like:

{
 "intent": "book_appointment",
 "confidence": 0.93,
 "slots": {
 "date": "2026-09-12",
 "time": "14:00",
 "duration": "30",
 "name": "Jane Doe",
 "phone": "+15551234567"
 }
}

Add a Function node after the webhook to merge date and time into an ISO-8601 string and compute the end time.

// Input: $json.slots
const { date, time, duration } = $json.slots;
const start = new Date(`${date}T${time}:00Z`);
const end = new Date(start.getTime() + Number(duration) * 60000);
return {
 json: {
 ...$json.slots,
 start_iso: start.toISOString(),
 end_iso: end.toISOString()
 }
};

Add a Google Calendar node (Operation: Create). Map fields:

Calendar field Source
summary {{ $json.name }} - Consultation
description "Booked via voice AI."
start.dateTime {{ $json.start_iso }}
end.dateTime {{ $json.end_iso }}
attendees[0].email (optional - if you collect email)
reminders.useDefault false
reminders.overrides[0].method popup
reminders.overrides[0].minutes 10

The node returns an id (Google eventId) we will need for reminders.

Add a Twilio node (Operation: Send SMS).

{{ $json.phone }}

 Hi {{ $json.name }}, your appointment is confirmed for {{ $json.date }} at {{ $json.time }} ({{ $json.duration }} min). Reply STOP to cancel.

https://api.elevenlabs.io/v1/text-to-speech/{{ $env.ELEVENLABS_VOICE_ID }}) xi-api-key: {{ $env.ELEVENLABS_API_KEY }}

{
 "text": "Your appointment is booked for {{ $json.date }} at {{ $json.time }}. We will call you a day before as a reminder.",
 "voice_settings": {
 "stability": 0.75,
 "similarity_boost": 0.85
 }
}

binary (audio/mpeg). The node outputs an MP3 buffer; pipe it straight to Twilio Make Call node.

Add a Twilio node (Operation: Make Call).

<?xml version="1.0" encoding="UTF-8"?>
<Response>
 <Play>{{ $node["ElevenLabs TTS"].json["data"] }}</Play>
</Response>

Why Twiml? Twilio expects a URL or inline XML. n8n can host a temporary endpoint that returns the MP3 as a URL; the above inline <Play> works when you expose the binary data via n8n's Webhook response.

Two approaches:

email or popup 24 h before the event. Below is the Cron approach (runs daily at 08:00 UTC).

{
 "nodes": [
 {
 "parameters": {
 "cronExpression": "0 8 * * *"
 },
 "name": "Daily Reminder Trigger",
 "type": "n8n-nodes-base.cron",
 "typeVersion": 1,
 "position": [100, 100]
 },
 {
 "parameters": {
 "operation": "Search",
 "calendarId": "primary",
 "timeMin": "{{$moment().add(1, 'day').startOf('day').toISOString()}}",
 "timeMax": "{{$moment().add(1, 'day').endOf('day').toISOString()}}",
 "maxResults": 50
 },
 "name": "Find Tomorrow Events",
 "type": "n8n-nodes-base.googleCalendar",
 "typeVersion": 1,
 "position": [300, 100]
 },
 {
 "parameters": {
 "functionCode": "return items.map(item => {\n const ev = item.json;\n return {\n json: {\n phone: ev.attendees?.[0]?.email || ev.description.match(/Phone:\\s*(\\+\\d+)/i)?.[1] || null,\n name: ev.summary.split(' - ')[0],\n date: ev.start.dateTime.split('T')[0],\n time: ev.start.dateTime.split('T')[1].substring(0,5)\n }\n };\n});"
 },
 "name": "Extract Info",
 "type": "n8n-nodes-base.function",
 "typeVersion": 1,
 "position": [500, 100]
 },
 {
 "parameters": {
 "url": "https://api.elevenlabs.io/v1/text-to-speech/{{ $env.ELEVENLABS_VOICE_ID }}",
 "options": {
 "bodyContentType": "json",
 "jsonParameters": true,
 "json": {
 "text": "Hello {{ $json.name }}, this is a reminder that you have an appointment tomorrow at {{ $json.time }}.",
 "voice_settings": {
 "stability": 0.75,
 "similarity_boost": 0.85
 }
 },
 "headers": {
 "xi-api-key": "={{ $env.ELEVENLABS_API_KEY }}"
 }
 },
 "responseFormat": "binary"
 },
 "name": "ElevenLabs Reminder TTS",
 "type": "n8n-nodes-base.httpRequest",
 "typeVersion": 1,
 "position": [700, 100]
 },
 {
 "parameters": {
 "to": "={{ $json.phone }}",
 "from": "{{ $env.TWILIO_NUMBER }}",
 "twiml": "<?xml version='1.0' encoding='UTF-8'?><Response><Play>{{ $node[\"ElevenLabs Reminder TTS\"].json[\"data\"] }}</Play></Response>"
 },
 "name": "Call Reminder",
 "type": "n8n-nodes-base.twilio",
 "typeVersion": 1,
 "position": [900, 100]
 }
 ],
 "connections": {
 "Daily Reminder Trigger": {
 "main": [
 [
 {
 "node": "Find Tomorrow Events",
 "type": "main",
 "index": 0
 }
 ]
 ]
 },
 "Find Tomorrow Events": {
 "main": [
 [
 {
 "node": "Extract Info",
 "type": "main",
 "index": 0
 }
 ]
 ]
 },
 "Extract Info": {
 "main": [
 [
 {
 "node": "ElevenLabs Reminder TTS",
 "type": "main",
 "index": 0
 }
 ]
 ]
 },
 "ElevenLabs Reminder TTS": {
 "main": [
 [
 {
 "node": "Call Reminder",
 "type": "main",
 "index": 0
 }
 ]
 ]
 }
 }
}

Key point: The workflow pulls the phone number from the event's description; you can also store it in a custom extendedProperties field during the booking step.

Return to the Vapi console, edit the agent's Webhook URL, and paste the publicly reachable n8n endpoint (e.g., https://YOUR_N8N_DOMAIN/webhook/vapi-book). Enable "Send webhook on every intent".

book_appointment, forwards the payload to n8n. If anything fails, check the Execution Log in n8n - each node's output is stored for debugging.

Failure mode Symptom Fix / Mitigation
Vapi minute quota Calls drop after a few hundred minutes in a month. Monitor usage in the Vapi dashboard; upgrade to a paid plan before hitting the free 5,000-minute ceiling.
ElevenLabs character limit TTS API returns 429 Too Many Requests . Cache repeated confirmation sentences; combine multiple sentences into a single request; purchase additional quota if needed.
Google OAuth token expiry "Invalid Credentials" error from Calendar node after ~1 hour. n8n's OAuth2 credential automatically refreshes if the refresh token is saved; ensure you selected**"Access type: offline"** when creating the client ID.
Twilio cost surprise Unexpected $ per-call charges after a volume spike. Set a daily spend limit in the Twilio console; log each call in n8n and the workflow if cost exceeds a threshold.
Timezone drift Appointments end up an hour early/late. Store and convert all dates in UTC ( Z suffix). Use themoment-timezone library in Function nodes if you need a specific zone (e.g.,America/New_York ).
Webhook not reachable n8n returns 404 or "Connection refused". Expose n8n behind a trusted domain with TLS (Let's Encrypt). Use a tunneling service (ngrok) only for dev; never in production.
Edge-case speech Caller says "next Monday" but Vapi extracts a wrong date. Add a fallback to OpenAI: if confidence < 0.8 , forward the raw transcript togpt-4o-mini with a prompt to parse a date.

Bottom line: The most common production blocker is auth token expiry. Keep the Google Calendar OAuth credentials refreshed automatically and store them in n8n's credential manager - that alone eliminates 80 % of runtime errors.

For a deeper technical reference, see n8n's documentation.

Yes. Instead of the Google Calendar node, use an HTTP Request node that calls Calendly's Create Scheduled Event endpoint (https://api.calendly.com/scheduled_events). You'll need a Calendly API key (found in Integrations → API). The rest of the workflow (SMS, TTS) stays unchanged.

Vapi returns the spoken time in the caller's local context, but it does not tag a zone. You must ask the caller for their city or use the phone number's country code to infer a default offset. In a Function node you can apply moment.tz(dateTime, "America/Los_Angeles") before sending the ISO string to Google Calendar.

The Vapi webhook fires as soon as intent extraction completes, regardless of call state. The workflow continues in n8n, creates the event, and sends an SMS. If you need a "call-back-only" flow, set a flag in the intent payload and branch to a no-action path that waits for the user to call back.

No. If you prefer a managed service, n8n Cloud starts at $20 / month and includes automatic SSL, scaling, and built-in credential storage. The self-hosted Docker image is free but you must manage updates and security patches yourself.

Add a Header Authentication node right after the webhook that checks for a custom X-Secret header (store the secret as an environment variable). Reject any request that doesn't match with a 401 response.

If you're hunting for more monetizable ideas, check out AI automations you can sell. And when you're ready to dive deeper, the free guide walks you through scaling voice AI agents from a single prototype to a multi-tenant SaaS.

Happy building - the world needs a voice AI for appointment booking that actually works, not just hype.

── more in #ai-agents 4 stories · sorted by recency
── more on @vapi 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/voice-ai-for-appoint…] indexed:0 read:9min 2026-09-13 ·