```html

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 engine
  • dispatch_boat_cleaner.py: Python script that consumes calendar events to trigger maintenance workflows
  • campaign_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:

  1. Source platforms (GetMyBoat/Boatsetter) → iCalendar feeds (HTTP GET)
  2. CalendarSync.gs → Parses iCal, extracts event metadata (title, dates, description)
  3. Google Calendar API → Writes/updates events in a dedicated "Platform Imports" calendar
  4. Lambda API endpoint → External systems query calendar via authenticated endpoint
  5. 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 boundaries
  • SUMMARY: Event title (booking reference)
  • DESCRIPTION: Customer details and boat assignment
  • UID: 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'])

Lambda API Endpoint