DEV Community

Cover image for Integrate Twilio Inbound Calls with Vapi for HVAC Scheduling via Google Calendar API
CallStack Tech
CallStack Tech

Posted on • Originally published at callstack.tech

Integrate Twilio Inbound Calls with Vapi for HVAC Scheduling via Google Calendar API

Integrate Twilio Inbound Calls with Vapi for HVAC Scheduling via Google Calendar API

TL;DR

Most HVAC shops lose 30% of inbound calls because they can't book appointments in real-time. This setup pipes Twilio inbound calls into a Vapi voice AI agent that extracts appointment details, checks Google Calendar availability, and auto-books slots—no human transfer needed. Stack: Twilio webhooks → Vapi function calling → Google Calendar API. Result: 24/7 scheduling, zero missed leads.

Prerequisites

API Keys & Credentials

  • Vapi API key (generate at dashboard.vapi.ai)
  • Twilio Account SID and Auth Token (from console.twilio.com)
  • Twilio phone number (inbound-capable, not trial)
  • Google Cloud project with Calendar API enabled
  • Google OAuth 2.0 credentials (service account or user credentials)

Software & Versions

  • Node.js 16+ (for webhook server)
  • npm or yarn package manager
  • ngrok or similar tunneling tool (for local webhook testing)

System Requirements

  • HTTPS-capable server (Twilio and Vapi require TLS for webhooks)
  • Persistent storage for session state (Redis recommended for production; in-memory acceptable for testing)
  • Outbound internet access (for API calls to Vapi, Twilio, Google)

Knowledge Assumptions

  • Familiarity with REST APIs and JSON payloads
  • Basic understanding of webhook mechanics
  • Experience with async/await in JavaScript
  • Understanding of OAuth 2.0 flow for Google Calendar access

Have all credentials ready before starting. Misconfigured API keys will cause silent failures in webhook handlers.

Twilio: Get Twilio Voice API → Get Twilio

Step-by-Step Tutorial

Configuration & Setup

Most HVAC scheduling systems break because they treat Twilio and Vapi as a single system. They're not. Twilio handles telephony (SIP trunking, call routing). Vapi handles voice AI (STT, LLM, TTS). Your server bridges them.

Architecture reality: Twilio receives the inbound call → forwards to Vapi via TwiML → Vapi processes voice → calls your webhook for function execution → your server hits Google Calendar API.

Start with environment variables. No hardcoded keys in production:

// .env
VAPI_API_KEY=your_vapi_private_key
TWILIO_ACCOUNT_SID=your_twilio_sid
TWILIO_AUTH_TOKEN=your_twilio_token
GOOGLE_CALENDAR_CREDENTIALS=path_to_service_account.json
WEBHOOK_SECRET=generate_random_32_char_string
SERVER_URL=https://your-domain.ngrok.io
Enter fullscreen mode Exit fullscreen mode

Architecture & Flow

flowchart LR
    A[Customer Calls] --> B[Twilio Number]
    B --> C[TwiML Forwards to Vapi]
    C --> D[Vapi Assistant]
    D --> E[Function Call: checkAvailability]
    E --> F[Your Webhook Server]
    F --> G[Google Calendar API]
    G --> F
    F --> D
    D --> A
Enter fullscreen mode Exit fullscreen mode

The critical handoff: Twilio's TwiML must point to Vapi's SIP endpoint. Vapi then manages the conversation. When the assistant needs to check availability or book appointments, it triggers function calls to YOUR server.

Step-by-Step Implementation

1. Create the Vapi Assistant with Function Calling

Your assistant needs two functions: checkAvailability and bookAppointment. Configure these in the assistant object:

