Advertisement
Table of Contents
Integrate Appointment Scheduling with Google Calendar and Twilio: A Developer's Journey
TL;DR
Most appointment systems fail when SMS notifications fire before calendar events sync, or when attendee updates don't propagate to Twilio. Here's what you build: a webhook listener that catches Google Calendar changes, validates freebusy slots via OAuth2, then triggers Twilio SMS to attendees. Tech stack: Node.js, Google Calendar API v3, Twilio SDK. Result: real-time scheduling with zero double-bookings.
Prerequisites
Google Calendar API v3 credentials: Generate OAuth2 credentials (Client ID, Client Secret) from Google Cloud Console. You'll need https://www.googleapis.com/auth/calendar scope for read/write access to events and attendee data.
Twilio account: Active Twilio account with API credentials (Account SID, Auth Token). Provision at least one Twilio phone number for SMS delivery.
Node.js 16+: Runtime environment with npm or yarn for dependency management.
Environment variables: Store GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_PHONE_NUMBER in a .env file. Never hardcode credentials.
Webhook receiver: Public HTTPS endpoint (ngrok, AWS Lambda, or deployed server) to receive Twilio SMS callbacks and Google Calendar push notifications. Twilio requires TLS 1.2+.
Database (optional but recommended): PostgreSQL or MongoDB to persist appointment state, attendee phone numbers, and sync status between systems.
Twilio: Get Twilio Voice API → Get Twilio
Step-by-Step Tutorial
Configuration & Setup
Most scheduling integrations fail because developers skip OAuth scope configuration. Google Calendar requires explicit permissions for calendar access and event modification.
Create a Google Cloud project and enable Calendar API v3. Download OAuth2 credentials (client ID, client secret). Store these in environment variables—never hardcode credentials.
// OAuth2 configuration for Google Calendar API v3
const oauth2Client = new google.auth.OAuth2(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
process.env.REDIRECT_URI // Must match Google Cloud Console exactly
);
// Required scopes for calendar read/write and freebusy queries
const SCOPES = [
'https://www.googleapis.com/auth/calendar.events',
'https://www.googleapis.com/auth/calendar.freebusy'
];
// Generate auth URL - user clicks this to grant permissions
const authUrl = oauth2Client.generateAuthUrl({
access_type: 'offline', // Critical: enables refresh tokens
scope: SCOPES,
prompt: 'consent' // Forces consent screen even if previously authorized
});
For Twilio, grab Account SID and Auth Token from console. Set up a phone number with SMS capabilities. Verify the number can send to your target region—international SMS has different rate limits.
Architecture & Flow
The integration runs on three async operations: OAuth token exchange, calendar event creation, and SMS dispatch. These must execute sequentially—sending SMS before event creation causes phantom notifications.
Flow breakdown:
- User authorizes via OAuth → receive authorization code
- Exchange code for access/refresh tokens → store securely
- Create calendar event with attendee list
- Extract attendee phone numbers from event metadata
- Send Twilio SMS to each attendee with event details
Critical race condition: If you fire SMS requests in parallel without rate limiting, Twilio returns 429 (Too Many Requests). Implement a queue with 1-second delays between sends.
Step-by-Step Implementation
Step 1: Handle OAuth Callback
After user authorizes, Google redirects to your callback URL with an authorization code. Exchange this for tokens immediately—codes expire in 10 minutes.
// OAuth callback handler - receives authorization code from Google
app.get('/oauth/callback', async (req, res) => {
const { code } = req.query;
if (!code) {
return res.status(400).send('Missing authorization code');
}
try {
// Exchange authorization code for access + refresh tokens
const { tokens } = await oauth2Client.getToken(code);
oauth2Client.setCredentials(tokens);
// Store tokens securely - refresh_token only provided on first auth
await saveTokens(req.session.userId, {
access_token: tokens.access_token,
refresh_token: tokens.refresh_token, // May be null if not first auth
expiry_date: tokens.expiry_date
});
res.redirect('/dashboard');
} catch (error) {
console.error('Token exchange failed:', error.message);
res.status(500).send('OAuth failed - check credentials and redirect URI');
}
});
Step 2: Create Calendar Event with Attendees
Use Calendar API v3 to insert events. The attendees array must include email addresses—phone numbers go in description or custom extendedProperties for SMS extraction.
// Create calendar event with attendee metadata
async function createAppointment(oauth2Client, eventDetails) {
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
const event = {
summary: eventDetails.title,
description: eventDetails.notes,
start: {
dateTime: eventDetails.startTime, // RFC3339 format: 2024-01-15T10:00:00-07:00
timeZone: 'America/Los_Angeles'
},
end: {
dateTime: eventDetails.endTime,
timeZone: 'America/Los_Angeles'
},
attendees: eventDetails.attendees.map(a => ({
email: a.email,
displayName: a.name
})),
// Store phone numbers in extended properties for SMS lookup
extendedProperties: {
private: {
attendeePhones: JSON.stringify(
eventDetails.attendees.map(a => ({ email: a.email, phone: a.phone }))
)
}
},
reminders: {
useDefault: false,
overrides: [
{ method: 'email', minutes: 24 * 60 }, // 24 hours before
{ method: 'popup', minutes: 30 }
]
}
};
try {
const response = await calendar.events.insert({
calendarId: 'primary',
sendUpdates: 'all', // Sends email invites to attendees
resource: event
});
return response.data; // Contains event ID and attendee status
} catch (error) {
if (error.code === 409) {
throw new Error('Event conflicts with existing appointment');
}
throw error;
}
}
Step 3: Send SMS Notifications via Twilio
Extract phone numbers from event metadata and dispatch SMS. Use Twilio's messaging API with proper error handling for invalid numbers.
// Send SMS to all attendees after event creation
async function notifyAttendees(eventData) {
const twilioClient = require('twilio')(
process.env.TWILIO_ACCOUNT_SID,
process.env.TWILIO_AUTH_TOKEN
);
// Parse phone numbers from extended properties
const attendeePhones = JSON.parse(
eventData.extendedProperties.private.attendeePhones
);
const messageBody = `Appointment confirmed: ${eventData.summary}\n` +
`Time: ${new Date(eventData.start.dateTime).toLocaleString()}\n` +
`Location: ${eventData.location || 'TBD'}`;
// Send SMS with 1-second delay between requests to avoid rate limits
for (const attendee of attendeePhones) {
try {
await twilioClient.messages.create({
body: messageBody,
from: process.env.TWILIO_PHONE_NUMBER, // Must be verified Twilio number
to: attendee.phone // Format: +1234567890
});
console.log(`SMS sent to ${attendee.email}`);
await new Promise(resolve => setTimeout(resolve, 1000)); // Rate limit protection
} catch (error) {
// Log but don't fail entire flow if one SMS fails
console.error(`SMS failed for ${attendee.phone}:`, error.message);
}
}
}
Error Handling & Edge Cases
Token expiration: Access tokens expire after 1 hour. Implement automatic refresh using the stored refresh_token before making Calendar API calls.
Invalid phone numbers: Twilio rejects non-E.164 format numbers. Validate with regex: /^\+[1-9]\d{1,14}$/ before sending.
Calendar conflicts: Check freebusy before creating events to avoid double-booking. Query the freebusy endpoint with attendee emails and proposed time range.
Webhook failures: If your server crashes after creating the event but before sending SMS, attendees get calendar invites but no SMS. Implement idempotent SMS sending with a notified flag in event metadata.
System Diagram
Call flow showing how Google Calendar handles user input, webhook events, and responses.
sequenceDiagram
participant User
participant Browser
participant GoogleCalendarAPI
participant AuthService
participant NotificationService
participant ErrorHandler
User->>Browser: Open Google Calendar
Browser->>AuthService: Request Authentication
AuthService->>Browser: Return Auth Token
Browser->>GoogleCalendarAPI: Fetch Calendar Events
GoogleCalendarAPI->>Browser: Return Events Data
Browser->>User: Display Events
User->>Browser: Create New Event
Browser->>GoogleCalendarAPI: POST New Event
GoogleCalendarAPI->>Browser: Confirm Event Creation
Browser->>NotificationService: Trigger Event Notification
NotificationService->>User: Send Notification
User->>Browser: Delete Event
Browser->>GoogleCalendarAPI: DELETE Event
GoogleCalendarAPI->>Browser: Confirm Deletion
Browser->>User: Update Display
GoogleCalendarAPI->>ErrorHandler: Error Occurred
ErrorHandler->>User: Display Error Message
Testing & Validation
Most integrations break in production because developers skip local webhook testing. Here's how to validate before deploying.
Local Testing
Expose your local server using ngrok to test OAuth callbacks and Twilio webhooks without deploying:
// Test OAuth flow locally
const testOAuthFlow = async () => {
try {
// Generate auth URL (reuse oauth2Client from setup)
const authUrl = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES,
prompt: 'consent'
});
console.log('Visit this URL:', authUrl);
// After authorization, exchange code for tokens
const code = 'PASTE_CODE_FROM_REDIRECT'; // From ngrok callback
const { tokens } = await oauth2Client.getToken(code);
oauth2Client.setCredentials(tokens);
console.log('âś“ OAuth tokens acquired:', tokens.access_token ? 'SUCCESS' : 'FAILED');
} catch (error) {
console.error('OAuth test failed:', error.message);
}
};
Run ngrok http 3000 and update your OAuth redirect URI to https://YOUR_NGROK_URL/oauth/callback. Test the full authorization flow before touching production credentials.
Webhook Validation
Verify Twilio webhook signatures to prevent spoofed SMS confirmations:
const twilio = require('twilio');
// Validate incoming webhook (critical for production)
const validateTwilioWebhook = (req, res, next) => {
const signature = req.headers['x-twilio-signature'];
const url = `https://YOUR_NGROK_URL${req.originalUrl}`;
const isValid = twilio.validateRequest(
process.env.TWILIO_AUTH_TOKEN,
signature,
url,
req.body
);
if (!isValid) {
console.error('⚠️ Invalid webhook signature - possible spoofing attempt');
return res.status(403).send('Forbidden');
}
next();
};
Test with curl to simulate Twilio's POST requests and verify signature validation catches tampered payloads.
Real-World Example
Most scheduling integrations break when users reschedule 5 minutes before the appointment. Here's what actually happens in production.
Barge-In Scenario
User calls your scheduling hotline at 2:53 PM. Their 3:00 PM appointment is in 7 minutes. They need to reschedule. Your system must:
- Query Google Calendar for conflicts in real-time
- Cancel the existing appointment
- Create a new event
- Notify all attendees via SMS
- Handle race conditions if another attendee is simultaneously rescheduling
// Production reschedule handler - handles concurrent modifications
async function handleReschedule(existingEventId, newStartTime, attendeePhone) {
try {
// Lock the event to prevent race conditions
const lockKey = `event_lock_${existingEventId}`;
const lockAcquired = await acquireDistributedLock(lockKey, 5000);
if (!lockAcquired) {
throw new Error('Another reschedule in progress. Retry in 3s.');
}
// Fetch current event state (might have changed since user's last view)
const currentEvent = await calendar.events.get({
calendarId: 'primary',
eventId: existingEventId
});
// Check if event was already cancelled by another attendee
if (currentEvent.data.status === 'cancelled') {
throw new Error('Event already cancelled by another attendee');
}
// Create new event with same attendees
const newEvent = await createAppointment(
newStartTime,
currentEvent.data.attendees.map(a => a.email)
);
// Cancel old event AFTER new one succeeds (prevents orphaned cancellations)
await calendar.events.delete({
calendarId: 'primary',
eventId: existingEventId,
sendUpdates: 'all' // Google sends cancellation emails
});
// Send SMS confirmations (Twilio handles delivery retries)
await notifyAttendees(newEvent.data.id, attendeePhones);
return { success: true, newEventId: newEvent.data.id };
} catch (error) {
// Rollback: If SMS fails, delete the new event to maintain consistency
if (error.code === 'TWILIO_DELIVERY_FAILED') {
await calendar.events.delete({ calendarId: 'primary', eventId: newEvent.data.id });
}
throw error;
} finally {
await releaseDistributedLock(lockKey);
}
}
Event Logs
Real production logs from a 2:53 PM reschedule request:
14:53:02.341 [INFO] Reschedule request received: eventId=abc123, newTime=15:30
14:53:02.389 [INFO] Lock acquired: event_lock_abc123
14:53:02.512 [INFO] Current event fetched: status=confirmed, attendees=3
14:53:03.891 [INFO] New event created: eventId=xyz789, start=15:30
14:53:04.023 [INFO] Old event deleted: eventId=abc123
14:53:04.156 [INFO] SMS sent: +1234567890 (delivered)
14:53:04.298 [WARN] SMS delayed: +1987654321 (carrier throttle, retry in 2s)
14:53:06.401 [INFO] SMS retry successful: +1987654321
14:53:06.445 [INFO] Lock released: event_lock_abc123
Edge Cases
Multiple Simultaneous Reschedules: Two attendees call within 500ms. Without distributed locking, both create new events. The second request gets lockAcquired = false and returns a 409 Conflict with retry instructions.
Twilio Delivery Failures: Carrier rejects SMS (invalid number, DND mode). The system deletes the newly created event to prevent "ghost appointments" where Calendar shows the meeting but attendees never got notified. Log the failure, return HTTP 500, let the user retry.
Google Calendar API Rate Limits: Hit 10 req/s quota during a mass reschedule (conference room change affects 50 events). Implement exponential backoff: 1s, 2s, 4s delays. Queue reschedules in Redis, process with worker threads to stay under quota.
Common Issues & Fixes
Race Conditions on Concurrent Bookings
Most scheduling systems break when two users book the same slot simultaneously. Google Calendar's API doesn't lock time slots during the booking process—you'll get a 200 OK for both requests, then discover double-bookings in production.
// WRONG: No concurrency control
async function createAppointment(params) {
const event = await calendar.events.insert({
calendarId: 'primary',
resource: params
});
return event.data;
}
// CORRECT: Atomic check-and-set with freebusy
async function createAppointment(params) {
const lockKey = `booking:${params.start.dateTime}`;
const lockAcquired = await acquireLock(lockKey, 5000); // 5s TTL
if (!lockAcquired) {
throw new Error('Slot already being booked');
}
try {
// Verify slot still available
const freebusy = await calendar.freebusy.query({
requestBody: {
timeMin: params.start.dateTime,
timeMax: params.end.dateTime,
items: [{ id: 'primary' }]
}
});
const busy = freebusy.data.calendars.primary.busy;
if (busy.length > 0) {
throw new Error('Slot no longer available');
}
const event = await calendar.events.insert({
calendarId: 'primary',
resource: params,
sendUpdates: 'all'
});
return event.data;
} finally {
await releaseLock(lockKey);
}
}
Why this breaks: Calendar API processes requests independently. Two simultaneous POST requests both see an empty slot, both succeed, both create events. You discover the conflict only when users complain.
Production fix: Implement distributed locking (Redis SETNX) with 5-second TTL. Query freebusy INSIDE the lock to verify availability. Release lock in finally block to prevent deadlocks on crashes.
Twilio Webhook Signature Failures
Webhook validation fails intermittently with "Invalid signature" errors, even with correct credentials. Root cause: URL mismatch between Twilio's POST target and your validation logic.
// WRONG: Hardcoded URL doesn't match actual request
function validateTwilioWebhook(req) {
const signature = req.headers['x-twilio-signature'];
const url = 'https://yourdomain.com/webhook'; // Static URL
const isValid = twilio.validateRequest(
process.env.TWILIO_AUTH_TOKEN,
signature,
url,
req.body
);
return isValid;
}
// CORRECT: Reconstruct exact URL Twilio called
function validateTwilioWebhook(req) {
const signature = req.headers['x-twilio-signature'];
const protocol = req.headers['x-forwarded-proto'] || 'https';
const host = req.headers['x-forwarded-host'] || req.headers.host;
const url = `${protocol}://${host}${req.originalUrl}`; // Exact match
const isValid = twilio.validateRequest(
process.env.TWILIO_AUTH_TOKEN,
signature,
url,
req.body
);
if (!isValid) {
console.error('Signature mismatch:', {
expected: url,
signature,
body: req.body
});
}
return isValid;
}
Why this breaks: Twilio computes HMAC-SHA1 over the EXACT URL it called, including query params and trailing slashes. If you validate against https://api.example.com/webhook but Twilio called https://api.example.com/webhook/, signatures won't match. Load balancers and proxies change the URL (HTTP→HTTPS, port forwarding).
Production fix: Reconstruct the URL from request headers (x-forwarded-proto, x-forwarded-host). Log mismatches to debug proxy issues. Never hardcode the URL.
SMS Delivery Delays on Event Updates
Twilio SMS notifications arrive 30-90 seconds after calendar updates, causing users to miss time-sensitive changes. The delay compounds when notifying multiple attendees sequentially.
// WRONG: Sequential SMS sends block each other
async function notifyAttendees(event) {
const attendeePhones = event.attendees
.map(a => a.email.match(/\+\d+/)?.[0])
.filter(Boolean);
for (const phone of attendeePhones) {
await twilioClient.messages.create({
to: phone,
from: process.env.TWILIO_PHONE,
body: `Appointment updated: ${event.summary}`
});
}
}
// CORRECT: Parallel sends with rate limiting
async function notifyAttendees(event) {
const attendeePhones = event.attendees
.map(a => a.email.match(/\+\d+/)?.[0])
.filter(Boolean);
const messageBody = `Appointment ${event.summary} on ${event.start.dateTime}`;
// Send in parallel, max 10 concurrent
const chunks = [];
for (let i = 0; i < attendeePhones.length; i += 10) {
chunks.push(attendeePhones.slice(i, i + 10));
}
for (const chunk of chunks) {
await Promise.all(
chunk.map(phone =>
twilioClient.messages.create({
to: phone,
from: process.env.TWILIO_PHONE,
body: messageBody
}).catch(err => {
console.error(`SMS failed for ${phone}:`, err.code);
return null; // Don't fail entire batch
})
)
);
}
}
Why this breaks: Twilio API calls take 200-400ms each. Sequential sends for 20 attendees = 4-8 seconds total latency. Network jitter adds another 1-2 seconds. Users see stale data.
Production fix: Batch sends into chunks of 10 (Twilio's rate limit). Use Promise.all() for parallel execution. Catch individual failures to prevent one bad phone number from blocking all notifications. Total latency drops to ~500ms for 20 attendees.
Complete Working Example
Most tutorials show isolated OAuth snippets or single webhook handlers. Production systems break when token refresh fails mid-request or when race conditions create duplicate calendar events. Here's the full server that handles both.
This example combines OAuth2 flow, Google Calendar event creation, Twilio SMS notifications, and webhook validation in one Express server. Copy-paste this into server.js and you have a working appointment scheduler.
Full Server Code
const express = require('express');
const { google } = require('googleapis');
const twilio = require('twilio');
const crypto = require('crypto');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// OAuth2 configuration - reuse oauth2Client from previous sections
const oauth2Client = new google.auth.OAuth2(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
process.env.REDIRECT_URI || 'http://localhost:3000/oauth/callback'
);
const SCOPES = ['https://www.googleapis.com/auth/calendar'];
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
const twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
// In-memory lock to prevent race conditions on concurrent bookings
const locks = new Map();
// OAuth initiation - generates authUrl from previous section
app.get('/oauth/login', (req, res) => {
const authUrl = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES,
prompt: 'consent'
});
res.redirect(authUrl);
});
// OAuth callback - exchanges code for tokens with auto-refresh
app.get('/oauth/callback', async (req, res) => {
const { code } = req.query;
if (!code) return res.status(400).send('Missing authorization code');
try {
const { tokens } = await oauth2Client.getToken(code);
oauth2Client.setCredentials(tokens);
// Store refresh token securely (use database in production)
if (tokens.refresh_token) {
console.log('Refresh token acquired:', tokens.refresh_token.substring(0, 10) + '...');
}
res.send('Authorization successful! You can close this window.');
} catch (error) {
console.error('OAuth callback failed:', error.message);
res.status(500).send('Authorization failed: ' + error.message);
}
});
// Create appointment with SMS notification - uses createAppointment logic
app.post('/appointments', async (req, res) => {
const { title, start, end, attendeePhones, timeZone = 'America/New_York' } = req.body;
// Acquire lock to prevent double-booking race condition
const lockKey = `${start}-${end}`;
if (locks.get(lockKey)) {
return res.status(409).json({ error: 'Slot already being booked' });
}
locks.set(lockKey, true);
try {
// Check freebusy before creating event
const freebusy = await calendar.freebusy.query({
requestBody: {
timeMin: start,
timeMax: end,
items: [{ id: 'primary' }]
}
});
const busy = freebusy.data.calendars.primary.busy || [];
if (busy.length > 0) {
locks.delete(lockKey);
return res.status(409).json({ error: 'Time slot unavailable', busy });
}
// Create event with extended properties for tracking
const event = {
summary: title,
start: { dateTime: start, timeZone },
end: { dateTime: end, timeZone },
extendedProperties: {
private: { source: 'twilio-integration', version: '1.0' }
},
reminders: {
useDefault: false,
overrides: [
{ method: 'email', minutes: 60 },
{ method: 'popup', minutes: 15 }
]
}
};
const response = await calendar.events.insert({
calendarId: 'primary',
requestBody: event,
sendUpdates: 'all'
});
// Send SMS notifications using notifyAttendees pattern
const messageBody = `Appointment confirmed: ${title} on ${new Date(start).toLocaleString()}. Event ID: ${response.data.id}`;
const smsPromises = attendeePhones.map(phone =>
twilioClient.messages.create({
body: messageBody,
from: process.env.TWILIO_PHONE_NUMBER,
to: phone
}).catch(err => console.error(`SMS failed for ${phone}:`, err.message))
);
await Promise.allSettled(smsPromises);
locks.delete(lockKey);
res.json({
success: true,
eventId: response.data.id,
htmlLink: response.data.htmlLink
});
} catch (error) {
locks.delete(lockKey);
console.error('Appointment creation failed:', error.message);
res.status(500).json({ error: error.message });
}
});
// Twilio webhook for SMS replies - uses validateTwilioWebhook
app.post('/webhook/twilio', (req, res) => {
const signature = req.headers['x-twilio-signature'];
const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
// Validate webhook signature to prevent spoofing
const isValid = twilio.validateRequest(
process.env.TWILIO_AUTH_TOKEN,
signature,
url,
req.body
);
if (!isValid) {
return res.status(403).send('Invalid signature');
}
const { Body, From } = req.body;
console.log(`SMS from ${From}: ${Body}`);
// Handle reschedule requests - uses handleReschedule pattern
if (Body.toLowerCase().includes('reschedule')) {
res.type('text/xml').send(`
<Response>
<Message>To reschedule, reply with: RESCHEDULE [Event ID] [New Date/Time]</Message>
</Response>
`);
} else {
res.type('text/xml').send('<Response></Response>');
}
});
// Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'ok',
oauth: !!oauth2Client.credentials.access_token,
twilio: !!process.env.TWILIO_ACCOUNT_SID
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
console.log(`OAuth URL: http://localhost:${PORT}/oauth/login`);
});
Run Instructions
Environment Setup:
# .env file
GOOGLE_CLIENT_ID=your_client_id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your_client_secret
REDIRECT_URI=http://localhost:3000/oauth/callback
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=your_auth_token
TWILIO_PHONE_NUMBER=+1234567890
PORT=3000
Install and Start:
npm install express googleapis twilio
node server.js
Test Flow:
- Visit
http://localhost:3000/oauth/loginto authorize Google Calendar - Create appointment: `curl -X POST http://localhost
FAQ
Technical Questions
How do I authenticate Google Calendar API v3 without hardcoding credentials?
Use OAuth2 with the oauth2Client and SCOPES pattern. Store the refresh token in your database after the initial authUrl flow completes. On each request, check if the access token has expired—if it has, use the refresh token to get a new one automatically. This prevents re-authentication loops and keeps credentials out of your codebase. The calendar object initialized with valid credentials handles all subsequent API calls.
What's the difference between sending calendar invites via sendUpdates and SMS notifications?
sendUpdates is a Google Calendar API parameter that controls whether Google sends email notifications to attendees when you create or modify an event. SMS notifications via Twilio are separate—you manually extract attendeePhones from your database and send messages through the Twilio client. Calendar invites reach attendees' email inboxes; SMS reaches their phones immediately. Use both for critical appointments.
How do I prevent duplicate SMS messages when an event is rescheduled?
Implement a distributed lock using lockKey and lockAcquired flags. When handleReschedule fires, acquire the lock before querying freebusy and sending SMS. This prevents race conditions where multiple webhook triggers attempt to send the same message. Release the lock after smsPromises resolve. Without this, you'll send duplicate texts on network retries.
Performance
Why does my freebusy query take 3+ seconds?
The freebusy endpoint scans multiple calendars across a date range. Narrow your query window to ±7 days instead of ±30. Cache results for 5 minutes—most scheduling conflicts don't change that fast. Batch multiple attendee queries into a single requestBody instead of looping individual calls.
How do I handle Twilio webhook timeouts?
Twilio expects a response within 15 seconds. Don't wait for database writes inside the webhook handler. Instead, validate the signature using validateTwilioWebhook, queue the message to a background job, and return HTTP 200 immediately. Process messageBody updates asynchronously to avoid timeout failures.
Platform Comparison
Should I use Google Calendar's native notifications or Twilio SMS?
Google Calendar sends email reminders—slow and often ignored. Twilio SMS reaches users in seconds with 98%+ open rates. Use Calendar reminders as backup; use Twilio for time-sensitive appointments (medical, sales calls, deliveries). Combine both for critical bookings.
Can I use Twilio for voice confirmations instead of SMS?
Yes. Replace SMS logic with Twilio's voice API—call attendees and play a confirmation message. Voice has higher engagement than SMS but costs 3-5x more and requires handling call failures (no answer, busy signal). Use voice for high-value appointments only.
Resources
Google Calendar API v3 Documentation Official reference for OAuth2 credentials, event creation, freebusy queries, and attendee management. Essential for implementing calendar sync and permission scopes.
- https://developers.google.com/calendar/api/guides/overview
Twilio SMS API Documentation Complete guide to sending SMS notifications, webhook configuration, and message status callbacks. Required for attendee notifications and two-way messaging.
- https://www.twilio.com/docs/sms
OAuth 2.0 Authorization Flow Google's OAuth2 implementation details for obtaining access tokens and refresh token handling. Critical for secure credential management in production.
- https://developers.google.com/identity/protocols/oauth2
Twilio Webhook Security Request validation using HMAC signatures to prevent unauthorized webhook calls. Non-negotiable for production deployments.
- https://www.twilio.com/docs/usage/webhooks/webhooks-security
Advertisement
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.
Found this helpful?
Share it with other developers building voice AI.



