{"slug": "voice-ai-for-appointment-booking-build-a-vapi-elevenlabs-agent-that-books-slots", "title": "voice ai for appointment booking: Build a Vapi + ElevenLabs Agent that books slots and sends reminders 24/7", "summary": "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.", "body_md": "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.\n\n| Tool | Plan / Price* | Role | \n|---|---|---|\n| **Vapi** | Free tier (up to 5,000 minutes/month) - check the pricing page | Speech recognition, intent extraction, phone number provisioning | \n| **ElevenLabs** | Free tier (10,000 characters/month) - check the pricing page | High-quality voice synthesis for confirmations & reminders | \n| **n8n** | Self-hosted Docker (free) **or** Cloud starter $20 / month | Orchestrates API calls between Vapi, ElevenLabs, Google Calendar, Twilio, OpenAI | \n| **Google Cloud - Calendar API** | Free tier (up to 1 million calls/month) - check pricing | Stores appointment slots, provides event IDs and reminders | \n| **Twilio** | Pay-as-you-go (≈ $0.0085 per outbound call, $0.0075 per SMS) | PSTN/SMS gateway for confirmations and reminder calls | \n| **OpenAI (optional)** | Free trial $18, then $0.002 per 1 k tokens | Fallback NLU when Vapi intent confidence is low | \n| **Calendly (optional)** | Free plan (basic scheduling) | Alternative booking UI if you prefer a web link over Google Calendar | \n\n*Prices are accurate as of August 2026; always verify on the provider's pricing page.\n\n**Estimated build time:** 6-8 hours (account setup ≈ 1 h, n8n workflow ≈ 3 h, testing ≈ 2 h).\n\nBelow is a concrete, copy-paste-ready path from \"zero\" to a production-ready voice AI for appointment booking.\n\n| Intent | Sample utterances | Slots (variables) | \n|---|---|---|\n| `book_appointment` | \"I'd like to book a meeting\", \"Schedule a 30-minute call next Thursday at 2 pm\" | `date` ,`time` ,`duration` ,`name` ,`phone` | \n| `reschedule_appointment` | \"Can we move my appointment to Friday?\", \"Change my booking\" | `event_id` ,`new_date` ,`new_time` | \n\n`https://YOUR_N8N_DOMAIN/webhook/vapi-book`).\n**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.\n\n`EXAMPLE_VOICE_ID`). `https://YOUR_N8N_DOMAIN/` as an authorized redirect URI. `credentials.json` and store it in the n8n data folder (`/root/.n8n/`).\n**Why OAuth?** The Calendar API requires a refresh token for long-running workflows; n8n can store it automatically.\n\n```\n# Pull the official n8n Docker image (latest tag as of Aug 2026)\ndocker run -d \\\n --name n8n \\\n -p 5678:5678 \\\n -v ~/.n8n:/root/.n8n \\\n n8nio/n8n:latest\n```\n\n`http://localhost:5678` and set a strong | Credential | Service | Fields | \n|---|---|---|\n| `Vapi API` | Vapi | `apiKey` | \n| `ElevenLabs API` | ElevenLabs | `apiKey` | \n| `Google Calendar OAuth2` | Google Calendar | upload `credentials.json` , authorize the account that owns the target calendar | \n| `Twilio` | Twilio | `Account SID` ,`Auth Token` ,`From` number (a purchased Twilio number) | \n\nThe workflow consists of three logical parts:\n\n**What this does:** Accepts the JSON Vapi sends after intent extraction.\n\n```\n{\n \"nodes\": [\n {\n \"parameters\": {\n \"httpMethod\": \"POST\",\n \"path\": \"vapi-book\",\n \"responseMode\": \"onReceived\"\n },\n \"name\": \"Vapi Webhook\",\n \"type\": \"n8n-nodes-base.webhook\",\n \"typeVersion\": 1,\n \"position\": [\n 250,\n 300\n ]\n }\n ]\n}\n```\n\nThe incoming payload looks like:\n\n```\n{\n \"intent\": \"book_appointment\",\n \"confidence\": 0.93,\n \"slots\": {\n \"date\": \"2026-09-12\",\n \"time\": \"14:00\",\n \"duration\": \"30\",\n \"name\": \"Jane Doe\",\n \"phone\": \"+15551234567\"\n }\n}\n```\n\nAdd a **Function** node after the webhook to merge `date` and `time` into an ISO-8601 string and compute the end time.\n\n``` js\n// Input: $json.slots\nconst { date, time, duration } = $json.slots;\nconst start = new Date(`${date}T${time}:00Z`);\nconst end = new Date(start.getTime() + Number(duration) * 60000);\nreturn {\n json: {\n ...$json.slots,\n start_iso: start.toISOString(),\n end_iso: end.toISOString()\n }\n};\n```\n\nAdd a **Google Calendar** node (Operation: *Create*). Map fields:\n\n| Calendar field | Source | \n|---|---|\n| `summary` | `{{ $json.name }} - Consultation` | \n| `description` | \"Booked via voice AI.\" | \n| `start.dateTime` | `{{ $json.start_iso }}` | \n| `end.dateTime` | `{{ $json.end_iso }}` | \n| `attendees[0].email` | (optional - if you collect email) | \n| `reminders.useDefault` | `false` | \n| `reminders.overrides[0].method` | `popup` | \n| `reminders.overrides[0].minutes` | `10` | \n\nThe node returns an `id` (Google `eventId`) we will need for reminders.\n\nAdd a **Twilio** node (Operation: *Send SMS*). \n\n`{{ $json.phone }}` \n\n```\n Hi {{ $json.name }}, your appointment is confirmed for {{ $json.date }} at {{ $json.time }} ({{ $json.duration }} min). Reply STOP to cancel.\n```\n\n`https://api.elevenlabs.io/v1/text-to-speech/{{ $env.ELEVENLABS_VOICE_ID }}`) `xi-api-key: {{ $env.ELEVENLABS_API_KEY }}` \n\n```\n{\n \"text\": \"Your appointment is booked for {{ $json.date }} at {{ $json.time }}. We will call you a day before as a reminder.\",\n \"voice_settings\": {\n \"stability\": 0.75,\n \"similarity_boost\": 0.85\n }\n}\n```\n\n`binary` (audio/mpeg). The node outputs an MP3 buffer; pipe it straight to Twilio **Make Call** node.\n\nAdd a **Twilio** node (Operation: *Make Call*). \n\n```\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Response>\n <Play>{{ $node[\"ElevenLabs TTS\"].json[\"data\"] }}</Play>\n</Response>\n```\n\n**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.\n\nTwo approaches:\n\n`email` or `popup` 24 h before the event. Below is the **Cron** approach (runs daily at 08:00 UTC).\n\n```\n{\n \"nodes\": [\n {\n \"parameters\": {\n \"cronExpression\": \"0 8 * * *\"\n },\n \"name\": \"Daily Reminder Trigger\",\n \"type\": \"n8n-nodes-base.cron\",\n \"typeVersion\": 1,\n \"position\": [100, 100]\n },\n {\n \"parameters\": {\n \"operation\": \"Search\",\n \"calendarId\": \"primary\",\n \"timeMin\": \"{{$moment().add(1, 'day').startOf('day').toISOString()}}\",\n \"timeMax\": \"{{$moment().add(1, 'day').endOf('day').toISOString()}}\",\n \"maxResults\": 50\n },\n \"name\": \"Find Tomorrow Events\",\n \"type\": \"n8n-nodes-base.googleCalendar\",\n \"typeVersion\": 1,\n \"position\": [300, 100]\n },\n {\n \"parameters\": {\n \"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});\"\n },\n \"name\": \"Extract Info\",\n \"type\": \"n8n-nodes-base.function\",\n \"typeVersion\": 1,\n \"position\": [500, 100]\n },\n {\n \"parameters\": {\n \"url\": \"https://api.elevenlabs.io/v1/text-to-speech/{{ $env.ELEVENLABS_VOICE_ID }}\",\n \"options\": {\n \"bodyContentType\": \"json\",\n \"jsonParameters\": true,\n \"json\": {\n \"text\": \"Hello {{ $json.name }}, this is a reminder that you have an appointment tomorrow at {{ $json.time }}.\",\n \"voice_settings\": {\n \"stability\": 0.75,\n \"similarity_boost\": 0.85\n }\n },\n \"headers\": {\n \"xi-api-key\": \"={{ $env.ELEVENLABS_API_KEY }}\"\n }\n },\n \"responseFormat\": \"binary\"\n },\n \"name\": \"ElevenLabs Reminder TTS\",\n \"type\": \"n8n-nodes-base.httpRequest\",\n \"typeVersion\": 1,\n \"position\": [700, 100]\n },\n {\n \"parameters\": {\n \"to\": \"={{ $json.phone }}\",\n \"from\": \"{{ $env.TWILIO_NUMBER }}\",\n \"twiml\": \"<?xml version='1.0' encoding='UTF-8'?><Response><Play>{{ $node[\\\"ElevenLabs Reminder TTS\\\"].json[\\\"data\\\"] }}</Play></Response>\"\n },\n \"name\": \"Call Reminder\",\n \"type\": \"n8n-nodes-base.twilio\",\n \"typeVersion\": 1,\n \"position\": [900, 100]\n }\n ],\n \"connections\": {\n \"Daily Reminder Trigger\": {\n \"main\": [\n [\n {\n \"node\": \"Find Tomorrow Events\",\n \"type\": \"main\",\n \"index\": 0\n }\n ]\n ]\n },\n \"Find Tomorrow Events\": {\n \"main\": [\n [\n {\n \"node\": \"Extract Info\",\n \"type\": \"main\",\n \"index\": 0\n }\n ]\n ]\n },\n \"Extract Info\": {\n \"main\": [\n [\n {\n \"node\": \"ElevenLabs Reminder TTS\",\n \"type\": \"main\",\n \"index\": 0\n }\n ]\n ]\n },\n \"ElevenLabs Reminder TTS\": {\n \"main\": [\n [\n {\n \"node\": \"Call Reminder\",\n \"type\": \"main\",\n \"index\": 0\n }\n ]\n ]\n }\n }\n}\n```\n\n**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.\n\nReturn 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\"**.\n\n`book_appointment`, forwards the payload to n8n. If anything fails, check the **Execution Log** in n8n - each node's output is stored for debugging.\n\n| Failure mode | Symptom | Fix / Mitigation | \n|---|---|---|\n| **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. | \n| **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. | \n| **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. | \n| **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 pause the workflow if cost exceeds a threshold. | \n| **Timezone drift** | Appointments end up an hour early/late. | Store and convert all dates in UTC ( `Z` suffix). Use the`moment-timezone` library in Function nodes if you need a specific zone (e.g.,`America/New_York` ). | \n| **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. | \n| **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 to`gpt-4o-mini` with a prompt to parse a date. | \n\n**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.\n\nFor a deeper technical reference, see [n8n's documentation](https://docs.n8n.io/).\n\nYes. 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. \n\nVapi 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. \n\nThe 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. \n\nNo. 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.\n\nAdd 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. \n\nIf you're hunting for more monetizable ideas, check out **[AI automations you can sell](https://getaab.com/ai-automations-to-sell)**. And when you're ready to dive deeper, the **[free guide](https://getaab.com/free)** walks you through scaling voice AI agents from a single prototype to a multi-tenant SaaS. \n\nHappy building - the world needs a voice AI for appointment booking that actually works, not just hype.", "url": "https://wpnews.pro/news/voice-ai-for-appointment-booking-build-a-vapi-elevenlabs-agent-that-books-slots", "canonical_source": "https://dev.to/samchenreviews/voice-ai-for-appointment-booking-build-a-vapi-elevenlabs-agent-that-books-slots-and-sends-2mge", "published_at": "2026-09-13 16:30:26+00:00", "updated_at": "2026-09-13 16:44:14.966096+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "natural-language-processing", "developer-tools", "ai-products"], "entities": ["Vapi", "ElevenLabs", "n8n", "Google Calendar", "Twilio", "OpenAI", "Calendly", "Google Cloud"], "alternates": {"html": "https://wpnews.pro/news/voice-ai-for-appointment-booking-build-a-vapi-elevenlabs-agent-that-books-slots", "markdown": "https://wpnews.pro/news/voice-ai-for-appointment-booking-build-a-vapi-elevenlabs-agent-that-books-slots.md", "text": "https://wpnews.pro/news/voice-ai-for-appointment-booking-build-a-vapi-elevenlabs-agent-that-books-slots.txt", "jsonld": "https://wpnews.pro/news/voice-ai-for-appointment-booking-build-a-vapi-elevenlabs-agent-that-books-slots.jsonld"}}