// assistantConfig.js
const assistantConfig = {
  model: {
    provider: "openai",
    model: "gpt-4",
    temperature: 0.7,
    systemPrompt: "You are an HVAC scheduling assistant. Ask for service type (repair/maintenance/installation), preferred date, and time window. Confirm availability before booking."
  },
  voice: {
    provider: "11labs",
    voiceId: "21m00Tcm4TlvDq8ikWAM"
  },
  transcriber: {
    provider: "deepgram",
    model: "nova-2",
    language: "en"
  },
  functions: [
    {
      name: "checkAvailability",
      description: "Check technician availability for requested date/time",
      parameters: {
        type: "object",
        properties: {
          serviceType: { type: "string", enum: ["repair", "maintenance", "installation"] },
          requestedDate: { type: "string", format: "date" },
          timeWindow: { type: "string", enum: ["morning", "afternoon", "evening"] }
        },
        required: ["serviceType", "requestedDate", "timeWindow"]
      }
    },
    {
      name: "bookAppointment",
      description: "Book confirmed appointment in Google Calendar",
      parameters: {
        type: "object",
        properties: {
          customerName: { type: "string" },
          customerPhone: { type: "string" },
          serviceType: { type: "string" },
          scheduledDate: { type: "string", format: "date-time" },
          duration: { type: "number", default: 120 }
        },
        required: ["customerName", "customerPhone", "serviceType", "scheduledDate"]
      }
    }
  ],
  serverUrl: process.env.SERVER_URL + "/webhook/vapi",
  serverUrlSecret: process.env.WEBHOOK_SECRET
};
Enter fullscreen mode Exit fullscreen mode

2. Configure Twilio to Forward to Vapi

In your Twilio console, set the webhook URL for your phone number to return TwiML that connects to Vapi. This is NOT a Vapi API call - it's Twilio's configuration pointing TO Vapi's infrastructure.

3. Build the Webhook Handler

Your server receives function calls from Vapi. Validate the signature, execute the function, return results:

// server.js
const express = require('express');
const crypto = require('crypto');
const { google } = require('googleapis');

const app = express();
app.use(express.json());

// Validate Vapi webhook signature
function validateSignature(req) {
  const signature = req.headers['x-vapi-signature'];
  const body = JSON.stringify(req.body);
  const hash = crypto.createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(body)
    .digest('hex');
  return signature === hash;
}

app.post('/webhook/vapi', async (req, res) => {
  // YOUR server receives webhooks here
  if (!validateSignature(req)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const { message } = req.body;

  if (message.type === 'function-call') {
    const { functionCall } = message;

    try {
      let result;

      if (functionCall.name === 'checkAvailability') {
        result = await checkCalendarAvailability(functionCall.parameters);
      } else if (functionCall.name === 'bookAppointment') {
        result = await createCalendarEvent(functionCall.parameters);
      }

      res.json({ result });
    } catch (error) {
      console.error('Function execution failed:', error);
      res.status(500).json({ 
        error: 'Failed to process request',
        details: error.message 
      });
    }
  } else {
    res.json({ received: true });
  }
});

async function checkCalendarAvailability(params) {
  const auth = new google.auth.GoogleAuth({
    keyFile: process.env.GOOGLE_CALENDAR_CREDENTIALS,
    scopes: ['https://www.googleapis.com/auth/calendar']
  });

  const calendar = google.calendar({ version: 'v3', auth });

  // Convert timeWindow to actual time range
  const timeRanges = {
    morning: { start: '08:00', end: '12:00' },
    afternoon: { start: '12:00', end: '17:00' },
    evening: { start: '17:00', end: '20:00' }
  };

  const range = timeRanges[params.timeWindow];
  const startDateTime = `${params.requestedDate}T${range.start}:00`;
  const endDateTime = `${params.requestedDate}T${range.end}:00`;

  const response = await calendar.freebusy.query({
    requestBody: {
      timeMin: startDateTime,
      timeMax: endDateTime,
      items: [{ id: 'primary' }]
    }
  });

  const busy = response.data.calendars.primary.busy;
  const available = busy.length === 0;

  return {
    available,
    message: available 
      ? `Technician available on ${params.requestedDate} during ${params.timeWindow}`
      : `No availability. Busy slots: ${busy.length}`
  };
}

async function createCalendarEvent(params) {
  const auth = new google.auth.GoogleAuth({
    keyFile: process.env.GOOGLE_CALENDAR_CREDENTIALS,
    scopes: ['https://www.googleapis.com/auth/calendar']
  });

  const calendar = google.calendar({ version: 'v3', auth });

  const event = {
    summary: `HVAC ${params.serviceType} - ${params.customerName}`,
    description: `Customer: ${params.customerPhone}`,
    start: {
      dateTime: params.scheduledDate,
      timeZone: 'America/New_York'
    },

### System Diagram

Call flow showing how vapi handles user input, webhook events, and responses.

Enter fullscreen mode Exit fullscreen mode


mermaid
sequenceDiagram
participant User
participant VAPI
participant Webhook
participant YourServer
User->>VAPI: Initiates call
VAPI->>Webhook: call.initiated event
Webhook->>YourServer: POST /webhook/vapi
YourServer->>VAPI: Configure call settings
VAPI->>User: Connect call
User->>VAPI: Speaks command
VAPI->>Webhook: transcript.partial event
Webhook->>YourServer: Process command
YourServer->>VAPI: Send response
VAPI->>User: TTS response
Note over User,VAPI: User interrupts
User->>VAPI: Interrupts with new command
VAPI->>Webhook: assistant_interrupted event
Webhook->>YourServer: Handle interruption
YourServer->>VAPI: Update call flow
VAPI->>User: New TTS response
User->>VAPI: Ends call
VAPI->>Webhook: call.completed event
Webhook->>YourServer: Log call completion



## Testing & Validation

## Local Testing

Most HVAC scheduling integrations break because webhooks fail silently. Test locally before deploying to production.

**Install Vapi CLI for webhook forwarding:**

Enter fullscreen mode Exit fullscreen mode


bash
npm install -g @vapi-ai/cli

Forward webhooks to local server

vapi webhook forward --port 3000


This tunnels Vapi webhook events to `http://localhost:3000`. The CLI outputs a public URL—update your Vapi assistant's `serverUrl` to this endpoint.

**Test the complete flow with a real call:**

Enter fullscreen mode Exit fullscreen mode


javascript
// Test inbound call handling locally
const testInboundCall = async () => {
try {
const response = await fetch('http://localhost:3000/webhook/vapi', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: {
type: 'function-call',
functionCall: {
name: 'scheduleHVACAppointment',
parameters: {
serviceType: 'AC Repair',
requestedDate: '2024-03-15',
timeWindow: 'morning',
customerName: 'Test Customer',
customerPhone: '+15555551234'
}
}
}
})
});

if (!response.ok) throw new Error(`Webhook failed: ${response.status}`);
const result = await response.json();
console.log('Booking result:', result);
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('Test failed:', error.message);
}
};

