The Problem
Building a booking system is deceptively complex. Off-the-shelf solutions like Calendly charge recurring fees and lack customization, while coding from scratch means wrestling with time zone conversions, double-booking prevention, availability checking across multiple calendars, and OAuth2 authentication flows. Google Calendar’s API is powerful but notoriously difficult to implement correctly—mishandle the token refresh logic, and your system breaks silently; forget to check for conflicts, and you’ll double-book appointments. This integration solves the core problem: creating a production-ready booking system that syncs with Google Calendar, prevents conflicts, handles authentication securely, and gives you complete control over the user experience and business logic. Get it right, and you’ll have a scalable booking infrastructure without monthly SaaS fees.
Tech Stack & Prerequisites
- Node.js v20+ and npm/pnpm
- TypeScript 5+ (recommended for type safety)
- Google Cloud Project with Calendar API enabled
- Google OAuth2 credentials (Client ID and Client Secret)
- googleapis npm package v128+
- Express.js v4.18+ for web server
- dotenv for environment variables
- date-fns or moment-timezone for time zone handling
- PostgreSQL or MongoDB (optional, for booking persistence)
- Basic understanding of OAuth2 flow and REST APIs
Step-by-Step Implementation
Step 1: Setup
Initialize your Node.js project:
mkdir google-calendar-booking
cd google-calendar-booking
npm init -y
npm install googleapis express dotenv date-fns
npm install -D typescript @types/node @types/express tsx nodemon
Initialize TypeScript:
npx tsc --init
Update tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Create project structure:
mkdir src
touch src/index.ts src/config.ts src/google-calendar.ts src/booking.ts src/routes.ts
Update package.json scripts:
{
"scripts": {
"dev": "nodemon --exec tsx src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
}
Step 2: Configuration
2.1: Set Up Google Cloud Project
- Go to Google Cloud Console
- Create a new project or select existing
- Enable Google Calendar API:
- Navigate to “APIs & Services” → “Library”
- Search for “Google Calendar API”
- Click “Enable”
- Create OAuth2 credentials:
- Go to “APIs & Services” → “Credentials”
- Click “Create Credentials” → “OAuth client ID”
- Choose “Web application”
- Add authorized redirect URIs:
http://localhost:3000/auth/callback - Save Client ID and Client Secret
2.2: Configure Environment Variables
Create .env file:
# .env
PORT=3000
# Google OAuth2 credentials
GOOGLE_CLIENT_ID=your_client_id_here.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your_client_secret_here
GOOGLE_REDIRECT_URI=http://localhost:3000/auth/callback
# Calendar configuration
CALENDAR_ID=primary
TIMEZONE=America/New_York
# Optional: Service account for server-to-server
# GOOGLE_SERVICE_ACCOUNT_EMAIL=
# GOOGLE_PRIVATE_KEY=
Add to .gitignore:
echo ".env" >> .gitignore
echo "node_modules/" >> .gitignore
echo "dist/" >> .gitignore
echo "tokens.json" >> .gitignore
Create src/config.ts:
// src/config.ts
import dotenv from 'dotenv';
dotenv.config();
export const config = {
port: process.env.PORT || 3000,
google: {
clientId: process.env.GOOGLE_CLIENT_ID || '',
clientSecret: process.env.GOOGLE_CLIENT_SECRET || '',
redirectUri: process.env.GOOGLE_REDIRECT_URI || '',
},
calendar: {
calendarId: process.env.CALENDAR_ID || 'primary',
timeZone: process.env.TIMEZONE || 'America/New_York',
},
} as const;
// Validate required environment variables
const requiredEnvVars = [
'GOOGLE_CLIENT_ID',
'GOOGLE_CLIENT_SECRET',
'GOOGLE_REDIRECT_URI',
];
for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
throw new Error(`Missing required environment variable: ${envVar}`);
}
}
Step 3: Core Logic
3.1: Google Calendar Client
Create src/google-calendar.ts:
// src/google-calendar.ts
import { google } from 'googleapis';
import { OAuth2Client } from 'google-auth-library';
import { config } from './config';
import fs from 'fs/promises';
import path from 'path';
export interface TimeSlot {
start: string; // ISO 8601 format
end: string;
}
export interface BookingRequest {
summary: string;
description?: string;
attendeeEmail: string;
startTime: string;
endTime: string;
timeZone?: string;
}
export class GoogleCalendarClient {
private oauth2Client: OAuth2Client;
private calendar;
private tokenPath = path.join(__dirname, '../tokens.json');
constructor() {
this.oauth2Client = new google.auth.OAuth2(
config.google.clientId,
config.google.clientSecret,
config.google.redirectUri
);
this.calendar = google.calendar({ version: 'v3', auth: this.oauth2Client });
}
/**
* Generate authorization URL for OAuth2 flow
*/
getAuthUrl(): string {
const scopes = [
'https://www.googleapis.com/auth/calendar',
'https://www.googleapis.com/auth/calendar.events',
];
return this.oauth2Client.generateAuthUrl({
access_type: 'offline', // Important: gets refresh token
scope: scopes,
prompt: 'consent', // Force consent screen to get refresh token
});
}
/**
* Exchange authorization code for tokens
*/
async getTokenFromCode(code: string): Promise<void> {
try {
const { tokens } = await this.oauth2Client.getToken(code);
this.oauth2Client.setCredentials(tokens);
// Save tokens to file for persistence
await this.saveTokens(tokens);
console.log('Tokens saved successfully');
} catch (error) {
console.error('Error getting token from code:', error);
throw error;
}
}
/**
* Load tokens from file
*/
async loadTokens(): Promise<boolean> {
try {
const tokensData = await fs.readFile(this.tokenPath, 'utf-8');
const tokens = JSON.parse(tokensData);
this.oauth2Client.setCredentials(tokens);
// Set up automatic token refresh
this.oauth2Client.on('tokens', async (newTokens) => {
if (newTokens.refresh_token) {
tokens.refresh_token = newTokens.refresh_token;
}
tokens.access_token = newTokens.access_token;
tokens.expiry_date = newTokens.expiry_date;
await this.saveTokens(tokens);
});
return true;
} catch (error) {
console.log('No saved tokens found');
return false;
}
}
/**
* Save tokens to file
*/
private async saveTokens(tokens: any): Promise<void> {
await fs.writeFile(this.tokenPath, JSON.stringify(tokens, null, 2));
}
/**
* Check if calendar is authenticated
*/
isAuthenticated(): boolean {
const credentials = this.oauth2Client.credentials;
return !!(credentials && credentials.access_token);
}
/**
* Get available time slots for a specific date
*/
async getAvailableSlots(
date: string, // YYYY-MM-DD format
slotDuration: number = 30, // minutes
workingHours: { start: string; end: string } = { start: '09:00', end: '17:00' }
): Promise<TimeSlot[]> {
try {
// Parse date and working hours
const [startHour, startMinute] = workingHours.start.split(':').map(Number);
const [endHour, endMinute] = workingHours.end.split(':').map(Number);
const dayStart = new Date(`${date}T${workingHours.start}:00`);
const dayEnd = new Date(`${date}T${workingHours.end}:00`);
// Get busy times from calendar
const response = await this.calendar.freebusy.query({
requestBody: {
timeMin: dayStart.toISOString(),
timeMax: dayEnd.toISOString(),
timeZone: config.calendar.timeZone,
items: [{ id: config.calendar.calendarId }],
},
});
const busySlots = response.data.calendars?.[config.calendar.calendarId]?.busy || [];
// Generate all possible slots
const allSlots: TimeSlot[] = [];
let currentTime = new Date(dayStart);
while (currentTime < dayEnd) {
const slotEnd = new Date(currentTime.getTime() + slotDuration * 60000);
if (slotEnd <= dayEnd) {
allSlots.push({
start: currentTime.toISOString(),
end: slotEnd.toISOString(),
});
}
currentTime = slotEnd;
}
// Filter out busy slots
const availableSlots = allSlots.filter((slot) => {
const slotStart = new Date(slot.start);
const slotEnd = new Date(slot.end);
return !busySlots.some((busy) => {
const busyStart = new Date(busy.start!);
const busyEnd = new Date(busy.end!);
// Check if slot overlaps with busy time
return slotStart < busyEnd && slotEnd > busyStart;
});
});
return availableSlots;
} catch (error) {
console.error('Error getting available slots:', error);
throw error;
}
}
/**
* Check if a specific time slot is available
*/
async isSlotAvailable(startTime: string, endTime: string): Promise<boolean> {
try {
const response = await this.calendar.freebusy.query({
requestBody: {
timeMin: startTime,
timeMax: endTime,
timeZone: config.calendar.timeZone,
items: [{ id: config.calendar.calendarId }],
},
});
const busySlots = response.data.calendars?.[config.calendar.calendarId]?.busy || [];
return busySlots.length === 0;
} catch (error) {
console.error('Error checking slot availability:', error);
throw error;
}
}
/**
* Create a calendar event (booking)
*/
async createBooking(booking: BookingRequest): Promise<any> {
try {
// First check if slot is available
const isAvailable = await this.isSlotAvailable(
booking.startTime,
booking.endTime
);
if (!isAvailable) {
throw new Error('Time slot is not available');
}
// Create event
const event = {
summary: booking.summary,
description: booking.description,
start: {
dateTime: booking.startTime,
timeZone: booking.timeZone || config.calendar.timeZone,
},
end: {
dateTime: booking.endTime,
timeZone: booking.timeZone || config.calendar.timeZone,
},
attendees: [{ email: booking.attendeeEmail }],
reminders: {
useDefault: false,
overrides: [
{ method: 'email', minutes: 24 * 60 }, // 1 day before
{ method: 'popup', minutes: 30 }, // 30 minutes before
],
},
conferenceData: {
createRequest: {
requestId: `booking-${Date.now()}`,
conferenceSolutionKey: { type: 'hangoutsMeet' },
},
},
};
const response = await this.calendar.events.insert({
calendarId: config.calendar.calendarId,
requestBody: event,
conferenceDataVersion: 1, // Enable Google Meet
sendUpdates: 'all', // Send email notifications
});
console.log('Booking created:', response.data.id);
return response.data;
} catch (error) {
console.error('Error creating booking:', error);
throw error;
}
}
/**
* Cancel a booking
*/
async cancelBooking(eventId: string): Promise<void> {
try {
await this.calendar.events.delete({
calendarId: config.calendar.calendarId,
eventId: eventId,
sendUpdates: 'all', // Notify attendees
});
console.log('Booking cancelled:', eventId);
} catch (error) {
console.error('Error cancelling booking:', error);
throw error;
}
}
/**
* Update a booking
*/
async updateBooking(
eventId: string,
updates: Partial<BookingRequest>
): Promise<any> {
try {
// Get existing event
const existingEvent = await this.calendar.events.get({
calendarId: config.calendar.calendarId,
eventId: eventId,
});
// Check if new time slot is available (if time is being changed)
if (updates.startTime && updates.endTime) {
const isAvailable = await this.isSlotAvailable(
updates.startTime,
updates.endTime
);
if (!isAvailable) {
throw new Error('New time slot is not available');
}
}
// Prepare updated event
const updatedEvent = {
...existingEvent.data,
summary: updates.summary || existingEvent.data.summary,
description: updates.description || existingEvent.data.description,
start: updates.startTime
? {
dateTime: updates.startTime,
timeZone: updates.timeZone || config.calendar.timeZone,
}
: existingEvent.data.start,
end: updates.endTime
? {
dateTime: updates.endTime,
timeZone: updates.timeZone || config.calendar.timeZone,
}
: existingEvent.data.end,
};
const response = await this.calendar.events.update({
calendarId: config.calendar.calendarId,
eventId: eventId,
requestBody: updatedEvent,
sendUpdates: 'all',
});
console.log('Booking updated:', response.data.id);
return response.data;
} catch (error) {
console.error('Error updating booking:', error);
throw error;
}
}
/**
* List upcoming bookings
*/
async listUpcomingBookings(maxResults: number = 10): Promise<any[]> {
try {
const response = await this.calendar.events.list({
calendarId: config.calendar.calendarId,
timeMin: new Date().toISOString(),
maxResults: maxResults,
singleEvents: true,
orderBy: 'startTime',
});
return response.data.items || [];
} catch (error) {
console.error('Error listing bookings:', error);
throw error;
}
}
}
3.2: Booking Service Layer
Create src/booking.ts:
// src/booking.ts
import { GoogleCalendarClient, BookingRequest } from './google-calendar';
import { addMinutes, format, parseISO } from 'date-fns';
export interface AvailabilityRequest {
date: string; // YYYY-MM-DD
duration?: number; // minutes, default 30
}
export interface CreateBookingRequest {
date: string; // YYYY-MM-DD
time: string; // HH:MM
duration: number; // minutes
clientName: string;
clientEmail: string;
notes?: string;
}
export class BookingService {
private calendarClient: GoogleCalendarClient;
constructor() {
this.calendarClient = new GoogleCalendarClient();
}
async initialize(): Promise<void> {
const hasTokens = await this.calendarClient.loadTokens();
if (!hasTokens) {
console.log('No authentication tokens found.');
console.log('Please visit the /auth endpoint to authenticate.');
}
}
getAuthUrl(): string {
return this.calendarClient.getAuthUrl();
}
async handleAuthCallback(code: string): Promise<void> {
await this.calendarClient.getTokenFromCode(code);
}
isAuthenticated(): boolean {
return this.calendarClient.isAuthenticated();
}
/**
* Get available time slots for booking
*/
async getAvailability(request: AvailabilityRequest) {
if (!this.isAuthenticated()) {
throw new Error('Not authenticated with Google Calendar');
}
const duration = request.duration || 30;
const slots = await this.calendarClient.getAvailableSlots(
request.date,
duration
);
// Format slots for frontend
return slots.map((slot) => ({
start: slot.start,
end: slot.end,
startTime: format(parseISO(slot.start), 'HH:mm'),
endTime: format(parseISO(slot.end), 'HH:mm'),
available: true,
}));
}
/**
* Create a new booking
*/
async createBooking(request: CreateBookingRequest) {
if (!this.isAuthenticated()) {
throw new Error('Not authenticated with Google Calendar');
}
// Construct ISO datetime strings
const startDateTime = `${request.date}T${request.time}:00`;
const startDate = parseISO(startDateTime);
const endDate = addMinutes(startDate, request.duration);
const bookingRequest: BookingRequest = {
summary: `Booking with ${request.clientName}`,
description: request.notes || `Appointment scheduled for ${request.duration} minutes`,
attendeeEmail: request.clientEmail,
startTime: startDate.toISOString(),
endTime: endDate.toISOString(),
};
const event = await this.calendarClient.createBooking(bookingRequest);
return {
bookingId: event.id,
summary: event.summary,
startTime: event.start.dateTime,
endTime: event.end.dateTime,
meetLink: event.hangoutLink,
status: event.status,
};
}
/**
* Cancel a booking
*/
async cancelBooking(bookingId: string) {
if (!this.isAuthenticated()) {
throw new Error('Not authenticated with Google Calendar');
}
await this.calendarClient.cancelBooking(bookingId);
return {
success: true,
message: 'Booking cancelled successfully',
};
}
/**
* Get upcoming bookings
*/
async getUpcomingBookings(limit: number = 10) {
if (!this.isAuthenticated()) {
throw new Error('Not authenticated with Google Calendar');
}
const events = await this.calendarClient.listUpcomingBookings(limit);
return events.map((event) => ({
id: event.id,
summary: event.summary,
description: event.description,
startTime: event.start.dateTime,
endTime: event.end.dateTime,
attendees: event.attendees?.map((a: any) => a.email) || [],
meetLink: event.hangoutLink,
status: event.status,
}));
}
}
3.3: Express Routes
Create src/routes.ts:
// src/routes.ts
import { Router, Request, Response } from 'express';
import { BookingService } from './booking';
const router = Router();
const bookingService = new BookingService();
// Initialize on startup
bookingService.initialize();
/**
* GET /auth
* Redirect user to Google OAuth consent screen
*/
router.get('/auth', (req: Request, res: Response) => {
const authUrl = bookingService.getAuthUrl();
res.redirect(authUrl);
});
/**
* GET /auth/callback
* Handle OAuth callback from Google
*/
router.get('/auth/callback', async (req: Request, res: Response) => {
try {
const code = req.query.code as string;
if (!code) {
return res.status(400).json({ error: 'No authorization code provided' });
}
await bookingService.handleAuthCallback(code);
res.json({
success: true,
message: 'Successfully authenticated with Google Calendar',
});
} catch (error) {
console.error('Auth callback error:', error);
res.status(500).json({ error: 'Authentication failed' });
}
});
/**
* GET /availability
* Get available time slots for a specific date
* Query params: date (YYYY-MM-DD), duration (minutes, optional)
*/
router.get('/availability', async (req: Request, res: Response) => {
try {
const { date, duration } = req.query;
if (!date) {
return res.status(400).json({ error: 'Date parameter is required' });
}
const slots = await bookingService.getAvailability({
date: date as string,
duration: duration ? parseInt(duration as string) : undefined,
});
res.json({
date,
slots,
totalAvailable: slots.length,
});
} catch (error: any) {
console.error('Availability error:', error);
res.status(500).json({ error: error.message });
}
});
/**
* POST /bookings
* Create a new booking
*/
router.post('/bookings', async (req: Request, res: Response) => {
try {
const { date, time, duration, clientName, clientEmail, notes } = req.body;
// Validate required fields
if (!date || !time || !duration || !clientName || !clientEmail) {
return res.status(400).json({
error: 'Missing required fields: date, time, duration, clientName, clientEmail',
});
}
const booking = await bookingService.createBooking({
date,
time,
duration,
clientName,
clientEmail,
notes,
});
res.status(201).json({
success: true,
booking,
});
} catch (error: any) {
console.error('Booking creation error:', error);
res.status(500).json({ error: error.message });
}
});
/**
* GET /bookings
* List upcoming bookings
*/
router.get('/bookings', async (req: Request, res: Response) => {
try {
const limit = req.query.limit ? parseInt(req.query.limit as string) : 10;
const bookings = await bookingService.getUpcomingBookings(limit);
res.json({
bookings,
total: bookings.length,
});
} catch (error: any) {
console.error('List bookings error:', error);
res.status(500).json({ error: error.message });
}
});
/**
* DELETE /bookings/:id
* Cancel a booking
*/
router.delete('/bookings/:id', async (req: Request, res: Response) => {
try {
const { id } = req.params;
const result = await bookingService.cancelBooking(id);
res.json(result);
} catch (error: any) {
console.error('Booking cancellation error:', error);
res.status(500).json({ error: error.message });
}
});
/**
* GET /status
* Check authentication status
*/
router.get('/status', (req: Request, res: Response) => {
const isAuthenticated = bookingService.isAuthenticated();
res.json({
authenticated: isAuthenticated,
message: isAuthenticated
? 'Connected to Google Calendar'
: 'Not authenticated. Visit /auth to connect.',
});
});
export default router;
3.4: Main Application
Create src/index.ts:
// src/index.ts
import express from 'express';
import { config } from './config';
import routes from './routes';
const app = express();
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// CORS (configure appropriately for production)
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
next();
});
// Routes
app.use('/', routes);
// Health check
app.get('/', (req, res) => {
res.json({
name: 'Google Calendar Booking API',
version: '1.0.0',
status: 'running',
});
});
// Start server
app.listen(config.port, () => {
console.log(`Server running on http://localhost:${config.port}`);
console.log(`Authenticate at: http://localhost:${config.port}/auth`);
});
Step 4: Testing
4.1: Start the Server
Run the development server:
npm run dev
```
Expected output:
```
Server running on http://localhost:3000
Authenticate at: http://localhost:3000/auth
4.2: Authenticate with Google
- Open browser and navigate to:
http://localhost:3000/auth - Sign in with your Google account
- Grant calendar permissions
- You’ll be redirected to
/auth/callbackwith success message
4.3: Test API Endpoints with cURL or Postman
Check authentication status:
curl http://localhost:3000/status
Expected response:
{
"authenticated": true,
"message": "Connected to Google Calendar"
}
Get available slots:
curl "http://localhost:3000/availability?date=2026-02-25&duration=30"
Expected response:
{
"date": "2026-02-25",
"slots": [
{
"start": "2026-02-25T09:00:00.000Z",
"end": "2026-02-25T09:30:00.000Z",
"startTime": "09:00",
"endTime": "09:30",
"available": true
},
...
],
"totalAvailable": 16
}
Create a booking:
curl -X POST http://localhost:3000/bookings \
-H "Content-Type: application/json" \
-d '{
"date": "2026-02-25",
"time": "10:00",
"duration": 30,
"clientName": "John Doe",
"clientEmail": "john@example.com",
"notes": "Initial consultation"
}'
Expected response:
{
"success": true,
"booking": {
"bookingId": "abc123xyz",
"summary": "Booking with John Doe",
"startTime": "2026-02-25T10:00:00.000Z",
"endTime": "2026-02-25T10:30:00.000Z",
"meetLink": "https://meet.google.com/xxx-yyyy-zzz",
"status": "confirmed"
}
}
List upcoming bookings:
curl http://localhost:3000/bookings
Cancel a booking:
curl -X DELETE http://localhost:3000/bookings/abc123xyz
4.4: Test in Google Calendar
- Open Google Calendar web interface
- Verify the created event appears
- Check that attendee email received invitation
- Verify Google Meet link is included
4.5: Create Test Script
Create src/test.ts:
// src/test.ts
import axios from 'axios';
const API_BASE = 'http://localhost:3000';
async function runTests() {
console.log('Starting API tests...\n');
try {
// Test 1: Check status
console.log('Test 1: Check authentication status');
const statusRes = await axios.get(`${API_BASE}/status`);
console.log('Status:', statusRes.data);
console.log('✓ Passed\n');
// Test 2: Get availability
console.log('Test 2: Get availability for tomorrow');
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const dateStr = tomorrow.toISOString().split('T')[0];
const availabilityRes = await axios.get(
`${API_BASE}/availability?date=${dateStr}&duration=30`
);
console.log(`Available slots: ${availabilityRes.data.totalAvailable}`);
console.log('✓ Passed\n');
// Test 3: Create booking (only if slots available)
if (availabilityRes.data.totalAvailable > 0) {
console.log('Test 3: Create test booking');
const firstSlot = availabilityRes.data.slots[0];
const bookingRes = await axios.post(`${API_BASE}/bookings`, {
date: dateStr,
time: firstSlot.startTime,
duration: 30,
clientName: 'Test User',
clientEmail: 'test@example.com',
notes: 'Automated test booking',
});
console.log('Booking created:', bookingRes.data.booking.bookingId);
console.log('✓ Passed\n');
// Test 4: List bookings
console.log('Test 4: List upcoming bookings');
const listRes = await axios.get(`${API_BASE}/bookings`);
console.log(`Total bookings: ${listRes.data.total}`);
console.log('✓ Passed\n');
// Test 5: Cancel booking
console.log('Test 5: Cancel test booking');
await axios.delete(
`${API_BASE}/bookings/${bookingRes.data.booking.bookingId}`
);
console.log('✓ Passed\n');
}
console.log('All tests passed! ✓');
} catch (error: any) {
console.error('Test failed:', error.response?.data || error.message);
}
}
runTests();
Run tests: npx tsx src/test.ts
Common Errors & Troubleshooting
1. Error: “invalid_grant” or “Token has been expired or revoked”
Cause: Refresh token expired, was revoked by user, or never obtained because access_type: 'offline' wasn’t set during initial OAuth flow.
Fix: Delete tokens.json and re-authenticate with prompt: 'consent':
// In getAuthUrl()
return this.oauth2Client.generateAuthUrl({
access_type: 'offline', // Critical: must be 'offline'
scope: scopes,
prompt: 'consent', // Force consent screen every time
});
Delete tokens and re-auth:
rm tokens.json
# Visit /auth endpoint again
2. Error: “Calendar API has not been used in project before or it is disabled”
Cause: Google Calendar API is not enabled in your Google Cloud project.
Fix: Enable the API manually:
- Go to Google Cloud Console
- Select your project
- Navigate to “APIs & Services” → “Library”
- Search “Google Calendar API”
- Click “Enable”
Or enable via command line:
gcloud services enable calendar-json.googleapis.com --project=YOUR_PROJECT_ID
3. Error: Time slots showing as unavailable when calendar is empty
Cause: Time zone mismatch between your code and Google Calendar. The working hours you define don’t align with the calendar’s time zone.
Fix: Always use consistent time zones and ISO 8601 format:
// Ensure consistent timezone handling
import { toZonedTime, format } from 'date-fns-tz';
const timeZone = 'America/New_York';
// When creating date from string
const localDate = toZonedTime(`${date}T${time}:00`, timeZone);
const isoString = format(localDate, "yyyy-MM-dd'T'HH:mm:ssXXX", { timeZone });
// Always pass timeZone to Calendar API
const event = {
start: {
dateTime: isoString,
timeZone: timeZone, // Explicit timezone
},
// ...
};
Security Checklist
- Never commit tokens.json or .env files to version control – add them to
.gitignore - Use HTTPS only in production – HTTP exposes OAuth tokens and API keys
- Implement rate limiting on booking endpoints to prevent abuse and calendar spam
- Validate all user inputs before passing to Calendar API (email format, date ranges, duration limits)
- Set up OAuth consent screen properly in Google Cloud Console with accurate app information
- Restrict API key scope to only necessary permissions (use least privilege principle)
- Implement CORS properly – don’t use
*in production, whitelist specific domains - Add authentication to your API endpoints so only authorized users can create/cancel bookings
- Log all booking actions for audit trail and debugging
- Handle token refresh automatically using the
tokensevent listener to prevent service interruptions - Set webhook verification if using Google Calendar push notifications
- Monitor API usage in Google Cloud Console to detect anomalies
- Implement booking limits per user/email to prevent spam
- Validate time zones to prevent booking manipulation across different zones
- Add CAPTCHA or similar to public booking forms to prevent bot abuse
Need Expert Help?
Building production-ready integrations requires careful attention to edge cases, security, and scalability. If you need assistance with custom booking systems, calendar integrations, or SaaS development, schedule a consultation with our team. We specialize in turning complex API integrations into robust, maintainable solutions.

Qasim is a skilled backend developer known for designing secure, scalable, and efficient systems. His expertise in API development and database architecture ensures robust and reliable digital solutions.



