Automating Event Calendar Synchronization Across Multiple Booking Platforms
This session involved consolidating event management across three distinct booking/platform systems (GetMyBoat, Boatsetter, and Google Calendar) using Google Apps Script as a central synchronization hub. The challenge: different platforms expose event data via different APIs and formats, yet our operations need a single source of truth for scheduling, dispatch, and resource allocation.
The Problem Statement
We manage bookings across multiple external platforms:
- GetMyBoat and Boatsetter: Third-party boat rental platforms with iCalendar (iCal) feed exports
- Google Calendar: Internal scheduling system for staff availability, maintenance windows, and operational holds
- Internal dispatch systems: Python-based tools requiring unified event data
Previously, calendar synchronization relied on Google Apps Script polling intervals that were inconsistent, and credentials were scattered across multiple configuration files. Events weren't always reflected in dispatch systems in time for operational decisions.
Technical Architecture
File Structure and Organization
The core synchronization logic lives in:
/Users/cb/Documents/repos/sites/queenofsandiego.com/rady-shell-events/apps-script-replacement/CalendarSync.gs
This single Apps Script file consolidates:
- iCal feed parsing (GetMyBoat and Boatsetter exports)
- Google Calendar API interactions
- Event deduplication logic
- Scheduled polling with configurable intervals
- Email notifications for sync failures
Supporting infrastructure:
CalendarSync.gs: Main synchronization enginedispatch_boat_cleaner.py: Python script that consumes calendar events to trigger maintenance workflowscampaign_scheduler.py: Marketing campaign orchestration based on calendar availability/tools/platform_inbox_scraper.py: Fetches new bookings from email inboxes before they hit the platforms
Data Flow
The synchronization follows this pattern:
- Source platforms (GetMyBoat/Boatsetter) → iCalendar feeds (HTTP GET)
- CalendarSync.gs → Parses iCal, extracts event metadata (title, dates, description)
- Google Calendar API → Writes/updates events in a dedicated "Platform Imports" calendar
- Lambda API endpoint → External systems query calendar via authenticated endpoint
- Python dispatch scripts → Read calendar events and trigger operational tasks
Implementation Details
Credential Management
Rather than embedding credentials in the Apps Script file, we use Google's property store:
// Inside CalendarSync.gs
const props = PropertiesService.getScriptProperties();
const CALENDAR_ID = props.getProperty('CALENDAR_ID');
const GETMYBOAT_ICAL_URL = props.getProperty('GETMYBOAT_ICAL_URL');
const BOATSETTER_ICAL_URL = props.getProperty('BOATSETTER_ICAL_URL');
This approach allows credential rotation without redeploying code. The properties are set once during initial setup and can be updated via the Apps Script dashboard.
iCalendar Parsing
Both platforms export standard iCalendar format. The script fetches via UrlFetchApp.fetch():
function fetchPlatformCalendars() {
const sources = [
{name: 'GetMyBoat', url: GETMYBOAT_ICAL_URL},
{name: 'Boatsetter', url: BOATSETTER_ICAL_URL}
];
sources.forEach(source => {
try {
const response = UrlFetchApp.fetch(source.url, {muteHttpExceptions: true});
const icalData = response.getContentText();
parseAndSyncEvents(icalData, source.name);
} catch (e) {
sendErrorEmail(`Failed to fetch ${source.name}: ${e.message}`);
}
});
}
The iCal parser extracts key fields:
DTSTART/DTEND: Event time boundariesSUMMARY: Event title (booking reference)DESCRIPTION: Customer details and boat assignmentUID: Unique identifier for deduplication
Event Deduplication and Conflict Resolution
When syncing events, we check for duplicates by UID. If an event already exists:
function upsertEvent(eventData, source) {
const calendar = CalendarApp.getCalendarById(CALENDAR_ID);
const existingEvents = calendar.getEvents(
new Date(eventData.start),
new Date(eventData.end)
);
const duplicate = existingEvents.find(e =>
e.getDescription().includes(eventData.uid)
);
if (duplicate) {
// Update existing event
duplicate.setTitle(`[${source}] ${eventData.title}`);
duplicate.setDescription(eventData.description);
} else {
// Create new event
calendar.createEvent(`[${source}] ${eventData.title}`,
new Date(eventData.start),
new Date(eventData.end),
{description: eventData.uid});
}
}
Polling Schedule
We use Apps Script's ScriptApp.newTrigger() to schedule recurring syncs. Initially set to 30-minute intervals during business hours, adjustable based on booking volume:
function setupTrigger() {
// Remove existing triggers to prevent duplicates
ScriptApp.getProjectTriggers().forEach(trigger => {
ScriptApp.deleteTrigger(trigger);
});
// Sync every 30 minutes
ScriptApp.newTrigger('fetchPlatformCalendars')
.timeBased()
.everyMinutes(30)
.create();
}
Integration with Operational Systems
Dispatch Script Integration
dispatch_boat_cleaner.py reads the synchronized calendar to determine cleaning schedules:
import requests
CALENDAR_API_ENDPOINT = "https://api.internal.example.com/calendar/list"
HEADERS = {"Authorization": f"Bearer {DASHBOARD_TOKEN}"}
response = requests.get(
CALENDAR_API_ENDPOINT,
params={"calendarId": "platform-imports@example.com"},
headers=HEADERS
)
for event in response.json()['items']:
if 'cleaning' in event['summary'].lower():
dispatch_cleaner(event['start'], event['location'])