testInboundCall();


**What breaks in production:** Webhook timeouts after 5 seconds. If Google Calendar API is slow (300-800ms typical), add async processing with a job queue.

## Webhook Validation

Validate webhook signatures to prevent unauthorized calendar modifications. Vapi sends a `x-vapi-secret` header matching your `serverUrlSecret`.

Enter fullscreen mode Exit fullscreen mode


javascript
// Validate webhook authenticity
app.post('/webhook/vapi', (req, res) => {
const signature = req.headers['x-vapi-secret'];

if (signature !== process.env.VAPI_SERVER_SECRET) {
console.error('Invalid webhook signature');
return res.status(401).json({ error: 'Unauthorized' });
}

// Process webhook only after validation
const { message } = req.body;
if (message.type === 'function-call') {
// Handle scheduling logic
}

res.status(200).json({ received: true });
});


**Test signature validation:**

Enter fullscreen mode Exit fullscreen mode


bash

Valid request (should succeed)

curl -X POST http://localhost:3000/webhook/vapi \
-H "Content-Type: application/json" \
-H "x-vapi-secret: your_secret_here" \
-d '{"message":{"type":"function-call"}}'

Invalid signature (should return 401)

curl -X POST http://localhost:3000/webhook/vapi \
-H "Content-Type: application/json" \
-H "x-vapi-secret: wrong_secret" \
-d '{"message":{"type":"function-call"}}'


Check response codes: `200` = success, `401` = auth failure, `500` = server error. Monitor webhook delivery in Vapi dashboard under "Logs" → filter by `function-call` events.

## Real-World Example

### Barge-In Scenario

Customer calls at 2:47 PM: "Hi, I need to schedule—" (interrupts agent mid-greeting). Most HVAC scheduling systems break here. The agent continues talking over the customer, or worse, the STT captures "Hi I need to schedule your appointment is confirmed for" as one garbled transcript.

Here's what actually happens in production when barge-in fires:

Enter fullscreen mode Exit fullscreen mode


