Fixing Boatsetter iCal Sync: OAuth Re-authorization and Trigger Activation in Google Apps Script
What Was Done
Boatsetter booking auto-sync wasn't working despite having the iCal URL integrated into the codebase. The root cause: the Google Apps Script (GAS) project had never been initialized with the required time-based triggers, and both Gmail and Calendar OAuth scopes had expired or been revoked. This post walks through the three-step fix process that got the sync pipeline operational.
The Problem: Three Blockers
- No trigger activation: The
calendarSyncSetup()function insites/queenofsandiego.com/CalendarSync.gs(line 355) was never executed, so the Apps Script scheduler had no trigger definitions registered. - Expired Gmail OAuth: The Gmail scope token used by
sendDailyReconciliation()had expired, blocking outbound reconciliation emails. - Revoked Calendar OAuth: The Google Calendar write scope had been revoked, preventing event creation even if the triggers fired.
Without all three of these pieces in place, the entire sync loop—fetch iCal → parse events → write to calendar → send summary email—would fail silently.
Technical Details: The Fix Process
Step 1: OAuth Re-authorization
Google Apps Script manages OAuth consent at the project level, not the individual deployment. When you run any function that requires a specific scope (Gmail, Calendar, etc.), GAS triggers the Google consent flow if the scope hasn't been authorized yet, or if the token has expired.
The fix: Run the testSync() function first (same file, line 563). This function exercises both the Calendar write scope and the Gmail scope:
// File: sites/queenofsandiego.com/CalendarSync.gs:563
function testSync() {
Logger.log("Testing Boatsetter iCal sync...");
syncAllChannels();
Logger.log("Test complete. Check execution log above.");
}
By running testSync(), the GAS editor will prompt the user with Google's OAuth consent dialog. You'll see something like "This app wants access to your Gmail account" and "This app wants to manage your Google Calendar." Clicking Allow grants the necessary scopes and refreshes the token.
Why this works: GAS scopes are declared in the manifest (appsscript.json) but only enforced when a function that uses those scopes actually executes. The testSync() function calls syncAllChannels(), which internally calls:
Calendar.Events.insert()→ requires Calendar scopeGmailApp.sendEmail()→ requires Gmail scope
Running the test forces these scopes to be evaluated, triggering the consent prompt.
Step 2: Activate Time-Based Triggers
Once OAuth is fixed, the actual trigger setup happens via calendarSyncSetup() (line 355):
// File: sites/queenofsandiego.com/CalendarSync.gs:355
function calendarSyncSetup() {
// Clear any existing triggers to avoid duplicates
const triggers = ScriptApp.getProjectTriggers();
triggers.forEach(t => ScriptApp.deleteTrigger(t));
// Register syncAllChannels to run every 30 minutes
ScriptApp.newTrigger("syncAllChannels")
.timeBased()
.everyMinutes(30)
.create();
// Register daily reconciliation email at 7:30 AM PT
ScriptApp.newTrigger("sendDailyReconciliation")
.timeBased()
.atHour(7)
.everyDays(1)
.create();
SpreadsheetApp.getActiveSpreadsheet()
.insertSheet("BookingLedger");
Logger.log("CalendarSync setup complete:");
Logger.log(" - BookingLedger tab created in ops sheet");
Logger.log(" - syncAllChannels: every 30 minutes");
Logger.log(" - sendDailyReconciliation: daily at 7:30am PT");
}
This function does two things:
- Registers time-based triggers: Uses
ScriptApp.newTrigger()to create two scheduled tasks. The 30-minute sync interval ensures Boatsetter bookings are pulled into Google Calendar within a reasonable window. The 7:30 AM PT reconciliation email (sent to operations) provides a daily summary of all bookings across channels. - Creates the BookingLedger sheet: This is the backing spreadsheet where sync metadata and booking records are stored.
After running this function, you can verify the triggers were created by checking the Apps Script editor sidebar: click the clock icon (Triggers) and you should see two entries: syncAllChannels and sendDailyReconciliation.
Step 3: Verify the Sync Works
Run testSync() again and monitor the execution log. You're looking for output like:
9:57:20 AM Notice Execution started
9:57:21 AM Info Fetching iCal from Boatsetter...
9:57:22 AM Info Parsed 3 events from Boatsetter iCal feed
9:57:23 AM Info Creating 3 calendar events in primary calendar
9:57:24 AM Info Synced Boatsetter bookings successfully
9:57:24 AM Notice Execution completed
If you see Exception: You do not have permission to access the requested resource, the OAuth re-auth didn't complete. Go back to Step 1 and ensure you clicked Allow on both consent prompts.
Infrastructure: GAS Project Details
Project URL: https://script.google.com/d/1HiEgjBrCGrnOIvr27nIk1E1qoR1pwKmWLigl191Tz0xq7LautJrIp9Ii/edit
Associated files:
CalendarSync.gs– Main sync logic, includes iCal parsing and Calendar API callsappsscript.json– Project manifest; declares required OAuth scopes:calendar,gmail,spreadsheetsICAL_FEEDSarray in CalendarSync.gs – Contains Boatsetter iCal URL and placeholders for future integrations (Viator, Airbnb, VRBO)
Related but separate GAS project: The Viator email scanner (jadaCalendarScanSetup() in JadaCalendarDashboard.gshttps://script.google.com/d/1dDpSK8JZda7XUpKIGlyyAX19KLL4JqFjYVtpcunB5ZE3-NMX_9v0lQJ5/edit