Table of Contents
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
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
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
};
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.
```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:
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:
// 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);
} 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.
Advertisement
Webhook Validation
Validate webhook signatures to prevent unauthorized calendar modifications. Vapi sends a x-vapi-secret header matching your serverUrlSecret.
// 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:
# 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:
// 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}`);
}
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);
}
}
res.sendStatus(200);
});
Event Logs
Real event sequence from production call (timestamps show the race condition):
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:
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;
}
}
// 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:
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:
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
// 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>`);
} 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' } });
}
} else {
res.json({ message: 'Event received' });
}
});
app.listen(3000, () => console.log('Server running on port 3000'));
Run Instructions
1. Install dependencies:
npm install express googleapis
2. Set environment variables:
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:
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
Official Documentation
- Vapi API Reference – Assistant configuration, function calling, webhook events
- Twilio Voice API – Inbound call routing, SIP integration, call control
- Google Calendar API – Event creation, OAuth 2.0 authentication, availability queries
Integration Guides
- Twilio Webhooks – StatusCallback, VoiceCallback event handling
- Google OAuth 2.0 Flow – Service account setup, token refresh patterns
GitHub & Community
- Vapi function calling examples: vapi-ai/examples
- Twilio Node.js SDK: twilio/twilio-node
References
- https://docs.vapi.ai/assistants/quickstart
- https://docs.vapi.ai/quickstart/introduction
- https://docs.vapi.ai/quickstart/web
- https://docs.vapi.ai/quickstart/phone
- https://docs.vapi.ai/chat/quickstart
- https://docs.vapi.ai/outbound-campaigns/quickstart
- https://docs.vapi.ai/workflows/quickstart
- https://docs.vapi.ai/api-reference/calls/create-phone-call
- https://docs.vapi.ai/assistants/structured-outputs-quickstart
- https://docs.vapi.ai/assistants
- https://docs.vapi.ai/server-url
- https://docs.vapi.ai/server-url/developing-locally
- https://docs.vapi.ai/
Written by
Voice AI Engineer & Creator
Building production voice AI systems and sharing what I learn. Focused on VAPI, LLM integrations, and real-time communication. Documenting the challenges most tutorials skip.
Tutorials in your inbox
Weekly voice AI tutorials and production tips. No spam.
Found this helpful?
Share it with other developers building voice AI.