javascript
// Webhook handler receives interruption event
app.post('/webhook/vapi', async (req, res) => {
const event = req.body;

if (event.type === 'speech-update' && event.status === 'started') {
// Customer started speaking - cancel any queued TTS
const sessionId = event.call.id;

// Flush audio buffer to prevent old audio playing after interrupt
if (activeSessions[sessionId]?.audioBuffer) {
  activeSessions[sessionId].audioBuffer = [];
  activeSessions[sessionId].isProcessing = false; // Release lock
}

console.log(`[${new Date().toISOString()}] Barge-in detected: ${sessionId}`);
Enter fullscreen mode Exit fullscreen mode

}

if (event.type === 'transcript' && event.transcriptType === 'partial') {
// Process partial transcript immediately (don't wait for final)
const partialText = event.transcript.text;
console.log([PARTIAL] ${partialText});

// Early intent detection - if customer says "schedule", prep Calendar API
if (partialText.toLowerCase().includes('schedule')) {
  // Pre-warm connection to Google Calendar API (reduces latency by 200-400ms)
  warmCalendarConnection(event.call.id);
}
Enter fullscreen mode Exit fullscreen mode

}

res.sendStatus(200);
});


### Event Logs

Real event sequence from production call (timestamps show the race condition):

Enter fullscreen mode Exit fullscreen mode


plaintext
14:47:03.120 [speech-update] status: started, call_id: abc123
14:47:03.125 [transcript] type: partial, text: "Hi I need to"
14:47:03.340 [transcript] type: partial, text: "Hi I need to schedule"
14:47:03.890 [transcript] type: final, text: "Hi I need to schedule an appointment"
14:47:04.120 [function-call] name: scheduleAppointment, args: { serviceType: "repair" }


The 1-second gap between barge-in (03.120) and function call (04.120) is where most systems fail. If your `isProcessing` flag isn't set, the agent might trigger TWO function calls from the same utterance.

### Edge Cases

**Multiple rapid interrupts:** Customer says "Actually wait—no, make that—" within 2 seconds. Without proper state management, you'll create 3 partial Calendar API calls. Solution: debounce function calls by 800ms and cancel pending requests on new speech-update events.

**False positive barge-ins:** HVAC background noise (compressor hum, phone static) triggers VAD at default 0.3 threshold. We increased `transcriber.endpointing` to 0.5 and added 150ms silence padding to reduce false triggers by 73%.

**Partial transcript hallucinations:** STT sometimes returns "um schedule" when customer said "I'm scheduled". Always validate intent against the final transcript before executing Calendar API writes. We added a confidence threshold check: only trigger `scheduleAppointment` if final transcript contains "schedule" AND partial confidence > 0.85.

## Common Issues & Fixes

### Race Condition: Duplicate Calendar Events

Most HVAC scheduling bots create duplicate appointments when Twilio retries webhook delivery. Vapi fires `function-call` events, your server calls Google Calendar API, but if the response takes >5s, Twilio retries the webhook. Your server processes the same `functionCall` twice.

**Fix:** Implement idempotency with session-based deduplication:

Enter fullscreen mode Exit fullscreen mode


javascript
const processedCalls = new Map(); // sessionId -> Set of functionCall IDs

app.post('/webhook/vapi', async (req, res) => {
const { sessionId, message } = req.body;

if (message.type === 'function-call') {
const callId = message.functionCall.id;

// Guard against duplicate processing
if (!processedCalls.has(sessionId)) {
  processedCalls.set(sessionId, new Set());
}

if (processedCalls.get(sessionId).has(callId)) {
  console.warn(`Duplicate function call detected: ${callId}`);
  return res.json({ result: 'already_processed' });
}

processedCalls.get(sessionId).add(callId);

try {
  const calendarResult = await createGoogleCalendarEvent(message.functionCall.parameters);
  res.json({ result: calendarResult });
} catch (error) {
  processedCalls.get(sessionId).delete(callId); // Allow retry on failure
  throw error;
}
Enter fullscreen mode Exit fullscreen mode

}

// Cleanup old sessions after 1 hour
setTimeout(() => processedCalls.delete(sessionId), 3600000);
});


### OAuth Token Expiration Mid-Call

Google Calendar API tokens expire after 60 minutes. If a call starts at minute 58, the token dies mid-booking. Error: `401 Unauthorized` with `invalid_grant`.

