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

> Source: <https://dev.to/samchenreviews/voice-ai-for-appointment-booking-build-a-vapi-elevenlabs-agent-that-books-slots-and-sends-2mge>
> Published: 2026-09-13 16:30:26+00:00

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.

```
# Pull the official n8n Docker image (latest tag as of Aug 2026)
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.

``` js
// 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 pause 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 the`moment-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 to`gpt-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](https://docs.n8n.io/).

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](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. 

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