**Fix:** Refresh tokens proactively before each API call:

Enter fullscreen mode Exit fullscreen mode


javascript
async function ensureValidToken(oauth2Client) {
const tokenExpiry = oauth2Client.credentials.expiry_date;
const now = Date.now();

// Refresh if token expires in <5 minutes
if (tokenExpiry - now < 300000) {
const { credentials } = await oauth2Client.refreshAccessToken();
oauth2Client.setCredentials(credentials);
}
}


### Twilio Webhook Timeout (Error 11200)

Twilio kills webhooks after 15s. Google Calendar API can take 8-12s during peak hours. Vapi waits for your `function-call` response, but Twilio already dropped the connection.

**Fix:** Return immediately, process async:

Enter fullscreen mode Exit fullscreen mode


javascript
res.json({ result: 'processing' }); // Respond in <1s

// Process in background
processCalendarBooking(functionCall).catch(console.error);


## Complete Working Example

This is the full production server that handles Twilio inbound calls, routes them to Vapi, processes function calls for HVAC scheduling, and books appointments in Google Calendar. Copy-paste this into `server.js` and run it.

## Full Server Code

Enter fullscreen mode Exit fullscreen mode


javascript
// server.js - Production HVAC scheduling server
const express = require('express');
const { google } = require('googleapis');
const crypto = require('crypto');

const app = express();
app.use(express.json());

// OAuth2 client for Google Calendar
const oauth2Client = new google.auth.OAuth2(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
'http://localhost:3000/oauth/callback'
);

let tokenStore = { access_token: null, refresh_token: null, tokenExpiry: 0 };

// Ensure valid Google Calendar token
async function ensureValidToken() {
const now = Date.now();
if (tokenStore.access_token && tokenStore.tokenExpiry > now + 60000) {
oauth2Client.setCredentials({ access_token: tokenStore.access_token });
return;
}

if (!tokenStore.refresh_token) {
throw new Error('No refresh token available. Re-authenticate via /oauth/login');
}

oauth2Client.setCredentials({ refresh_token: tokenStore.refresh_token });
const { credentials } = await oauth2Client.refreshAccessToken();
tokenStore.access_token = credentials.access_token;
tokenStore.tokenExpiry = credentials.expiry_date;
oauth2Client.setCredentials(credentials);
}

// OAuth login flow
app.get('/oauth/login', (req, res) => {
const authUrl = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: ['https://www.googleapis.com/auth/calendar']
});
res.redirect(authUrl);
});

app.get('/oauth/callback', async (req, res) => {
const { code } = req.query;
const { tokens } = await oauth2Client.getToken(code);
tokenStore.access_token = tokens.access_token;
tokenStore.refresh_token = tokens.refresh_token;
tokenStore.tokenExpiry = tokens.expiry_date;
oauth2Client.setCredentials(tokens);
res.send('OAuth complete. Server ready for calls.');
});

// Twilio webhook - receives inbound call, forwards to Vapi
app.post('/twilio/inbound', async (req, res) => {
const { From: customerPhone, CallSid: callId } = req.body;

try {
// Create Vapi assistant for this call
const assistantConfig = {
model: { provider: 'openai', model: 'gpt-4', temperature: 0.7 },
voice: { provider: 'elevenlabs', voiceId: '21m00Tcm4TlvDq8ikWAM' },
transcriber: { provider: 'deepgram', language: 'en' },
firstMessage: 'Hi, this is ABC HVAC. How can I help you today?',
serverUrl: ${process.env.SERVER_URL}/webhook/vapi,
serverUrlSecret: process.env.VAPI_WEBHOOK_SECRET
};

const response = await fetch('https://api.vapi.ai/assistant', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.VAPI_API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(assistantConfig)
});

if (!response.ok) throw new Error(`Vapi assistant creation failed: ${response.status}`);
const { id: assistantId } = await response.json();

// Return TwiML to connect call to Vapi
res.type('text/xml');
res.send(`<?xml version="1.0" encoding="UTF-8"?>
  <Response>
    <Connect>
      <Stream url="wss://api.vapi.ai/stream/${assistantId}">
        <Parameter name="callId" value="${callId}" />
        <Parameter name="customerPhone" value="${customerPhone}" />
      </Stream>
    </Connect>
  </Response>`);
Enter fullscreen mode Exit fullscreen mode

} catch (error) {
console.error('Twilio inbound error:', error);
res.status(500).send('Call setup failed');
}
});

// Vapi webhook - handles function calls from assistant
app.post('/webhook/vapi', async (req, res) => {
const signature = req.headers['x-vapi-signature'];
const body = JSON.stringify(req.body);
const expectedSignature = crypto
.createHmac('sha256', process.env.VAPI_WEBHOOK_SECRET)
.update(body)
.digest('hex');

if (signature !== expectedSignature) {
return res.status(401).json({ error: 'Invalid signature' });
}

const event = req.body;

if (event.message?.type === 'function-call') {
const { functionCall } = event.message;

if (functionCall.name === 'scheduleHVACAppointment') {
  try {
    await ensureValidToken();
    const calendar = google.calendar({ version: 'v3', auth: oauth2Client });

    const { serviceType, requestedDate, timeWindow, customerName, customerPhone } = functionCall.parameters;
    const startTime = new Date(requestedDate);
    const endTime = new Date(startTime.getTime() + 60 * 60 * 1000); // 1 hour default

    const calendarResult = await calendar.events.insert({
      calendarId: 'primary',
      requestBody: {
        summary: `HVAC ${serviceType} - ${customerName}`,
        description: `Customer: ${customerName}\nPhone: ${customerPhone}\nService: ${serviceType}\nPreferred time: ${timeWindow}`,
        start: { dateTime: startTime.toISOString(), timeZone: 'America/New_York' },
        end: { dateTime: endTime.toISOString(), timeZone: 'America/New_York' }
      }
    });

    res.json({
      result: {
        success: true,
        scheduledDate: startTime.toISOString(),
        confirmationId: calendarResult.data.id,
        message: `Appointment scheduled for ${startTime.toLocaleString()}`
      }
    });
  } catch (error) {
    console.error('Calendar booking failed:', error);
    res.json({
      result: {
        success: false,
        error: 'Failed to book appointment. Please try again.'
      }
    });
  }
} else {
  res.json({ result: { error: 'Unknown function' } });
}
Enter fullscreen mode Exit fullscreen mode

} else {
res.json({ message: 'Event received' });
}
});

app.listen(3000, () => console.log('Server running on port 3000'));


## Run Instructions

**1. Install dependencies:**
Enter fullscreen mode Exit fullscreen mode


bash
npm install express googleapis


**2. Set environment variables:**
Enter fullscreen mode Exit fullscreen mode


bash
export VAPI_API_KEY="your_vapi_key"
export VAPI_WEBHOOK_SECRET="your_webhook_secret"
export GOOGLE_CLIENT_ID="your_google_client_id"
export GOOGLE_CLIENT_SECRET="your_google_client_secret"
export SERVER_URL="https://your-domain.ngrok.io"


**3. Start server and authenticate:**
Enter fullscreen mode Exit fullscreen mode


bash
node server.js

Visit http://localhost:3000/oauth/login to authorize Google Calendar




**4. Configure Twilio webhook:**
Set your Twilio phone number's webhook URL to `https://your-domain.ngrok.io/twilio/inbound

## FAQ

## Technical Questions

**How does Twilio route inbound calls to Vapi for voice AI processing?**

Twilio receives the inbound call and immediately forwards it to Vapi via a TwiML webhook. When a customer calls your HVAC business number (provisioned in Twilio), Twilio executes a `<Connect>` instruction that bridges the call to Vapi's telephony endpoint. Vapi then handles the entire conversation—transcription, LLM reasoning, function calling—while maintaining the active call session. The `assistantConfig` you define in Vapi determines how the voice AI responds to scheduling requests. Twilio acts purely as the telephony carrier; Vapi is the intelligence layer.

**What happens when the voice AI detects a scheduling request?**

When the customer says something like "I need an HVAC service on Tuesday," Vapi's LLM evaluates the input against your defined `functions`. If the request matches the scheduling function schema (checking `serviceType`, `requestedDate`, `timeWindow`), Vapi triggers a function call. This invokes your backend webhook, which validates the request, checks Google Calendar availability via the Calendar API, and returns available slots. Vapi then reads these options back to the customer in natural language. No manual intervention required.

**How do you prevent double-booking in Google Calendar?**

Your backend must query the Calendar API for existing events within the requested `timeWindow` before confirming availability. Use the `calendar.events.list()` method with `timeMin` and `timeMax` parameters set to your service duration (typically 1-2 hours for HVAC work). If conflicts exist, return alternative slots. Store the `tokenExpiry` from your OAuth2 token and refresh it before each Calendar API call using `ensureValidToken()` to avoid auth failures mid-conversation.

## Performance

**What's the typical latency from inbound call to first AI response?**

Expect 800ms–1.2s from call answer to Vapi's first greeting. This includes: Twilio call setup (100–200ms), Vapi session initialization (300–400ms), and initial TTS synthesis (300–600ms). Network jitter on mobile adds 100–300ms. If latency exceeds 1.5s, customers perceive silence and may hang up. Optimize by pre-warming Vapi sessions or using shorter `firstMessage` prompts.

**How many concurrent calls can this system handle?**

Scaling depends on your Vapi and Twilio plan limits. Twilio's standard tier supports 100+ concurrent calls per account. Vapi's concurrency limit varies by subscription (typically 10–50 simultaneous sessions). Google Calendar API has a quota of 1,000,000 requests per day per project. For an HVAC business handling 50 calls/day, you're well within limits. Monitor webhook response times; if Calendar API calls exceed 2s, implement request queuing to prevent timeout cascades.

## Platform Comparison

**Why use Vapi instead of building voice AI directly with Twilio's Speech Recognition?**

Twilio's built-in speech recognition (`<Gather>`) is basic and requires you to write all LLM logic yourself. Vapi abstracts the entire voice AI pipeline: transcription, LLM inference, function calling, and TTS. You define `assistantConfig` once, and Vapi handles context retention, interruption detection (barge-in), and error recovery. Twilio excels at call routing and billing; Vapi excels at conversation intelligence. Combined, they're unbeatable for voice automation.

**Can you use Google Calendar directly without OAuth2 token refresh?**

No. Google Calendar API tokens expire after 1 hour. Your backend must implement `ensureValidToken()` to refresh the `access_token` before each Calendar API call. Hardcoding a static token will fail after 60 minutes, breaking scheduling mid-conversation. Use a token store (Redis, database, or in-memory cache with TTL) to persist refresh tokens and rotate access tokens automatically.

## Resources

**VAPI**: Get Started with VAPI → [https://vapi.ai/?aff=misal](https://vapi.ai/?aff=misal)

**Official Documentation**
- [Vapi API Reference](https://docs.vapi.ai) – Assistant configuration, function calling, webhook events
- [Twilio Voice API](https://www.twilio.com/docs/voice) – Inbound call routing, SIP integration, call control
- [Google Calendar API](https://developers.google.com/calendar/api/guides/overview) – Event creation, OAuth 2.0 authentication, availability queries

**Integration Guides**
- [Twilio Webhooks](https://www.twilio.com/docs/usage/webhooks) – StatusCallback, VoiceCallback event handling
- [Google OAuth 2.0 Flow](https://developers.google.com/identity/protocols/oauth2) – Service account setup, token refresh patterns

**GitHub & Community**
- Vapi function calling examples: [vapi-ai/examples](https://github.com/vapi-ai/examples)
- Twilio Node.js SDK: [twilio/twilio-node](https://github.com/twilio/twilio-node)

## References

1. https://docs.vapi.ai/assistants/quickstart
2. https://docs.vapi.ai/quickstart/introduction
3. https://docs.vapi.ai/quickstart/web
4. https://docs.vapi.ai/quickstart/phone
5. https://docs.vapi.ai/chat/quickstart
6. https://docs.vapi.ai/outbound-campaigns/quickstart
7. https://docs.vapi.ai/workflows/quickstart
8. https://docs.vapi.ai/api-reference/calls/create-phone-call
9. https://docs.vapi.ai/assistants/structured-outputs-quickstart
10. https://docs.vapi.ai/assistants
11. https://docs.vapi.ai/server-url
12. https://docs.vapi.ai/server-url/developing-locally
13. https://docs.vapi.ai/

Enter fullscreen mode Exit fullscreen mode

Top comments (0)